1
//! Functions to download or load directory objects, using the
2
//! state machines in the `states` module.
3

            
4
use std::num::NonZeroUsize;
5
use std::ops::Deref;
6
use std::result::Result as StdResult;
7
use std::{
8
    collections::HashMap,
9
    sync::{Arc, Weak},
10
    time::{Duration, SystemTime},
11
};
12

            
13
use crate::DirMgrConfig;
14
use crate::DocSource;
15
use crate::err::BootstrapAction;
16
use crate::state::{DirState, PoisonedState};
17
use crate::{
18
    DirMgr, DocId, DocQuery, DocumentText, Error, Readiness, Result,
19
    docid::{self, ClientRequest},
20
    upgrade_weak_ref,
21
};
22

            
23
use futures::FutureExt;
24
use futures::StreamExt;
25
use oneshot_fused_workaround as oneshot;
26
use tor_dirclient::DirResponse;
27
use tor_error::{info_report, warn_report};
28
use tor_rtcompat::Runtime;
29
use tor_rtcompat::scheduler::TaskSchedule;
30
use tracing::{debug, info, instrument, trace, warn};
31

            
32
use crate::storage::Store;
33
#[cfg(test)]
34
use std::sync::LazyLock;
35
#[cfg(test)]
36
use std::sync::Mutex;
37
use tor_circmgr::{CircMgr, DirInfo};
38
use tor_netdir::{NetDir, NetDirProvider as _};
39
use tor_netdoc::doc::netstatus::ConsensusFlavor;
40

            
41
/// Given a Result<()>, exit the current function if it is anything other than
42
/// Ok(), or a nonfatal error.
43
macro_rules! propagate_fatal_errors {
44
    ( $e:expr ) => {
45
        let v: Result<()> = $e;
46
        if let Err(e) = v {
47
            match e.bootstrap_action() {
48
                BootstrapAction::Nonfatal => {}
49
                _ => return Err(e),
50
            }
51
        }
52
    };
53
}
54

            
55
/// Identifier for an attempt to bootstrap a directory.
56
///
57
/// Every time that we decide to download a new directory, _despite already
58
/// having one_, counts as a new attempt.
59
///
60
/// These are used to track the progress of each attempt independently.
61
#[derive(Copy, Clone, Debug, derive_more::Display, Eq, PartialEq, Ord, PartialOrd)]
62
#[display("{0}", id)]
63
pub(crate) struct AttemptId {
64
    /// Which attempt at downloading a directory is this?
65
    id: NonZeroUsize,
66
}
67

            
68
impl AttemptId {
69
    /// Return a new unused AtomicUsize that will be greater than any previous
70
    /// one.
71
    ///
72
    /// # Panics
73
    ///
74
    /// Panics if we have exhausted the possible space of AtomicIds.
75
8
    pub(crate) fn next() -> Self {
76
        use std::sync::atomic::{AtomicUsize, Ordering};
77
        /// atomic used to generate the next attempt.
78
        static NEXT: AtomicUsize = AtomicUsize::new(1);
79
8
        let id = NEXT.fetch_add(1, Ordering::Relaxed);
80
8
        let id = id.try_into().expect("Allocated too many AttemptIds");
81
8
        Self { id }
82
8
    }
83
}
84

            
85
/// If there were errors from a peer in `outcome`, record those errors by
86
/// marking the circuit (if any) as needing retirement, and noting the peer
87
/// (if any) as having failed.
88
fn note_request_outcome<R: Runtime>(
89
    circmgr: &CircMgr<R>,
90
    outcome: &tor_dirclient::Result<tor_dirclient::DirResponse>,
91
) {
92
    use tor_dirclient::{Error::RequestFailed, RequestFailedError};
93
    // Extract an error and a source from this outcome, if there is one.
94
    //
95
    // This is complicated because DirResponse can encapsulate the notion of
96
    // a response that failed part way through a download: in the case, it
97
    // has some data, and also an error.
98
    let (err, source) = match outcome {
99
        Ok(req) => {
100
            if let (Some(e), Some(source)) = (req.error(), req.source()) {
101
                (
102
                    RequestFailed(RequestFailedError {
103
                        error: e.clone(),
104
                        source: Some(source.clone()),
105
                    }),
106
                    source,
107
                )
108
            } else {
109
                return;
110
            }
111
        }
112
        Err(
113
            error @ RequestFailed(RequestFailedError {
114
                source: Some(source),
115
                ..
116
            }),
117
        ) => (error.clone(), source),
118
        _ => return,
119
    };
120

            
121
    note_cache_error(circmgr, source, &err.into());
122
}
123

            
124
/// Record that a problem has occurred because of a failure in an answer from `source`.
125
fn note_cache_error<R: Runtime>(
126
    circmgr: &CircMgr<R>,
127
    source: &tor_dirclient::SourceInfo,
128
    problem: &Error,
129
) {
130
    use tor_circmgr::ExternalActivity;
131

            
132
    if !problem.indicates_cache_failure() {
133
        return;
134
    }
135

            
136
    // Does the error here tell us whom to really blame?  If so, blame them
137
    // instead.
138
    //
139
    // (This can happen if we notice a problem while downloading a certificate,
140
    // but the real problem is that the consensus was no good.)
141
    let real_source = match problem {
142
        Error::NetDocError {
143
            source: DocSource::DirServer { source: Some(info) },
144
            ..
145
        } => info,
146
        _ => source,
147
    };
148

            
149
    info_report!(problem, "Marking {:?} as failed", real_source);
150
    circmgr.note_external_failure(real_source.cache_id(), ExternalActivity::DirCache);
151
    circmgr.retire_circ(source.unique_circ_id());
152
}
153

            
154
/// Record that `source` has successfully given us some directory info.
155
fn note_cache_success<R: Runtime>(circmgr: &CircMgr<R>, source: &tor_dirclient::SourceInfo) {
156
    use tor_circmgr::ExternalActivity;
157

            
158
    trace!("Marking {:?} as successful", source);
159
    circmgr.note_external_success(source.cache_id(), ExternalActivity::DirCache);
160
}
161

            
162
/// Load every document in `missing` and try to apply it to `state`.
163
12
fn load_and_apply_documents<R: Runtime>(
164
12
    missing: &[DocId],
165
12
    dirmgr: &Arc<DirMgr<R>>,
166
12
    state: &mut Box<dyn DirState>,
167
12
    changed: &mut bool,
168
12
) -> Result<()> {
169
    /// How many documents will we try to load at once?  We try to keep this from being too large,
170
    /// to avoid excessive RAM usage.
171
    ///
172
    /// TODO: we may well want to tune this.
173
    const CHUNK_SIZE: usize = 256;
174
12
    for chunk in missing.chunks(CHUNK_SIZE) {
175
12
        let documents = {
176
12
            let store = dirmgr.store.lock().expect("store lock poisoned");
177
12
            load_documents_from_store(chunk, &**store)?
178
        };
179

            
180
12
        state.add_from_cache(documents, changed)?;
181
    }
182

            
183
12
    Ok(())
184
12
}
185

            
186
/// Load a set of documents from a `Store`, returning all documents found in the store.
187
/// Note that this may be less than the number of documents in `missing`.
188
12
fn load_documents_from_store(
189
12
    missing: &[DocId],
190
12
    store: &dyn Store,
191
12
) -> Result<HashMap<DocId, DocumentText>> {
192
12
    let mut loaded = HashMap::new();
193
12
    for query in docid::partition_by_type(missing.iter().copied()).values() {
194
12
        query.load_from_store_into(&mut loaded, store)?;
195
    }
196
12
    Ok(loaded)
197
12
}
198

            
199
/// Try to update `state` by loading cached information, on success returns the
200
/// current wall-clock time.
201
8
fn update_state<R: Runtime>(
202
8
    dirmgr: &Arc<DirMgr<R>>,
203
8
    attempt_id: AttemptId,
204
8
    state: &mut Box<dyn DirState>,
205
8
) -> Result<SystemTime> {
206
8
    let state_desc = state.describe();
207
8
    let mut changed = false;
208
8
    trace!(attempt=%attempt_id, state=%state_desc,"Attempting to load directory information from cache.");
209
8
    let load_result = load_once(dirmgr, state, attempt_id, &mut changed);
210
8
    trace!(attempt=%attempt_id, state=%state_desc, outcome=?load_result, "Load attempt complete.");
211
8
    if let Err(e) = &load_result {
212
        // If the load failed but the error can be blamed on a directory
213
        // cache, do so.
214
        if let Some(source) = e.responsible_cache() {
215
            dirmgr.note_errors(attempt_id, 1);
216
            note_cache_error(dirmgr.circmgr()?.deref(), source, e);
217
        }
218
8
    }
219
8
    propagate_fatal_errors!(load_result);
220
8
    Ok(dirmgr.runtime.wallclock())
221
8
}
222

            
223
/// Applies changes from `state` to the manager's netdir.
224
///
225
/// Updates and broadcasts download progress based on the current `state`.
226
/// Panics if manager's store lock is poisoned.
227
// TODO(eta): Consider deprecating state.is_ready().
228
16
fn apply_state<R: Runtime>(
229
16
    dirmgr: &Arc<DirMgr<R>>,
230
16
    state: &mut Box<dyn DirState>,
231
16
    attempt_id: AttemptId,
232
16
) -> Result<()> {
233
16
    let mut store = dirmgr.store.lock().expect("store lock poisoned");
234
16
    dirmgr.apply_netdir_changes(state, &mut **store)?;
235
16
    dirmgr.update_progress(attempt_id, state.bootstrap_progress());
236
16
    Ok(())
237
16
}
238

            
239
/// Errors returned by `advance_state`.
240
enum AdvanceStateError {
241
    /// The directory has already been downloaded completely.
242
    AlreadyComplete,
243
    /// The state can't currently be advanced.
244
    CantAdvanceYet,
245
}
246

            
247
/// Advances the progress of the directory download.
248
///
249
/// Returns `Ok(())` if the state was successfully advanced.
250
///
251
/// # Errors
252
///
253
/// - Returns `AdvanceStateError::AlreadyComplete` if the directory has already been
254
///   fully downloaded.
255
/// - Returns `AdvanceStateError::CantAdvanceYet` if the directory is incomplete and
256
///   the download cannot advance yet.
257
10
fn advance_state(
258
10
    state: &mut Box<dyn DirState>,
259
10
    attempt_id: AttemptId,
260
10
) -> StdResult<(), AdvanceStateError> {
261
10
    if state.is_ready(Readiness::Complete) {
262
4
        trace!(attempt=%attempt_id, state=%state.describe(), "Directory is now Complete.");
263
4
        return Err(AdvanceStateError::AlreadyComplete);
264
6
    }
265

            
266
6
    if state.can_advance() {
267
4
        advance(state);
268
4
        trace!(attempt=%attempt_id, state=%state.describe(), "State has advanced.");
269
4
        return Ok(());
270
2
    }
271

            
272
2
    Err(AdvanceStateError::CantAdvanceYet)
273
10
}
274

            
275
/// Return value of `perform_download`.
276
///
277
/// Describes the outcome of the attempt to download the current directory's state.
278
enum DownloadOutcome {
279
    /// the download failed with a non-fatal error.
280
    DownloadFailed,
281
    /// The directory being downloaded is outdated.
282
    DirectoryOutdated,
283
    /// The download attempt was successful, and the result has been applied to
284
    /// the state.
285
    Applied,
286
}
287

            
288
/// Attempts to download the current directory's state.
289
///
290
/// On success, it reports the outcome of the download using a
291
/// `DownloadOutcome` instance and updates `now` to the current
292
/// wallclock time.
293
///
294
/// The possible outcomes are:
295
/// - `DownloadOutcome::DownloadFailed`: the download failed with a non-fatal
296
///   error; the caller should proceed to the next state.
297
/// - `DownloadOutcome::DirectoryOutdated`: The directory being downloaded is
298
///   outdated. The caller should attempt a new download. As a side effect,
299
///   the directory state has been reset.
300
/// - `DownloadOutcome::Applied`: The download attempt was successful, and the
301
///   result has been applied to the state.
302
///
303
/// Returns an error only if a fatal error occurred.
304
2
async fn perform_download<R: Runtime>(
305
2
    attempt_id: AttemptId,
306
2
    dirmgr: &Weak<DirMgr<R>>,
307
2
    now: &mut SystemTime,
308
2
    parallelism: u8,
309
2
    schedule: &mut TaskSchedule<R>,
310
2
    state: &mut Box<dyn DirState>,
311
2
) -> Result<DownloadOutcome> {
312
2
    let reset_time = no_more_than_a_week_from(*now, state.reset_time());
313

            
314
    *now = {
315
2
        let dirmgr = upgrade_weak_ref(dirmgr)?;
316
2
        futures::select_biased! {
317
2
            outcome = download_attempt(&dirmgr, state, parallelism.into(), attempt_id).fuse() => {
318
2
                if let Err(e) = outcome {
319
                    warn_report!(e, attempt=%attempt_id, "Error while downloading.");
320
                    propagate_fatal_errors!(Err(e));
321
                    return Ok(DownloadOutcome::DownloadFailed);
322
                } else {
323
2
                    trace!(attempt=%attempt_id, "Successfully downloaded some information.");
324
                }
325
            }
326
2
            _ = schedule.sleep_until_wallclock(reset_time).fuse() => {
327
                // We need to reset. This can happen if (for
328
                // example) we're downloading the last few
329
                // microdescriptors on a consensus that now
330
                // we're ready to replace.
331
                info!(attempt=%attempt_id, "Directory being fetched is now outdated; resetting download state.");
332
                reset(state);
333
                return Ok(DownloadOutcome::DirectoryOutdated);
334
            },
335
        };
336
2
        dirmgr.runtime.wallclock()
337
    };
338
2
    Ok(DownloadOutcome::Applied)
339
2
}
340

            
341
/// Construct an appropriate ClientRequest to download a consensus
342
/// of the given flavor.
343
8
pub(crate) fn make_consensus_request(
344
8
    now: SystemTime,
345
8
    flavor: ConsensusFlavor,
346
8
    store: &dyn Store,
347
8
    config: &DirMgrConfig,
348
8
) -> Result<ClientRequest> {
349
8
    let mut request = tor_dirclient::request::ConsensusRequest::new(flavor);
350

            
351
8
    let default_cutoff = crate::default_consensus_cutoff(now, &config.tolerance)?;
352

            
353
8
    match store.latest_consensus_meta(flavor) {
354
4
        Ok(Some(meta)) => {
355
4
            let valid_after = meta.lifetime().valid_after();
356
4
            request.set_last_consensus_date(std::cmp::max(valid_after, default_cutoff));
357
4
            request.push_old_consensus_digest(*meta.sha3_256_of_signed());
358
4
        }
359
4
        latest => {
360
4
            if let Err(e) = latest {
361
                warn_report!(e, "Error loading directory metadata");
362
4
            }
363
            // If we don't have a consensus, then request one that's
364
            // "reasonably new".  That way, our clock is set far in the
365
            // future, we won't download stuff we can't use.
366
4
            request.set_last_consensus_date(default_cutoff);
367
        }
368
    }
369

            
370
8
    request.set_skew_limit(
371
        // If we are _fast_ by at least this much, then any valid directory will
372
        // seem to be at least this far in the past.
373
8
        config.tolerance.post_valid_tolerance(),
374
        // If we are _slow_ by this much, then any valid directory will seem to
375
        // be at least this far in the future.
376
8
        config.tolerance.pre_valid_tolerance(),
377
    );
378

            
379
8
    Ok(ClientRequest::Consensus(request))
380
8
}
381

            
382
/// Construct a set of `ClientRequest`s in order to fetch the documents in `docs`.
383
14
pub(crate) fn make_requests_for_documents<R: Runtime>(
384
14
    rt: &R,
385
14
    docs: &[DocId],
386
14
    store: &dyn Store,
387
14
    config: &DirMgrConfig,
388
14
) -> Result<Vec<ClientRequest>> {
389
14
    let mut res = Vec::new();
390
18
    for q in docid::partition_by_type(docs.iter().copied())
391
14
        .into_values()
392
14
        .flat_map(|x| x.split_for_download().into_iter())
393
    {
394
18
        match q {
395
4
            DocQuery::LatestConsensus { flavor, .. } => {
396
4
                res.push(make_consensus_request(
397
4
                    rt.wallclock(),
398
4
                    flavor,
399
4
                    store,
400
4
                    config,
401
                )?);
402
            }
403
2
            DocQuery::AuthCert(ids) => {
404
2
                res.push(ClientRequest::AuthCert(ids.into_iter().collect()));
405
2
            }
406
8
            DocQuery::Microdesc(ids) => {
407
8
                res.push(ClientRequest::Microdescs(ids.into_iter().collect()));
408
8
            }
409
            #[cfg(feature = "routerdesc")]
410
4
            DocQuery::RouterDesc(ids) => {
411
4
                res.push(ClientRequest::RouterDescs(ids.into_iter().collect()));
412
4
            }
413
        }
414
    }
415
14
    Ok(res)
416
14
}
417

            
418
/// Launch a single client request and get an associated response.
419
#[instrument(level = "trace", skip_all)]
420
async fn fetch_single<R: Runtime>(
421
    rt: &R,
422
    request: ClientRequest,
423
    current_netdir: Option<&NetDir>,
424
    circmgr: Arc<CircMgr<R>>,
425
) -> Result<(ClientRequest, DirResponse)> {
426
    let dirinfo: DirInfo = match current_netdir {
427
        Some(netdir) => netdir.into(),
428
        None => tor_circmgr::DirInfo::Nothing,
429
    };
430
    let outcome =
431
        tor_dirclient::get_resource(request.as_requestable(), dirinfo, rt, circmgr.clone()).await;
432

            
433
    note_request_outcome(&circmgr, &outcome);
434

            
435
    let resource = outcome?;
436
    Ok((request, resource))
437
}
438

            
439
/// Testing helper: if this is Some, then we return it in place of any
440
/// response to fetch_multiple.
441
///
442
/// Note that only one test uses this: otherwise there would be a race
443
/// condition. :p
444
#[cfg(test)]
445
2
static CANNED_RESPONSE: LazyLock<Mutex<Vec<String>>> = LazyLock::new(|| Mutex::new(vec![]));
446

            
447
/// Launch a set of download requests for a set of missing objects in
448
/// `missing`, and return each request along with the response it received.
449
///
450
/// Don't launch more than `parallelism` requests at once.
451
#[instrument(level = "trace", skip_all)]
452
2
async fn fetch_multiple<R: Runtime>(
453
2
    dirmgr: Arc<DirMgr<R>>,
454
2
    attempt_id: AttemptId,
455
2
    missing: &[DocId],
456
2
    parallelism: usize,
457
2
) -> Result<Vec<(ClientRequest, DirResponse)>> {
458
    let requests = {
459
        let store = dirmgr.store.lock().expect("store lock poisoned");
460
        make_requests_for_documents(&dirmgr.runtime, missing, &**store, &dirmgr.config.get())?
461
    };
462

            
463
    trace!(attempt=%attempt_id, "Launching {} requests for {} documents",
464
           requests.len(), missing.len());
465

            
466
    #[cfg(test)]
467
    {
468
        let m = CANNED_RESPONSE.lock().expect("Poisoned mutex");
469
        if !m.is_empty() {
470
            return Ok(requests
471
                .into_iter()
472
                .zip(m.iter().map(DirResponse::from_get_body))
473
                .collect());
474
        }
475
    }
476

            
477
    let circmgr = dirmgr.circmgr()?;
478
    // Only use timely directories for bootstrapping directories; otherwise, we'll try fallbacks.
479
    let netdir = dirmgr.netdir(tor_netdir::Timeliness::Timely).ok();
480

            
481
    // TODO: instead of waiting for all the queries to finish, we
482
    // could stream the responses back or something.
483
    let responses: Vec<Result<(ClientRequest, DirResponse)>> = futures::stream::iter(requests)
484
        .map(|query| fetch_single(&dirmgr.runtime, query, netdir.as_deref(), circmgr.clone()))
485
        .buffer_unordered(parallelism)
486
        .collect()
487
        .await;
488

            
489
    let mut useful_responses = Vec::new();
490
    for r in responses {
491
        // TODO: on some error cases we might want to stop using this source.
492
        match r {
493
            Ok((request, response)) => {
494
                if response.status_code() == 200 {
495
                    useful_responses.push((request, response));
496
                } else {
497
                    trace!(
498
                        "cache declined request; reported status {:?}",
499
                        response.status_code()
500
                    );
501
                }
502
            }
503
            Err(e) => warn_report!(e, "error while downloading"),
504
        }
505
    }
506

            
507
    trace!(attempt=%attempt_id, "received {} useful responses from our requests.", useful_responses.len());
508

            
509
    Ok(useful_responses)
510
2
}
511

            
512
/// Try to update `state` by loading cached information from `dirmgr`.
513
14
fn load_once<R: Runtime>(
514
14
    dirmgr: &Arc<DirMgr<R>>,
515
14
    state: &mut Box<dyn DirState>,
516
14
    attempt_id: AttemptId,
517
14
    changed_out: &mut bool,
518
14
) -> Result<()> {
519
14
    let missing = state.missing_docs();
520
14
    let mut changed = false;
521
14
    let outcome: Result<()> = if missing.is_empty() {
522
2
        trace!("Found no missing documents; can't advance current state");
523
2
        Ok(())
524
    } else {
525
12
        trace!(
526
            "Found {} missing documents; trying to load them",
527
            missing.len()
528
        );
529

            
530
12
        load_and_apply_documents(&missing, dirmgr, state, &mut changed)
531
    };
532

            
533
    // We have to update the status here regardless of the outcome, if we got
534
    // any information: even if there was an error, we might have received
535
    // partial information that changed our status.
536
14
    if changed {
537
12
        dirmgr.update_progress(attempt_id, state.bootstrap_progress());
538
12
        *changed_out = true;
539
12
    }
540

            
541
14
    outcome
542
14
}
543

            
544
/// Try to load as much state as possible for a provided `state` from the
545
/// cache in `dirmgr`, advancing the state to the extent possible.
546
///
547
/// No downloads are performed; the provided state will not be reset.
548
2
pub(crate) fn load<R: Runtime>(
549
2
    dirmgr: &Arc<DirMgr<R>>,
550
2
    mut state: Box<dyn DirState>,
551
2
    attempt_id: AttemptId,
552
2
) -> Result<Box<dyn DirState>> {
553
2
    let mut safety_counter = 0_usize;
554
    loop {
555
6
        trace!(attempt=%attempt_id, state=%state.describe(), "Loading from cache");
556
6
        let mut changed = false;
557
6
        let outcome = load_once(dirmgr, &mut state, attempt_id, &mut changed);
558
6
        apply_state(dirmgr, &mut state, attempt_id)?;
559
6
        trace!(attempt=%attempt_id, ?outcome, "Load operation completed.");
560

            
561
6
        if let Err(e) = outcome {
562
            match e.bootstrap_action() {
563
                BootstrapAction::Nonfatal => {
564
                    debug!("Recoverable error loading from cache: {}", e);
565
                }
566
                BootstrapAction::Fatal | BootstrapAction::Reset => {
567
                    return Err(e);
568
                }
569
            }
570
6
        }
571

            
572
6
        if state.can_advance() {
573
2
            state = state.advance();
574
2
            trace!(attempt=%attempt_id, state=state.describe(), "State has advanced.");
575
2
            safety_counter = 0;
576
        } else {
577
4
            if !changed {
578
                // TODO: Are there more nonfatal errors that mean we should
579
                // break?
580
2
                trace!(attempt=%attempt_id, state=state.describe(), "No state advancement after load; nothing more to find in the cache.");
581
2
                break;
582
2
            }
583
2
            safety_counter += 1;
584
2
            assert!(
585
2
                safety_counter < 100,
586
                "Spent 100 iterations in the same state: this is a bug"
587
            );
588
        }
589
    }
590

            
591
2
    Ok(state)
592
2
}
593

            
594
/// Helper: Make a set of download attempts for the current directory state,
595
/// and on success feed their results into the state object.
596
///
597
/// This can launch one or more download requests, but will not launch more
598
/// than `parallelism` requests at a time.
599
#[instrument(level = "trace", skip_all)]
600
2
async fn download_attempt<R: Runtime>(
601
2
    dirmgr: &Arc<DirMgr<R>>,
602
2
    state: &mut Box<dyn DirState>,
603
2
    parallelism: usize,
604
2
    attempt_id: AttemptId,
605
2
) -> Result<()> {
606
    let missing = state.missing_docs();
607
    let fetched = fetch_multiple(Arc::clone(dirmgr), attempt_id, &missing, parallelism).await?;
608
    let mut n_errors = 0;
609
    for (client_req, dir_response) in fetched {
610
        let source = dir_response.source().cloned();
611
        let text = match String::from_utf8(dir_response.into_output_unchecked())
612
            .map_err(Error::BadUtf8FromDirectory)
613
        {
614
            Ok(t) => t,
615
            Err(e) => {
616
                if let Some(source) = source {
617
                    n_errors += 1;
618
                    note_cache_error(dirmgr.circmgr()?.deref(), &source, &e);
619
                }
620
                continue;
621
            }
622
        };
623
        match dirmgr.expand_response_text(&client_req, text) {
624
            Ok(text) => {
625
                let doc_source = DocSource::DirServer {
626
                    source: source.clone(),
627
                };
628
                let mut changed = false;
629
                let outcome = state.add_from_download(
630
                    &text,
631
                    &client_req,
632
                    doc_source,
633
                    Some(&dirmgr.store),
634
                    &mut changed,
635
                );
636

            
637
                if !changed {
638
                    debug_assert!(outcome.is_err());
639
                }
640

            
641
                if let Some(source) = source {
642
                    if let Err(e) = &outcome {
643
                        n_errors += 1;
644
                        note_cache_error(dirmgr.circmgr()?.deref(), &source, e);
645
                    } else {
646
                        note_cache_success(dirmgr.circmgr()?.deref(), &source);
647
                    }
648
                }
649

            
650
                if let Err(e) = &outcome {
651
                    dirmgr.note_errors(attempt_id, 1);
652
                    warn_report!(e, "error while adding directory info");
653
                }
654
                propagate_fatal_errors!(outcome);
655
            }
656
            Err(e) => {
657
                warn_report!(e, "Error when expanding directory text");
658
                if let Some(source) = source {
659
                    n_errors += 1;
660
                    note_cache_error(dirmgr.circmgr()?.deref(), &source, &e);
661
                }
662
                propagate_fatal_errors!(Err(e));
663
            }
664
        }
665
    }
666
    if n_errors != 0 {
667
        dirmgr.note_errors(attempt_id, n_errors);
668
    }
669
    dirmgr.update_progress(attempt_id, state.bootstrap_progress());
670

            
671
    Ok(())
672
2
}
673

            
674
/// Download information into a DirState state machine until it is
675
/// ["complete"](Readiness::Complete), or until we hit a non-recoverable error.
676
///
677
/// Use `dirmgr` to load from the cache or to launch downloads.
678
///
679
/// Keep resetting the state as needed.
680
///
681
/// The first time that the state becomes ["usable"](Readiness::Usable), notify
682
/// the sender in `tx_usable`.
683
#[instrument(level = "trace", skip_all)]
684
4
pub(crate) async fn download<R: Runtime>(
685
4
    dirmgr: Weak<DirMgr<R>>,
686
4
    state: &mut Box<dyn DirState>,
687
4
    schedule: &mut TaskSchedule<R>,
688
4
    attempt_id: AttemptId,
689
4
    tx_usable: &mut Option<oneshot::Sender<()>>,
690
4
) -> Result<()> {
691
    let runtime = upgrade_weak_ref(&dirmgr)?.runtime.clone();
692

            
693
    trace!(attempt=%attempt_id, state=%state.describe(), "Trying to download directory material.");
694

            
695
    'next_state: loop {
696
        let retry_config = state.dl_config();
697
        let parallelism = retry_config.parallelism();
698

            
699
        // In theory this could be inside the loop below maybe?  If we
700
        // want to drop the restriction that the missing() members of a
701
        // state must never grow, then we'll need to move it inside.
702
        let mut now = update_state(&upgrade_weak_ref(&dirmgr)?, attempt_id, state)?;
703

            
704
        apply_state(&upgrade_weak_ref(&dirmgr)?, state, attempt_id)?;
705

            
706
        // Skip the downloads if we can...
707

            
708
        match advance_state(state, attempt_id) {
709
            Ok(_) => continue 'next_state,
710
            Err(AdvanceStateError::AlreadyComplete) => return Ok(()),
711
            Err(AdvanceStateError::CantAdvanceYet) => {}
712
        }
713

            
714
        let reset_time = no_more_than_a_week_from(runtime.wallclock(), state.reset_time());
715

            
716
        let mut retry = retry_config.schedule();
717
        let mut delay = None;
718

            
719
        // Make several attempts to fetch whatever we're missing,
720
        // until either we can advance, or we've got a complete
721
        // document, or we run out of tries, or we run out of time.
722
        'next_attempt: for attempt in retry_config.attempts() {
723
            // We wait at the start of this loop, on all attempts but the first.
724
            // This ensures that we always wait between attempts, but not after
725
            // the final attempt.
726
            let next_delay = retry.next_delay(&mut rand::rng());
727
            if let Some(delay) = delay.replace(next_delay) {
728
                let time_until_reset = reset_time
729
                    .duration_since(now)
730
                    .unwrap_or(Duration::from_secs(0));
731
                let real_delay = delay.min(time_until_reset);
732
                debug!(attempt=%attempt_id, "Waiting {:?} for next download attempt...", real_delay);
733
                schedule.sleep(real_delay).await?;
734

            
735
                now = upgrade_weak_ref(&dirmgr)?.runtime.wallclock();
736
                if now >= reset_time {
737
                    info!(attempt=%attempt_id, "Directory being fetched is now outdated; resetting download state.");
738
                    reset(state);
739
                    continue 'next_state;
740
                }
741
            }
742

            
743
            info!(attempt=%attempt_id, "{}: {}", attempt + 1, state.describe());
744
            match perform_download(attempt_id, &dirmgr, &mut now, parallelism, schedule, state)
745
                .await?
746
            {
747
                DownloadOutcome::DownloadFailed => continue 'next_state,
748
                DownloadOutcome::DirectoryOutdated => continue 'next_attempt,
749
                DownloadOutcome::Applied => {}
750
            }
751

            
752
            propagate_fatal_errors!(apply_state(&upgrade_weak_ref(&dirmgr)?, state, attempt_id));
753

            
754
            // Exit if the download is complete. Report usable-ness if appropriate.
755
            match advance_state(state, attempt_id) {
756
                Err(AdvanceStateError::AlreadyComplete) => return Ok(()),
757
                Ok(()) => continue 'next_state,
758
                Err(AdvanceStateError::CantAdvanceYet) => {
759
                    if state.is_ready(Readiness::Usable) {
760
                        if let Some(tx) = tx_usable.take() {
761
                            trace!(attempt=%attempt_id, state=%state.describe(), "directory is now usable.");
762
                            let _ = tx.send(());
763
                        }
764
                    }
765
                }
766
            }
767
        }
