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::{
7
    collections::HashMap,
8
    sync::{Arc, Weak},
9
    time::{Duration, SystemTime},
10
};
11

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
182
12
    Ok(())
183
12
}
184

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

            
198
/// Construct an appropriate ClientRequest to download a consensus
199
/// of the given flavor.
200
8
pub(crate) fn make_consensus_request(
201
8
    now: SystemTime,
202
8
    flavor: ConsensusFlavor,
203
8
    store: &dyn Store,
204
8
    config: &DirMgrConfig,
205
8
) -> Result<ClientRequest> {
206
8
    let mut request = tor_dirclient::request::ConsensusRequest::new(flavor);
207

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

            
210
8
    match store.latest_consensus_meta(flavor) {
211
4
        Ok(Some(meta)) => {
212
4
            let valid_after = meta.lifetime().valid_after();
213
4
            request.set_last_consensus_date(std::cmp::max(valid_after, default_cutoff));
214
4
            request.push_old_consensus_digest(*meta.sha3_256_of_signed());
215
4
        }
216
4
        latest => {
217
4
            if let Err(e) = latest {
218
                warn_report!(e, "Error loading directory metadata");
219
4
            }
220
            // If we don't have a consensus, then request one that's
221
            // "reasonably new".  That way, our clock is set far in the
222
            // future, we won't download stuff we can't use.
223
4
            request.set_last_consensus_date(default_cutoff);
224
        }
225
    }
226

            
227
8
    request.set_skew_limit(
228
        // If we are _fast_ by at least this much, then any valid directory will
229
        // seem to be at least this far in the past.
230
8
        config.tolerance.post_valid_tolerance(),
231
        // If we are _slow_ by this much, then any valid directory will seem to
232
        // be at least this far in the future.
233
8
        config.tolerance.pre_valid_tolerance(),
234
    );
235

            
236
8
    Ok(ClientRequest::Consensus(request))
237
8
}
238

            
239
/// Construct a set of `ClientRequest`s in order to fetch the documents in `docs`.
240
14
pub(crate) fn make_requests_for_documents<R: Runtime>(
241
14
    rt: &R,
242
14
    docs: &[DocId],
243
14
    store: &dyn Store,
244
14
    config: &DirMgrConfig,
245
14
) -> Result<Vec<ClientRequest>> {
246
14
    let mut res = Vec::new();
247
18
    for q in docid::partition_by_type(docs.iter().copied())
248
14
        .into_iter()
249
14
        .flat_map(|(_, x)| x.split_for_download().into_iter())
250
    {
251
18
        match q {
252
4
            DocQuery::LatestConsensus { flavor, .. } => {
253
4
                res.push(make_consensus_request(
254
4
                    rt.wallclock(),
255
4
                    flavor,
256
4
                    store,
257
4
                    config,
258
                )?);
259
            }
260
2
            DocQuery::AuthCert(ids) => {
261
2
                res.push(ClientRequest::AuthCert(ids.into_iter().collect()));
262
2
            }
263
8
            DocQuery::Microdesc(ids) => {
264
8
                res.push(ClientRequest::Microdescs(ids.into_iter().collect()));
265
8
            }
266
            #[cfg(feature = "routerdesc")]
267
4
            DocQuery::RouterDesc(ids) => {
268
4
                res.push(ClientRequest::RouterDescs(ids.into_iter().collect()));
269
4
            }
270
        }
271
    }
272
14
    Ok(res)
273
14
}
274

            
275
/// Launch a single client request and get an associated response.
276
async fn fetch_single<R: Runtime>(
277
    rt: &R,
278
    request: ClientRequest,
279
    current_netdir: Option<&NetDir>,
280
    circmgr: Arc<CircMgr<R>>,
281
) -> Result<(ClientRequest, DirResponse)> {
282
    let dirinfo: DirInfo = match current_netdir {
283
        Some(netdir) => netdir.into(),
284
        None => tor_circmgr::DirInfo::Nothing,
285
    };
286
    let outcome =
287
        tor_dirclient::get_resource(request.as_requestable(), dirinfo, rt, circmgr.clone()).await;
288

            
289
    note_request_outcome(&circmgr, &outcome);
290

            
291
    let resource = outcome?;
292
    Ok((request, resource))
293
}
294

            
295
/// Testing helper: if this is Some, then we return it in place of any
296
/// response to fetch_multiple.
297
///
298
/// Note that only one test uses this: otherwise there would be a race
299
/// condition. :p
300
#[cfg(test)]
301
2
static CANNED_RESPONSE: LazyLock<Mutex<Vec<String>>> = LazyLock::new(|| Mutex::new(vec![]));
302

            
303
/// Launch a set of download requests for a set of missing objects in
304
/// `missing`, and return each request along with the response it received.
305
///
306
/// Don't launch more than `parallelism` requests at once.
307
#[allow(clippy::cognitive_complexity)] // TODO: maybe refactor?
308
2
async fn fetch_multiple<R: Runtime>(
309
2
    dirmgr: Arc<DirMgr<R>>,
310
2
    attempt_id: AttemptId,
311
2
    missing: &[DocId],
312
2
    parallelism: usize,
313
2
) -> Result<Vec<(ClientRequest, DirResponse)>> {
314
2
    let requests = {
315
2
        let store = dirmgr.store.lock().expect("store lock poisoned");
316
2
        make_requests_for_documents(&dirmgr.runtime, missing, &**store, &dirmgr.config.get())?
317
    };
318

            
319
2
    trace!(attempt=%attempt_id, "Launching {} requests for {} documents",
320
           requests.len(), missing.len());
321

            
322
    #[cfg(test)]
323
    {
324
2
        let m = CANNED_RESPONSE.lock().expect("Poisoned mutex");
325
2
        if !m.is_empty() {
326
2
            return Ok(requests
327
2
                .into_iter()
328
2
                .zip(m.iter().map(DirResponse::from_body))
329
2
                .collect());
330
        }
331
    }
332

            
333
    let circmgr = dirmgr.circmgr()?;
334
    // Only use timely directories for bootstrapping directories; otherwise, we'll try fallbacks.
335
    let netdir = dirmgr.netdir(tor_netdir::Timeliness::Timely).ok();
336

            
337
    // TODO: instead of waiting for all the queries to finish, we
338
    // could stream the responses back or something.
339
    let responses: Vec<Result<(ClientRequest, DirResponse)>> = futures::stream::iter(requests)
340
        .map(|query| fetch_single(&dirmgr.runtime, query, netdir.as_deref(), circmgr.clone()))
341
        .buffer_unordered(parallelism)
342
        .collect()
343
        .await;
344

            
345
    let mut useful_responses = Vec::new();
346
    for r in responses {
347
        // TODO: on some error cases we might want to stop using this source.
348
        match r {
349
            Ok((request, response)) => {
350
                if response.status_code() == 200 {
351
                    useful_responses.push((request, response));
352
                } else {
353
                    trace!(
354
                        "cache declined request; reported status {:?}",
355
                        response.status_code()
356
                    );
357
                }
358
            }
359
            Err(e) => warn_report!(e, "error while downloading"),
360
        }
361
    }
362

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

            
365
    Ok(useful_responses)
366
2
}
367

            
368
/// Try to update `state` by loading cached information from `dirmgr`.
369
14
async fn load_once<R: Runtime>(
370
14
    dirmgr: &Arc<DirMgr<R>>,
371
14
    state: &mut Box<dyn DirState>,
372
14
    attempt_id: AttemptId,
373
14
    changed_out: &mut bool,
374
14
) -> Result<()> {
375
14
    let missing = state.missing_docs();
376
14
    let mut changed = false;
377
14
    let outcome: Result<()> = if missing.is_empty() {
378
2
        trace!("Found no missing documents; can't advance current state");
379
2
        Ok(())
380
    } else {
381
12
        trace!(
382
            "Found {} missing documents; trying to load them",
383
            missing.len()
384
        );
385

            
386
12
        load_and_apply_documents(&missing, dirmgr, state, &mut changed)
387
    };
388

            
389
    // We have to update the status here regardless of the outcome, if we got
390
    // any information: even if there was an error, we might have received
391
    // partial information that changed our status.
392
14
    if changed {
393
12
        dirmgr.update_progress(attempt_id, state.bootstrap_progress());
394
12
        *changed_out = true;
395
12
    }
396

            
397
14
    outcome
398
14
}
399

            
400
/// Try to load as much state as possible for a provided `state` from the
401
/// cache in `dirmgr`, advancing the state to the extent possible.
402
///
403
/// No downloads are performed; the provided state will not be reset.
404
#[allow(clippy::cognitive_complexity)] // TODO: Refactor? Somewhat due to tracing.
405
2
pub(crate) async fn load<R: Runtime>(
406
2
    dirmgr: Arc<DirMgr<R>>,
407
2
    mut state: Box<dyn DirState>,
408
2
    attempt_id: AttemptId,
409
2
) -> Result<Box<dyn DirState>> {
410
2
    let mut safety_counter = 0_usize;
411
    loop {
412
6
        trace!(attempt=%attempt_id, state=%state.describe(), "Loading from cache");
413
6
        let mut changed = false;
414
6
        let outcome = load_once(&dirmgr, &mut state, attempt_id, &mut changed).await;
415
        {
416
6
            let mut store = dirmgr.store.lock().expect("store lock poisoned");
417
6
            dirmgr.apply_netdir_changes(&mut state, &mut **store)?;
418
6
            dirmgr.update_progress(attempt_id, state.bootstrap_progress());
419
        }
420
6
        trace!(attempt=%attempt_id, ?outcome, "Load operation completed.");
421

            
422
6
        if let Err(e) = outcome {
423
            match e.bootstrap_action() {
424
                BootstrapAction::Nonfatal => {
425
                    debug!("Recoverable error loading from cache: {}", e);
426
                }
427
                BootstrapAction::Fatal | BootstrapAction::Reset => {
428
                    return Err(e);
429
                }
430
            }
431
6
        }
432

            
433
6
        if state.can_advance() {
434
2
            state = state.advance();
435
2
            trace!(attempt=%attempt_id, state=state.describe(), "State has advanced.");
436
2
            safety_counter = 0;
437
        } else {
438
4
            if !changed {
439
                // TODO: Are there more nonfatal errors that mean we should
440
                // break?
441
2
                trace!(attempt=%attempt_id, state=state.describe(), "No state advancement after load; nothing more to find in the cache.");
442
2
                break;
443
2
            }
444
2
            safety_counter += 1;
445
2
            assert!(
446
2
                safety_counter < 100,
447
                "Spent 100 iterations in the same state: this is a bug"
448
            );
449
        }
450
    }
451

            
452
2
    Ok(state)
453
2
}
454

            
455
/// Helper: Make a set of download attempts for the current directory state,
456
/// and on success feed their results into the state object.
457
///
458
/// This can launch one or more download requests, but will not launch more
459
/// than `parallelism` requests at a time.
460
#[allow(clippy::cognitive_complexity)] // TODO: Refactor?
461
2
async fn download_attempt<R: Runtime>(
462
2
    dirmgr: &Arc<DirMgr<R>>,
463
2
    state: &mut Box<dyn DirState>,
464
2
    parallelism: usize,
465
2
    attempt_id: AttemptId,
466
2
) -> Result<()> {
467
2
    let missing = state.missing_docs();
468
2
    let fetched = fetch_multiple(Arc::clone(dirmgr), attempt_id, &missing, parallelism).await?;
469
2
    let mut n_errors = 0;
470
4
    for (client_req, dir_response) in fetched {
471
2
        let source = dir_response.source().cloned();
472
2
        let text = match String::from_utf8(dir_response.into_output_unchecked())
473
2
            .map_err(Error::BadUtf8FromDirectory)
474
        {
475
2
            Ok(t) => t,
476
            Err(e) => {
477
                if let Some(source) = source {
478
                    n_errors += 1;
479
                    note_cache_error(dirmgr.circmgr()?.deref(), &source, &e);
480
                }
481
                continue;
482
            }
483
        };
484
2
        match dirmgr.expand_response_text(&client_req, text) {
485
2
            Ok(text) => {
486
2
                let doc_source = DocSource::DirServer {
487
2
                    source: source.clone(),
488
2
                };
489
2
                let mut changed = false;
490
2
                let outcome = state.add_from_download(
491
2
                    &text,
492
2
                    &client_req,
493
2
                    doc_source,
494
2
                    Some(&dirmgr.store),
495
2
                    &mut changed,
496
                );
497

            
498
2
                if !changed {
499
                    debug_assert!(outcome.is_err());
500
2
                }
501

            
502
2
                if let Some(source) = source {
503
                    if let Err(e) = &outcome {
504
                        n_errors += 1;
505
                        note_cache_error(dirmgr.circmgr()?.deref(), &source, e);
506
                    } else {
507
                        note_cache_success(dirmgr.circmgr()?.deref(), &source);
508
                    }
509
2
                }
510

            
511
2
                if let Err(e) = &outcome {
512
                    dirmgr.note_errors(attempt_id, 1);
513
                    warn_report!(e, "error while adding directory info");
514
2
                }
515
2
                propagate_fatal_errors!(outcome);
516
            }
517
            Err(e) => {
518
                warn_report!(e, "Error when expanding directory text");
519
                if let Some(source) = source {
520
                    n_errors += 1;
521
                    note_cache_error(dirmgr.circmgr()?.deref(), &source, &e);
522
                }
523
                propagate_fatal_errors!(Err(e));
524
            }
525
        }
526
    }
527
2
    if n_errors != 0 {
528
        dirmgr.note_errors(attempt_id, n_errors);
529
2
    }
530
2
    dirmgr.update_progress(attempt_id, state.bootstrap_progress());
531

            
532
2
    Ok(())
533
2
}
534

            
535
/// Download information into a DirState state machine until it is
536
/// ["complete"](Readiness::Complete), or until we hit a non-recoverable error.
537
///
538
/// Use `dirmgr` to load from the cache or to launch downloads.
539
///
540
/// Keep resetting the state as needed.
541
///
542
/// The first time that the state becomes ["usable"](Readiness::Usable), notify
543
/// the sender in `on_usable`.
544
#[allow(clippy::cognitive_complexity)] // TODO: Refactor!
545
4
pub(crate) async fn download<R: Runtime>(
546
4
    dirmgr: Weak<DirMgr<R>>,
547
4
    state: &mut Box<dyn DirState>,
548
4
    schedule: &mut TaskSchedule<R>,
549
4
    attempt_id: AttemptId,
550
4
    on_usable: &mut Option<oneshot::Sender<()>>,
551
4
) -> Result<()> {
552
4
    let runtime = upgrade_weak_ref(&dirmgr)?.runtime.clone();
553

            
554
4
    trace!(attempt=%attempt_id, state=%state.describe(), "Trying to download directory material.");
555

            
556
    'next_state: loop {
557
8
        let retry_config = state.dl_config();
558
8
        let parallelism = retry_config.parallelism();
559

            
560
        // In theory this could be inside the loop below maybe?  If we
561
        // want to drop the restriction that the missing() members of a
562
        // state must never grow, then we'll need to move it inside.
563
8
        let mut now = {
564
8
            let dirmgr = upgrade_weak_ref(&dirmgr)?;
565
8
            let mut changed = false;
566
8
            trace!(attempt=%attempt_id, state=%state.describe(),"Attempting to load directory information from cache.");
567
8
            let load_result = load_once(&dirmgr, state, attempt_id, &mut changed).await;
568
8
            trace!(attempt=%attempt_id, state=%state.describe(), outcome=?load_result, "Load attempt complete.");
569
8
            if let Err(e) = &load_result {
570
                // If the load failed but the error can be blamed on a directory
571
                // cache, do so.
572
                if let Some(source) = e.responsible_cache() {
573
                    dirmgr.note_errors(attempt_id, 1);
574
                    note_cache_error(dirmgr.circmgr()?.deref(), source, e);
575
                }
576
8
            }
577
8
            propagate_fatal_errors!(load_result);
578
8
            dirmgr.runtime.wallclock()
579
        };
580

            
581
        // Apply any netdir changes that the state gives us.
582
        // TODO(eta): Consider deprecating state.is_ready().
583
        {
584
8
            let dirmgr = upgrade_weak_ref(&dirmgr)?;
585
8
            let mut store = dirmgr.store.lock().expect("store lock poisoned");
586
8
            dirmgr.apply_netdir_changes(state, &mut **store)?;
587
8
            dirmgr.update_progress(attempt_id, state.bootstrap_progress());
588
        }
589
        // Skip the downloads if we can...
590
8
        if state.can_advance() {
591
4
            advance(state);
592
4
            trace!(attempt=%attempt_id, state=%state.describe(), "State has advanced.");
593
4
            continue 'next_state;
594
4
        }
595
4
        if state.is_ready(Readiness::Complete) {
596
2
            trace!(attempt=%attempt_id, state=%state.describe(), "Directory is now Complete.");
597
2
            return Ok(());
598
2
        }
599

            
600
2
        let reset_time = no_more_than_a_week_from(runtime.wallclock(), state.reset_time());
601

            
602
2
        let mut retry = retry_config.schedule();
603
2
        let mut delay = None;
604

            
605
        // Make several attempts to fetch whatever we're missing,
606
        // until either we can advance, or we've got a complete
607
        // document, or we run out of tries, or we run out of time.
608
2
        'next_attempt: for attempt in retry_config.attempts() {
609
            // We wait at the start of this loop, on all attempts but the first.
610
            // This ensures that we always wait between attempts, but not after
611
            // the final attempt.
612
2
            let next_delay = retry.next_delay(&mut rand::rng());
613
2
            if let Some(delay) = delay.replace(next_delay) {
614
                let time_until_reset = {
615
                    reset_time
616
                        .duration_since(now)
617
                        .unwrap_or(Duration::from_secs(0))
618
                };
619
                let real_delay = delay.min(time_until_reset);
620
                debug!(attempt=%attempt_id, "Waiting {:?} for next download attempt...", real_delay);
621
                schedule.sleep(real_delay).await?;
622

            
623
                now = upgrade_weak_ref(&dirmgr)?.runtime.wallclock();
624
                if now >= reset_time {
625
                    info!(attempt=%attempt_id, "Directory being fetched is now outdated; resetting download state.");
626
                    reset(state);
627
                    continue 'next_state;
628
                }
629
2
            }
630

            
631
2
            info!(attempt=%attempt_id, "{}: {}", attempt + 1, state.describe());
632
2
            let reset_time = no_more_than_a_week_from(now, state.reset_time());
633

            
634
            now = {
635
2
                let dirmgr = upgrade_weak_ref(&dirmgr)?;
636
2
                futures::select_biased! {
637
2
                    outcome = download_attempt(&dirmgr, state, parallelism.into(), attempt_id).fuse() => {
638
2
                        if let Err(e) = outcome {
639
                            warn_report!(e, attempt=%attempt_id, "Error while downloading.");
640
                            propagate_fatal_errors!(Err(e));
641
                            continue 'next_attempt;
642
                        } else {
643
2
                            trace!(attempt=%attempt_id, "Successfully downloaded some information.");
644
                        }
645
                    }
646
2
                    _ = schedule.sleep_until_wallclock(reset_time).fuse() => {
647
                        // We need to reset. This can happen if (for
648
                        // example) we're downloading the last few
649
                        // microdescriptors on a consensus that now
650
                        // we're ready to replace.
651
                        info!(attempt=%attempt_id, "Directory being fetched is now outdated; resetting download state.");
652
                        reset(state);
653
                        continue 'next_state;
654
                    },
655
                };
656
2
                dirmgr.runtime.wallclock()
657
            };
658

            
659
            // Apply any netdir changes that the state gives us.
660
            // TODO(eta): Consider deprecating state.is_ready().
661
            {
662
2
                let dirmgr = upgrade_weak_ref(&dirmgr)?;
663
2
                let mut store = dirmgr.store.lock().expect("store lock poisoned");
664
2
                let outcome = dirmgr.apply_netdir_changes(state, &mut **store);
665
2
                dirmgr.update_progress(attempt_id, state.bootstrap_progress());
666
2
                propagate_fatal_errors!(outcome);
667
            }
668

            
669
            // Exit if there is nothing more to download.
670
2
            if state.is_ready(Readiness::Complete) {
671
2
                trace!(attempt=%attempt_id, state=%state.describe(), "Directory is now Complete.");
672
2
                return Ok(());
673
            }
674

            
675
            // Report usable-ness if appropriate.
676
            if on_usable.is_some() && state.is_ready(Readiness::Usable) {
677
                trace!(attempt=%attempt_id, state=%state.describe(), "Directory is now Usable.");
678
                // Unwrap should be safe due to parent `.is_some()` check
679
                #[allow(clippy::unwrap_used)]
680
                let _ = on_usable.take().unwrap().send(());
681
            }
682

            
683
            if state.can_advance() {
684
                // We have enough info to advance to another state.
685
                advance(state);
686
                trace!(attempt=%attempt_id, state=%state.describe(), "State has advanced.");
687
                continue 'next_state;
688
            }
689
        }
690

            
691
        // We didn't advance the state, after all the retries.
692
        warn!(n_attempts=retry_config.n_attempts(),
693
              state=%state.describe(),
694
              "Unable to advance downloading state");
695
        return Err(Error::CantAdvanceState);
696
    }
697
4
}
698

            
699
/// Replace `state` with `state.reset()`.
700
fn reset(state: &mut Box<dyn DirState>) {
701
    let cur_state = std::mem::replace(state, Box::new(PoisonedState));
702
    *state = cur_state.reset();
703
}
704

            
705
/// Replace `state` with `state.advance()`.
706
4
fn advance(state: &mut Box<dyn DirState>) {
707
4
    let cur_state = std::mem::replace(state, Box::new(PoisonedState));
708
4
    *state = cur_state.advance();
709
4
}
710

            
711
/// Helper: Clamp `v` so that it is no more than one week from `now`.
712
///
713
/// If `v` is absent, return the time that's one week from now.
714
///
715
/// We use this to determine a reset time when no reset time is
716
/// available, or when it is too far in the future.
717
12
fn no_more_than_a_week_from(now: SystemTime, v: Option<SystemTime>) -> SystemTime {
718
12
    let one_week_later = now + Duration::new(86400 * 7, 0);
719
12
    match v {
720
6
        Some(t) => std::cmp::min(t, one_week_later),
721
6
        None => one_week_later,
722
    }
723
12
}
724

            
725
#[cfg(test)]
726
mod test {
727
    // @@ begin test lint list maintained by maint/add_warning @@
728
    #![allow(clippy::bool_assert_comparison)]
729
    #![allow(clippy::clone_on_copy)]
730
    #![allow(clippy::dbg_macro)]
731
    #![allow(clippy::mixed_attributes_style)]
732
    #![allow(clippy::print_stderr)]
733
    #![allow(clippy::print_stdout)]
734
    #![allow(clippy::single_char_pattern)]
735
    #![allow(clippy::unwrap_used)]
736
    #![allow(clippy::unchecked_duration_subtraction)]
737
    #![allow(clippy::useless_vec)]
738
    #![allow(clippy::needless_pass_by_value)]
739
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
740
    use super::*;
741
    use crate::storage::DynStore;
742
    use crate::test::new_mgr;
743
    use std::sync::Mutex;
744
    use tor_dircommon::retry::DownloadSchedule;
745
    use tor_netdoc::doc::microdesc::MdDigest;
746
    use tor_rtcompat::SleepProvider;
747

            
748
    #[test]
749
    fn week() {
750
        let now = SystemTime::now();
751
        let one_day = Duration::new(86400, 0);
752

            
753
        assert_eq!(no_more_than_a_week_from(now, None), now + one_day * 7);
754
        assert_eq!(
755
            no_more_than_a_week_from(now, Some(now + one_day)),
756
            now + one_day
757
        );
758
        assert_eq!(
759
            no_more_than_a_week_from(now, Some(now - one_day)),
760
            now - one_day
761
        );
762
        assert_eq!(
763
            no_more_than_a_week_from(now, Some(now + 30 * one_day)),
764
            now + one_day * 7
765
        );
766
    }
767

            
768
    /// A fake implementation of DirState that just wants a fixed set
769
    /// of microdescriptors.  It doesn't care if it gets them: it just
770
    /// wants to be told that the IDs exist.
771
    #[derive(Debug, Clone)]
772
    struct DemoState {
773
        second_time_around: bool,
774
        got_items: HashMap<MdDigest, bool>,
775
    }
776

            
777
    // Constants from Lou Reed
778
    const H1: MdDigest = *b"satellite's gone up to the skies";
779
    const H2: MdDigest = *b"things like that drive me out of";
780
    const H3: MdDigest = *b"my mind i watched it for a littl";
781
    const H4: MdDigest = *b"while i like to watch things on ";
782
    const H5: MdDigest = *b"TV Satellite of love Satellite--";
783

            
784
    impl DemoState {
785
        fn new1() -> Self {
786
            DemoState {
787
                second_time_around: false,
788
                got_items: vec![(H1, false), (H2, false)].into_iter().collect(),
789
            }
790
        }
791
        fn new2() -> Self {
792
            DemoState {
793
                second_time_around: true,
794
                got_items: vec![(H3, false), (H4, false), (H5, false)]
795
                    .into_iter()
796
                    .collect(),
797
            }
798
        }
799
        fn n_ready(&self) -> usize {
800
            self.got_items.values().filter(|x| **x).count()
801
        }
802
    }
803

            
804
    impl DirState for DemoState {
805
        fn describe(&self) -> String {
806
            format!("{:?}", &self)
807
        }
808
        fn bootstrap_progress(&self) -> crate::event::DirProgress {
809
            crate::event::DirProgress::default()
810
        }
811
        fn is_ready(&self, ready: Readiness) -> bool {
812
            match (ready, self.second_time_around) {
813
                (_, false) => false,
814
                (Readiness::Complete, true) => self.n_ready() == self.got_items.len(),
815
                (Readiness::Usable, true) => self.n_ready() >= self.got_items.len() - 1,
816
            }
817
        }
818
        fn can_advance(&self) -> bool {
819
            if self.second_time_around {
820
                false
821
            } else {
822
                self.n_ready() == self.got_items.len()
823
            }
824
        }
825
        fn missing_docs(&self) -> Vec<DocId> {
826
            self.got_items
827
                .iter()
828
                .filter_map(|(id, have)| {
829
                    if *have {
830
                        None
831
                    } else {
832
                        Some(DocId::Microdesc(*id))
833
                    }
834
                })
835
                .collect()
836
        }
837
        fn add_from_cache(
838
            &mut self,
839
            docs: HashMap<DocId, DocumentText>,
840
            changed: &mut bool,
841
        ) -> Result<()> {
842
            for id in docs.keys() {
843
                if let DocId::Microdesc(id) = id {
844
                    if self.got_items.get(id) == Some(&false) {
845
                        self.got_items.insert(*id, true);
846
                        *changed = true;
847
                    }
848
                }
849
            }
850
            Ok(())
851
        }
852
        fn add_from_download(
853
            &mut self,
854
            text: &str,
855
            _request: &ClientRequest,
856
            _source: DocSource,
857
            _storage: Option<&Mutex<DynStore>>,
858
            changed: &mut bool,
859
        ) -> Result<()> {
860
            for token in text.split_ascii_whitespace() {
861
                if let Ok(v) = hex::decode(token) {
862
                    if let Ok(id) = v.try_into() {
863
                        if self.got_items.get(&id) == Some(&false) {
864
                            self.got_items.insert(id, true);
865
                            *changed = true;
866
                        }
867
                    }
868
                }
869
            }
870
            Ok(())
871
        }
872
        fn dl_config(&self) -> DownloadSchedule {
873
            DownloadSchedule::default()
874
        }
875
        fn advance(self: Box<Self>) -> Box<dyn DirState> {
876
            if self.can_advance() {
877
                Box::new(Self::new2())
878
            } else {
879
                self
880
            }
881
        }
882
        fn reset_time(&self) -> Option<SystemTime> {
883
            None
884
        }
885
        fn reset(self: Box<Self>) -> Box<dyn DirState> {
886
            Box::new(Self::new1())
887
        }
888
    }
889

            
890
    #[test]
891
    fn all_in_cache() {
892
        // Let's try bootstrapping when everything is in the cache.
893
        tor_rtcompat::test_with_one_runtime!(|rt| async {
894
            let now = rt.wallclock();
895
            let (_tempdir, mgr) = new_mgr(rt.clone());
896
            let (mut schedule, _handle) = TaskSchedule::new(rt);
897

            
898
            {
899
                let mut store = mgr.store_if_rw().unwrap().lock().unwrap();
900
                for h in [H1, H2, H3, H4, H5] {
901
                    store.store_microdescs(&[("ignore", &h)], now).unwrap();
902
                }
903
            }
904
            let mgr = Arc::new(mgr);
905
            let attempt_id = AttemptId::next();
906

            
907
            // Try just a load.
908
            let state = Box::new(DemoState::new1());
909
            let result = super::load(Arc::clone(&mgr), state, attempt_id)
910
                .await
911
                .unwrap();
912
            assert!(result.is_ready(Readiness::Complete));
913

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

            
917
            let mut on_usable = None;
918
            super::download(
919
                Arc::downgrade(&mgr),
920
                &mut state,
921
                &mut schedule,
922
                attempt_id,
923
                &mut on_usable,
924
            )
925
            .await
926
            .unwrap();
927
            assert!(state.is_ready(Readiness::Complete));
928
        });
929
    }
930

            
931
    #[test]
932
    fn partly_in_cache() {
933
        // Let's try bootstrapping with all of phase1 and part of
934
        // phase 2 in cache.
935
        tor_rtcompat::test_with_one_runtime!(|rt| async {
936
            let now = rt.wallclock();
937
            let (_tempdir, mgr) = new_mgr(rt.clone());
938
            let (mut schedule, _handle) = TaskSchedule::new(rt);
939

            
940
            {
941
                let mut store = mgr.store_if_rw().unwrap().lock().unwrap();
942
                for h in [H1, H2, H3] {
943
                    store.store_microdescs(&[("ignore", &h)], now).unwrap();
944
                }
945
            }
946
            {
947
                let mut resp = CANNED_RESPONSE.lock().unwrap();
948
                // H4 and H5.
949
                *resp = vec![
950
                    "7768696c652069206c696b6520746f207761746368207468696e6773206f6e20
951
                     545620536174656c6c697465206f66206c6f766520536174656c6c6974652d2d"
952
                        .to_owned(),
953
                ];
954
            }
955
            let mgr = Arc::new(mgr);
956
            let mut on_usable = None;
957
            let attempt_id = AttemptId::next();
958

            
959
            let mut state: Box<dyn DirState> = Box::new(DemoState::new1());
960
            super::download(
961
                Arc::downgrade(&mgr),
962
                &mut state,
963
                &mut schedule,
964
                attempt_id,
965
                &mut on_usable,
966
            )
967
            .await
968
            .unwrap();
969
            assert!(state.is_ready(Readiness::Complete));
970
        });
971
    }
972
}