768

            
769
        // We didn't advance the state, after all the retries.
770
        warn!(n_attempts=retry_config.n_attempts(),
771
              state=%state.describe(),
772
              "Unable to advance downloading state");
773
        return Err(Error::CantAdvanceState);
774
    }
775
4
}
776

            
777
/// Replace `state` with `state.reset()`.
778
fn reset(state: &mut Box<dyn DirState>) {
779
    let cur_state = std::mem::replace(state, Box::new(PoisonedState));
780
    *state = cur_state.reset();
781
}
782

            
783
/// Replace `state` with `state.advance()`.
784
4
fn advance(state: &mut Box<dyn DirState>) {
785
4
    let cur_state = std::mem::replace(state, Box::new(PoisonedState));
786
4
    *state = cur_state.advance();
787
4
}
788

            
789
/// Helper: Clamp `v` so that it is no more than one week from `now`.
790
///
791
/// If `v` is absent, return the time that's one week from now.
792
///
793
/// We use this to determine a reset time when no reset time is
794
/// available, or when it is too far in the future.
795
12
fn no_more_than_a_week_from(now: SystemTime, v: Option<SystemTime>) -> SystemTime {
796
12
    let one_week_later = now + Duration::new(86400 * 7, 0);
797
12
    match v {
798
6
        Some(t) => std::cmp::min(t, one_week_later),
799
6
        None => one_week_later,
800
    }
801
12
}
802

            
803
#[cfg(test)]
804
mod test {
805
    // @@ begin test lint list maintained by maint/add_warning @@
806
    #![allow(clippy::bool_assert_comparison)]
807
    #![allow(clippy::clone_on_copy)]
808
    #![allow(clippy::dbg_macro)]
809
    #![allow(clippy::mixed_attributes_style)]
810
    #![allow(clippy::print_stderr)]
811
    #![allow(clippy::print_stdout)]
812
    #![allow(clippy::single_char_pattern)]
813
    #![allow(clippy::unwrap_used)]
814
    #![allow(clippy::unchecked_time_subtraction)]
815
    #![allow(clippy::useless_vec)]
816
    #![allow(clippy::needless_pass_by_value)]
817
    #![allow(clippy::string_slice)] // See arti#2571
818
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
819
    use super::*;
820
    use crate::storage::DynStore;
821
    use crate::test::new_mgr;
822
    use std::sync::Mutex;
823
    use tor_dircommon::retry::DownloadSchedule;
824
    use tor_netdoc::doc::microdesc::MdDigest;
825
    use tor_rtcompat::SleepProvider;
826
    use web_time_compat::SystemTimeExt;
827

            
828
    #[test]
829
    fn week() {
830
        let now = SystemTime::get();
831
        let one_day = Duration::new(86400, 0);
832

            
833
        assert_eq!(no_more_than_a_week_from(now, None), now + one_day * 7);
834
        assert_eq!(
835
            no_more_than_a_week_from(now, Some(now + one_day)),
836
            now + one_day
837
        );
838
        assert_eq!(
839
            no_more_than_a_week_from(now, Some(now - one_day)),
840
            now - one_day
841
        );
842
        assert_eq!(
843
            no_more_than_a_week_from(now, Some(now + 30 * one_day)),
844
            now + one_day * 7
845
        );
846
    }
847

            
848
    /// A fake implementation of DirState that just wants a fixed set
849
    /// of microdescriptors.  It doesn't care if it gets them: it just
850
    /// wants to be told that the IDs exist.
851
    #[derive(Debug, Clone)]
852
    struct DemoState {
853
        second_time_around: bool,
854
        got_items: HashMap<MdDigest, bool>,
855
    }
856

            
857
    // Constants from Lou Reed
858
    const H1: MdDigest = *b"satellite's gone up to the skies";
859
    const H2: MdDigest = *b"things like that drive me out of";
860
    const H3: MdDigest = *b"my mind i watched it for a littl";
861
    const H4: MdDigest = *b"while i like to watch things on ";
862
    const H5: MdDigest = *b"TV Satellite of love Satellite--";
863

            
864
    impl DemoState {
865
        fn new1() -> Self {
866
            DemoState {
867
                second_time_around: false,
868
                got_items: vec![(H1, false), (H2, false)].into_iter().collect(),
869
            }
870
        }
871
        fn new2() -> Self {
872
            DemoState {
873
                second_time_around: true,
874
                got_items: vec![(H3, false), (H4, false), (H5, false)]
875
                    .into_iter()
876
                    .collect(),
877
            }
878
        }
879
        fn n_ready(&self) -> usize {
880
            self.got_items.values().filter(|x| **x).count()
881
        }
882
    }
883

            
884
    impl DirState for DemoState {
885
        fn describe(&self) -> String {
886
            format!("{:?}", self)
887
        }
888
        fn bootstrap_progress(&self) -> crate::event::DirProgress {
889
            crate::event::DirProgress::default()
890
        }
891
        fn is_ready(&self, ready: Readiness) -> bool {
892
            match (ready, self.second_time_around) {
893
                (_, false) => false,
894
                (Readiness::Complete, true) => self.n_ready() == self.got_items.len(),
895
                (Readiness::Usable, true) => self.n_ready() >= self.got_items.len() - 1,
896
            }
897
        }
898
        fn can_advance(&self) -> bool {
899
            if self.second_time_around {
900
                false
901
            } else {
902
                self.n_ready() == self.got_items.len()
903
            }
904
        }
905
        fn missing_docs(&self) -> Vec<DocId> {
906
            self.got_items
907
                .iter()
908
                .filter_map(|(id, have)| {
909
                    if *have {
910
                        None
911
                    } else {
912
                        Some(DocId::Microdesc(*id))
913
                    }
914
                })
915
                .collect()
916
        }
917
        fn add_from_cache(
918
            &mut self,
919
            docs: HashMap<DocId, DocumentText>,
920
            changed: &mut bool,
921
        ) -> Result<()> {
922
            for id in docs.keys() {
923
                if let DocId::Microdesc(id) = id {
924
                    if self.got_items.get(id) == Some(&false) {
925
                        self.got_items.insert(*id, true);
926
                        *changed = true;
927
                    }
928
                }
929
            }
930
            Ok(())
931
        }
932
        fn add_from_download(
933
            &mut self,
934
            text: &str,
935
            _request: &ClientRequest,
936
            _source: DocSource,
937
            _storage: Option<&Mutex<DynStore>>,
938
            changed: &mut bool,
939
        ) -> Result<()> {
940
            for token in text.split_ascii_whitespace() {
941
                if let Ok(v) = hex::decode(token) {
942
                    if let Ok(id) = v.try_into() {
943
                        if self.got_items.get(&id) == Some(&false) {
944
                            self.got_items.insert(id, true);
945
                            *changed = true;
946
                        }
947
                    }
948
                }
949
            }
950
            Ok(())
951
        }
952
        fn dl_config(&self) -> DownloadSchedule {
953
            DownloadSchedule::default()
954
        }
955
        fn advance(self: Box<Self>) -> Box<dyn DirState> {
956
            if self.can_advance() {
957
                Box::new(Self::new2())
958
            } else {
959
                self
960
            }
961
        }
962
        fn reset_time(&self) -> Option<SystemTime> {
963
            None
964
        }
965
        fn reset(self: Box<Self>) -> Box<dyn DirState> {
966
            Box::new(Self::new1())
967
        }
968
    }
969

            
970
    #[test]
971
    fn all_in_cache() {
972
        // Let's try bootstrapping when everything is in the cache.
973
        tor_rtcompat::test_with_one_runtime!(|rt| async {
974
            let now = rt.wallclock();
975
            let (_tempdir, mgr) = new_mgr(rt.clone());
976
            let (mut schedule, _handle) = TaskSchedule::new(rt);
977

            
978
            {
979
                let mut store = mgr.store_if_rw().unwrap().lock().unwrap();
980
                for h in [H1, H2, H3, H4, H5] {
981
                    store.store_microdescs(&[("ignore", &h)], now).unwrap();
982
                }
983
            }
984
            let mgr = Arc::new(mgr);
985
            let attempt_id = AttemptId::next();
986

            
987
            // Try just a load.
988
            let state = Box::new(DemoState::new1());
989
            let result = super::load(&mgr, state, attempt_id).unwrap();
990
            assert!(result.is_ready(Readiness::Complete));
991

            
992
            // Try a bootstrap that could (but won't!) download.
993
            let mut state: Box<dyn DirState> = Box::new(DemoState::new1());
994

            
995
            let mut tx_usable = None;
996
            super::download(
997
                Arc::downgrade(&mgr),
998
                &mut state,
999
                &mut schedule,
                attempt_id,
                &mut tx_usable,
            )
            .await
            .unwrap();
            assert!(state.is_ready(Readiness::Complete));
        });
    }
    #[test]
    fn partly_in_cache() {
        // Let's try bootstrapping with all of phase1 and part of
        // phase 2 in cache.
        tor_rtcompat::test_with_one_runtime!(|rt| async {
            let now = rt.wallclock();
            let (_tempdir, mgr) = new_mgr(rt.clone());
            let (mut schedule, _handle) = TaskSchedule::new(rt);
            {
                let mut store = mgr.store_if_rw().unwrap().lock().unwrap();
                for h in [H1, H2, H3] {
                    store.store_microdescs(&[("ignore", &h)], now).unwrap();
                }
            }
            {
                let mut resp = CANNED_RESPONSE.lock().unwrap();
                // H4 and H5.
                *resp = vec![
                    "7768696c652069206c696b6520746f207761746368207468696e6773206f6e20
                     545620536174656c6c697465206f66206c6f766520536174656c6c6974652d2d"
                        .to_owned(),
                ];
            }
            let mgr = Arc::new(mgr);
            let mut tx_usable = None;
            let attempt_id = AttemptId::next();
            let mut state: Box<dyn DirState> = Box::new(DemoState::new1());
            super::download(
                Arc::downgrade(&mgr),
                &mut state,
                &mut schedule,
                attempt_id,
                &mut tx_usable,
            )
            .await
            .unwrap();
            assert!(state.is_ready(Readiness::Complete));
        });
    }
}