1
//! Publish and maintain onion service descriptors
2
//!
3
//! See the [`reactor`] module-level documentation for more details.
4

            
5
mod backoff;
6
mod descriptor;
7
mod reactor;
8
mod reupload_timer;
9

            
10
use crate::config::restricted_discovery::RestrictedDiscoveryKeys;
11
use crate::internal_prelude::*;
12
use crate::pow::PowManager;
13

            
14
use backoff::{BackoffError, BackoffSchedule, RetriableError, Runner};
15
use descriptor::{DescriptorStatus, VersionedDescriptor, build_sign};
16
use reactor::Reactor;
17
use reactor::read_blind_id_keypair;
18
use reupload_timer::ReuploadTimer;
19

            
20
use tor_config_path::CfgPathResolver;
21
use tor_persist::state_dir::InstanceRawSubdir;
22

            
23
pub use reactor::UploadError;
24
pub(crate) use reactor::{Mockable, OVERALL_UPLOAD_TIMEOUT, Real};
25

            
26
/// A handle for the Hsdir Publisher for an onion service.
27
///
28
/// This handle represents a set of tasks that identify the hsdirs for each
29
/// relevant time period, construct descriptors, publish them, and keep them
30
/// up-to-date.
31
#[must_use = "If you don't call launch() on the publisher, it won't publish any descriptors."]
32
pub(crate) struct Publisher<R: Runtime, M: Mockable> {
33
    /// The runtime.
34
    runtime: R,
35
    /// The service for which we're publishing descriptors.
36
    nickname: HsNickname,
37
    /// A source for new network directories that we use to determine
38
    /// our HsDirs.
39
    dir_provider: Arc<dyn NetDirProvider>,
40
    /// Mockable state.
41
    ///
42
    /// This is used for launching circuits and for obtaining random number generators.
43
    mockable: M,
44
    /// The onion service config.
45
    config: Arc<OnionServiceConfig>,
46
    /// A channel for receiving IPT change notifications.
47
    ipt_watcher: IptsPublisherView,
48
    /// A channel for receiving onion service config change notifications.
49
    config_rx: watch::Receiver<Arc<OnionServiceConfig>>,
50
    /// The key manager.
51
    keymgr: Arc<KeyMgr>,
52
    /// A sender for updating the status of the onion service.
53
    status_tx: PublisherStatusSender,
54
    /// Path resolver for configuration files.
55
    path_resolver: Arc<CfgPathResolver>,
56
    /// Proof-of-work state
57
    pow_manager: Arc<PowManager<R>>,
58
    /// Queue on which we receive messages from the [`PowManager`] telling us that a seed has
59
    /// rotated and thus we need to republish the descriptor for a particular time period.
60
    update_from_pow_manager_rx: mpsc::Receiver<TimePeriod>,
61
    /// The directory to dump invalid HS descriptors to.
62
    ///
63
    /// In practice, we should never be generating invalid HS descriptors,
64
    /// but if we do, they will be dumped to this directory for debugging.
65
    bad_hsdescs_dir: InstanceRawSubdir,
66
}
67

            
68
impl<R: Runtime, M: Mockable> Publisher<R, M> {
69
    /// Create a new publisher.
70
    ///
71
    /// When it launches, it will know no keys or introduction points,
72
    /// and will therefore not upload any descriptors.
73
    ///
74
    /// The publisher won't start publishing until you call [`Publisher::launch`].
75
    #[allow(clippy::too_many_arguments)]
76
4
    pub(crate) fn new(
77
4
        runtime: R,
78
4
        nickname: HsNickname,
79
4
        dir_provider: Arc<dyn NetDirProvider>,
80
4
        mockable: impl Into<M>,
81
4
        ipt_watcher: IptsPublisherView,
82
4
        config_rx: watch::Receiver<Arc<OnionServiceConfig>>,
83
4
        status_tx: PublisherStatusSender,
84
4
        keymgr: Arc<KeyMgr>,
85
4
        path_resolver: Arc<CfgPathResolver>,
86
4
        pow_manager: Arc<PowManager<R>>,
87
4
        update_from_pow_manager_rx: mpsc::Receiver<TimePeriod>,
88
4
        bad_hsdescs_dir: InstanceRawSubdir,
89
4
    ) -> Self {
90
4
        let config = config_rx.borrow().clone();
91
4
        Self {
92
4
            runtime,
93
4
            nickname,
94
4
            dir_provider,
95
4
            mockable: mockable.into(),
96
4
            config,
97
4
            ipt_watcher,
98
4
            config_rx,
99
4
            status_tx,
100
4
            keymgr,
101
4
            path_resolver,
102
4
            pow_manager,
103
4
            update_from_pow_manager_rx,
104
4
            bad_hsdescs_dir,
105
4
        }
106
4
    }
107

            
108
    /// Launch the publisher reactor.
109
4
    pub(crate) fn launch(self) -> Result<(), StartupError> {
110
        let Publisher {
111
4
            runtime,
112
4
            nickname,
113
4
            dir_provider,
114
4
            mockable,
115
4
            config,
116
4
            ipt_watcher,
117
4
            config_rx,
118
4
            status_tx,
119
4
            keymgr,
120
4
            path_resolver,
121
4
            pow_manager,
122
4
            update_from_pow_manager_rx: publisher_update_rx,
123
4
            bad_hsdescs_dir,
124
4
        } = self;
125

            
126
4
        let reactor = Reactor::new(
127
4
            runtime.clone(),
128
4
            nickname,
129
4
            dir_provider,
130
4
            mockable,
131
4
            &config,
132
4
            ipt_watcher,
133
4
            config_rx,
134
4
            status_tx,
135
4
            keymgr,
136
4
            path_resolver,
137
4
            pow_manager,
138
4
            publisher_update_rx,
139
4
            bad_hsdescs_dir,
140
        );
141

            
142
4
        runtime
143
4
            .spawn(async move {
144
4
                match reactor.run().await {
145
                    Ok(()) => debug!("the publisher reactor has shut down"),
146
                    Err(e) => warn_report!(e, "the publisher reactor has shut down"),
147
                }
148
            })
149
4
            .map_err(|e| StartupError::Spawn {
150
                spawning: "publisher reactor task",
151
                cause: e.into(),
152
            })?;
153

            
154
4
        Ok(())
155
4
    }
156
}
157

            
158
#[cfg(test)]
159
mod test {
160
    // @@ begin test lint list maintained by maint/add_warning @@
161
    #![allow(clippy::bool_assert_comparison)]
162
    #![allow(clippy::clone_on_copy)]
163
    #![allow(clippy::dbg_macro)]
164
    #![allow(clippy::mixed_attributes_style)]
165
    #![allow(clippy::print_stderr)]
166
    #![allow(clippy::print_stdout)]
167
    #![allow(clippy::single_char_pattern)]
168
    #![allow(clippy::unwrap_used)]
169
    #![allow(clippy::unchecked_time_subtraction)]
170
    #![allow(clippy::useless_vec)]
171
    #![allow(clippy::needless_pass_by_value)]
172
    #![allow(clippy::string_slice)] // See arti#2571
173
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
174
    use super::*;
175

            
176
    use std::collections::HashMap;
177
    use std::io;
178
    use std::path::Path;
179
    use std::pin::Pin;
180
    use std::sync::Mutex;
181
    use std::sync::atomic::{AtomicUsize, Ordering};
182
    use std::task::{Context, Poll};
183

            
184
    use async_trait::async_trait;
185
    use fs_mistrust::Mistrust;
186
    use futures::{AsyncRead, AsyncWrite};
187
    use tempfile::{TempDir, tempdir};
188
    use test_temp_dir::test_temp_dir;
189

            
190
    use tor_basic_utils::test_rng::{TestingRng, testing_rng};
191

            
192
    use tor_hscrypto::pk::{HsBlindId, HsDescSigningKeypair, HsId, HsIdKey, HsIdKeypair};
193
    use tor_key_forge::ToEncodableKey;
194
    use tor_keymgr::{ArtiNativeKeystore, KeyMgrBuilder, KeySpecifier};
195
    use tor_llcrypto::pk::{ed25519, rsa};
196
    use tor_netdir::testprovider::TestNetDirProvider;
197
    use tor_netdir::{NetDir, testnet};
198
    use tor_netdoc::doc::hsdesc::test_data;
199
    use tor_rtcompat::ToplevelBlockOn;
200
    use tor_rtmock::MockRuntime;
201

            
202
    use crate::HsNickname;
203
    use crate::config::OnionServiceConfigBuilder;
204
    use crate::ipt_set::{IptInSet, IptSet, ipts_channel};
205
    use crate::pow::NewPowManager;
206
    use crate::publish::reactor::MockableDirTunnel;
207
    use crate::status::{OnionServiceStatus, StatusSender};
208
    use crate::test::create_storage_handles;
209
    use crate::{
210
        BlindIdKeypairSpecifier, BlindIdPublicKeySpecifier, DescSigningKeypairSpecifier,
211
        HsIdKeypairSpecifier, HsIdPublicKeySpecifier,
212
    };
213

            
214
    /// The nickname of the test service.
215
    const TEST_SVC_NICKNAME: &str = "test-svc";
216

            
217
    /// The HTTP response the HSDir returns if everything went well.
218
    const OK_RESPONSE: &str = "HTTP/1.1 200 OK\r\n\r\n";
219

            
220
    /// The HTTP response the HSDir returns if something went wrong
221
    const ERR_RESPONSE: &str = "HTTP/1.1 500 UH_OH\r\n\r\n";
222

            
223
    /// The error doesn't matter (we return a dummy io::Error from poll_read).
224
    ///
225
    /// NOTE: ideally, this would be an io::Result, but io::Error isn't Clone (the tests need to
226
    /// clone the iterator over these Results for each HSDir).
227
    type PollReadResult<T> = Result<T, ()>;
228

            
229
    /// A trait for our poll_read response iterator.
230
    trait PollReadIter:
231
        Iterator<Item = PollReadResult<String>> + Send + Sync + Clone + Unpin + 'static
232
    {
233
    }
234

            
235
    impl<I> PollReadIter for I where
236
        I: Iterator<Item = PollReadResult<String>> + Send + Sync + Clone + Unpin + 'static
237
    {
238
    }
239

            
240
    #[derive(Clone, Debug, Default)]
241
    struct MockReactorState<I: PollReadIter> {
242
        /// The number of `POST /tor/hs/3/publish` requests sent by the reactor.
243
        publish_count: Arc<AtomicUsize>,
244
        /// The values returned by `DataStream::poll_read` when uploading to an HSDir.
245
        ///
246
        /// The values represent the HTTP response (or lack thereof) each HSDir sends upon
247
        /// receiving a POST request for uploading a descriptor.
248
        ///
249
        /// Note: this field is only used for populating responses_for_hsdir. Each time
250
        /// get_or_launch_specific is called for a new CircTarget, this iterator is cloned and
251
        /// added to the responses_for_hsdir entry corresponding to the new CircTarget (HSDir).
252
        poll_read_responses: I,
253
        /// The responses that will be returned by each test HSDir (identified by its RsaIdentity).
254
        ///
255
        /// Used for testing whether the reactor correctly retries on failure.
256
        responses_for_hsdir: Arc<Mutex<HashMap<rsa::RsaIdentity, I>>>,
257
    }
258

            
259
    #[async_trait]
260
    impl<I: PollReadIter> Mockable for MockReactorState<I> {
261
        type Rng = TestingRng;
262
        type Tunnel = MockClientCirc<I>;
263

            
264
        fn thread_rng(&self) -> Self::Rng {
265
            testing_rng()
266
        }
267

            
268
        async fn get_or_launch_hs_dir<T>(
269
            &self,
270
            _netdir: &tor_netdir::NetDir,
271
            target: T,
272
        ) -> Result<Self::Tunnel, tor_circmgr::Error>
273
        where
274
            T: tor_linkspec::CircTarget + Send + Sync,
275
        {
276
            // Look up the next poll_read value to return for this relay.
277
            let id = target.rsa_identity().unwrap();
278
            let mut map = self.responses_for_hsdir.lock().unwrap();
279
            let poll_read_responses = map
280
                .entry(*id)
281
                .or_insert_with(|| self.poll_read_responses.clone());
282

            
283
            Ok(MockClientCirc {
284
                publish_count: Arc::clone(&self.publish_count),
285
                poll_read_responses: poll_read_responses.clone(),
286
            })
287
        }
288

            
289
        fn estimate_upload_timeout(&self) -> Duration {
290
            // chosen arbitrarily for testing.
291
            Duration::from_secs(30)
292
        }
293
    }
294

            
295
    #[derive(Debug, Clone)]
296
    struct MockClientCirc<I: PollReadIter> {
297
        /// The number of `POST /tor/hs/3/publish` requests sent by the reactor.
298
        publish_count: Arc<AtomicUsize>,
299
        /// The values to return from `poll_read`.
300
        ///
301
        /// Used for testing whether the reactor correctly retries on failure.
302
        poll_read_responses: I,
303
    }
304

            
305
    #[async_trait]
306
    impl<I: PollReadIter> MockableDirTunnel for MockClientCirc<I> {
307
        type DataStream = MockDataStream<I>;
308

            
309
        async fn begin_dir_stream(&self) -> Result<Self::DataStream, tor_circmgr::Error> {
310
            Ok(MockDataStream {
311
                publish_count: Arc::clone(&self.publish_count),
312
                // TODO: this will need to change when we start reusing circuits (currently,
313
                // we only ever create one data stream per circuit).
314
                poll_read_responses: self.poll_read_responses.clone(),
315
                at_start: true,
316
            })
317
        }
318

            
319
        fn source_info(&self) -> tor_proto::Result<Option<tor_dirclient::SourceInfo>> {
320
            Ok(None)
321
        }
322
    }
323

            
324
    #[derive(Debug)]
325
    struct MockDataStream<I: PollReadIter> {
326
        /// The number of `POST /tor/hs/3/publish` requests sent by the reactor.
327
        publish_count: Arc<AtomicUsize>,
328
        /// The values to return from `poll_read`.
329
        ///
330
        /// Used for testing whether the reactor correctly retries on failure.
331
        poll_read_responses: I,
332
        /// Are we at the start of a stream?
333
        ///
334
        /// (We keep track of this so we can assert that streams begin with a reasonable
335
        /// HTTP header.)
336
        at_start: bool,
337
    }
338

            
339
    impl<I: PollReadIter> AsyncRead for MockDataStream<I> {
340
        fn poll_read(
341
            mut self: Pin<&mut Self>,
342
            _cx: &mut Context<'_>,
343
            buf: &mut [u8],
344
        ) -> Poll<io::Result<usize>> {
345
            match self.as_mut().poll_read_responses.next() {
346
                Some(res) => {
347
                    match res {
348
                        Ok(res) => {
349
                            buf[..res.len()].copy_from_slice(res.as_bytes());
350

            
351
                            Poll::Ready(Ok(res.len()))
352
                        }
353
                        Err(()) => {
354
                            // Return an error. This should cause the reactor to reattempt the
355
                            // upload.
356
                            Poll::Ready(Err(io::Error::other("test error")))
357
                        }
358
                    }
359
                }
360
                None => Poll::Ready(Ok(0)),
361
            }
362
        }
363
    }
364

            
365
    impl<I: PollReadIter> AsyncWrite for MockDataStream<I> {
366
        fn poll_write(
367
            mut self: Pin<&mut Self>,
368
            _cx: &mut Context<'_>,
369
            buf: &[u8],
370
        ) -> Poll<io::Result<usize>> {
371
            let request = std::str::from_utf8(buf).unwrap();
372

            
373
            if self.at_start {
374
                assert!(request.starts_with("POST /tor/hs/3/publish HTTP/1.0\r\n"));
375
                let _prev = self.publish_count.fetch_add(1, Ordering::SeqCst);
376
                self.as_mut().at_start = false;
377
            }
378

            
379
            Poll::Ready(Ok(request.len()))
380
        }
381

            
382
        fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
383
            Poll::Ready(Ok(()))
384
        }
385

            
386
        fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
387
            Poll::Ready(Ok(()))
388
        }
389
    }
390

            
391
    /// Insert the specified key into the keystore.
392
    fn insert_svc_key<K>(key: K, keymgr: &KeyMgr, svc_key_spec: &dyn KeySpecifier)
393
    where
394
        K: ToEncodableKey,
395
    {
396
        keymgr
397
            .insert(
398
                key,
399
                svc_key_spec,
400
                tor_keymgr::KeystoreSelector::Primary,
401
                true,
402
            )
403
            .unwrap();
404
    }
405

            
406
    /// Create a new `KeyMgr`, provisioning its keystore with the necessary keys.
407
    fn init_keymgr(
408
        keystore_dir: &TempDir,
409
        nickname: &HsNickname,
410
        netdir: &NetDir,
411
    ) -> (HsId, HsBlindId, Arc<KeyMgr>) {
412
        let period = netdir.hs_time_period();
413

            
414
        let mut rng = testing_rng();
415
        let keypair = ed25519::Keypair::generate(&mut rng);
416
        let id_pub = HsIdKey::from(keypair.verifying_key());
417
        let id_keypair = HsIdKeypair::from(ed25519::ExpandedKeypair::from(&keypair));
418

            
419
        let (hs_blind_id_key, hs_blind_id_kp, _subcredential) =
420
            id_keypair.compute_blinded_key(period).unwrap();
421

            
422
        let keystore = ArtiNativeKeystore::from_path_and_mistrust(
423
            keystore_dir,
424
            &Mistrust::new_dangerously_trust_everyone(),
425
        )
426
        .unwrap();
427

            
428
        // Provision the keystore with the necessary keys:
429
        let keymgr = KeyMgrBuilder::default()
430
            .primary_store(Box::new(keystore))
431
            .build()
432
            .unwrap();
433

            
434
        insert_svc_key(
435
            id_keypair,
436
            &keymgr,
437
            &HsIdKeypairSpecifier::new(nickname.clone()),
438
        );
439

            
440
        insert_svc_key(
441
            id_pub.clone(),
442
            &keymgr,
443
            &HsIdPublicKeySpecifier::new(nickname.clone()),
444
        );
445

            
446
        insert_svc_key(
447
            hs_blind_id_kp,
448
            &keymgr,
449
            &BlindIdKeypairSpecifier::new(nickname.clone(), period),
450
        );
451

            
452
        insert_svc_key(
453
            hs_blind_id_key.clone(),
454
            &keymgr,
455
            &BlindIdPublicKeySpecifier::new(nickname.clone(), period),
456
        );
457

            
458
        insert_svc_key(
459
            HsDescSigningKeypair::from(ed25519::Keypair::generate(&mut rng)),
460
            &keymgr,
461
            &DescSigningKeypairSpecifier::new(nickname.clone(), period),
462
        );
463

            
464
        let hs_id = id_pub.into();
465
        (hs_id, hs_blind_id_key.into(), keymgr.into())
466
    }
467

            
468
    fn build_test_config(nickname: HsNickname) -> OnionServiceConfig {
469
        OnionServiceConfigBuilder::default()
470
            .nickname(nickname)
471
            .rate_limit_at_intro(None)
472
            .build()
473
            .unwrap()
474
    }
475

            
476
    #[allow(clippy::too_many_arguments)]
477
    fn run_test<I: PollReadIter>(
478
        runtime: MockRuntime,
479
        nickname: HsNickname,
480
        keymgr: Arc<KeyMgr>,
481
        pv: IptsPublisherView,
482
        config_rx: watch::Receiver<Arc<OnionServiceConfig>>,
483
        status_tx: StatusSender,
484
        netdir: NetDir,
485
        reactor_event: impl FnOnce(),
486
        poll_read_responses: I,
487
        expected_upload_count: usize,
488
        republish_count: usize,
489
        expect_errors: bool,
490
    ) {
491
        runtime.clone().block_on(async move {
492
            let netdir_provider: Arc<dyn NetDirProvider> =
493
                Arc::new(TestNetDirProvider::from(netdir));
494
            let publish_count = Default::default();
495
            let circpool = MockReactorState {
496
                publish_count: Arc::clone(&publish_count),
497
                poll_read_responses,
498
                responses_for_hsdir: Arc::new(Mutex::new(Default::default())),
499
            };
500

            
501
            let temp_dir = test_temp_dir!();
502
            let state_dir = temp_dir.subdir_untracked("state_dir");
503
            let mistrust = fs_mistrust::Mistrust::new_dangerously_trust_everyone();
504
            let state_dir = StateDirectory::new(state_dir, &mistrust).unwrap();
505
            let state_handle = state_dir.acquire_instance(&nickname).unwrap();
506
            let pow_nonce_dir = state_handle.raw_subdir("pow_nonces").unwrap();
507
            let pow_manager_storage_handle = state_handle.storage_handle("pow_manager").unwrap();
508
            let bad_hsdescs_dir = state_handle.raw_subdir("bad_hsdescs").unwrap();
509

            
510
            let NewPowManager {
511
                pow_manager,
512
                rend_req_tx: _,
513
                rend_req_rx: _,
514
                publisher_update_rx: update_from_pow_manager_rx,
515
            } = PowManager::new(
516
                runtime.clone(),
517
                nickname.clone(),
518
                pow_nonce_dir,
519
                keymgr.clone(),
520
                pow_manager_storage_handle,
521
                netdir_provider.clone(),
522
                status_tx.clone().into(),
523
                config_rx.clone(),
524
            )
525
            .unwrap();
526
            let mut status_rx = status_tx.subscribe();
527
            let publisher: Publisher<MockRuntime, MockReactorState<_>> = Publisher::new(
528
                runtime.clone(),
529
                nickname,
530
                netdir_provider,
531
                circpool,
532
                pv,
533
                config_rx,
534
                status_tx.into(),
535
                keymgr,
536
                Arc::new(CfgPathResolver::default()),
537
                pow_manager,
538
                update_from_pow_manager_rx,
539
                bad_hsdescs_dir,
540
            );
541

            
542
            publisher.launch().unwrap();
543
            runtime.progress_until_stalled().await;
544
            let status = status_rx.next().await.unwrap().publisher_status();
545
            assert_eq!(State::Shutdown, status.state());
546
            assert!(status.current_problem().is_none());
547

            
548
            // Check that we haven't published anything yet
549
            assert_eq!(publish_count.load(Ordering::SeqCst), 0);
550

            
551
            reactor_event();
552

            
553
            runtime.progress_until_stalled().await;
554

            
555
            // We need to manually advance the time, because some of our tests check that the
556
            // failed uploads are retried, and there's a sleep() between the retries
557
            // (see BackoffSchedule::next_delay).
558
            runtime.advance_by(Duration::from_secs(1)).await;
559
            runtime.progress_until_stalled().await;
560

            
561
            let initial_publish_count = publish_count.load(Ordering::SeqCst);
562
            assert_eq!(initial_publish_count, expected_upload_count);
563

            
564
            let status = status_rx.next().await.unwrap().publisher_status();
565
            if expect_errors {
566
                // The upload results aren't ready yet.
567
                assert_eq!(State::Bootstrapping, status.state());
568
            } else {
569
                // The test network doesn't have an SRV for the previous TP,
570
                // so we are "unreachable".
571
                assert_eq!(State::DegradedUnreachable, status.state());
572
            }
573
            assert!(status.current_problem().is_none());
574

            
575
            if republish_count > 0 {
576
                /// The latest time the descriptor can be republished.
577
                const MAX_TIMEOUT: Duration = Duration::from_secs(60 * 120);
578

            
579
                // Wait until the reactor triggers the necessary number of reuploads.
580
                runtime
581
                    .advance_by(MAX_TIMEOUT * (republish_count as u32))
582
                    .await;
583
                runtime.progress_until_stalled().await;
584

            
585
                let min_upload_count = expected_upload_count * republish_count;
586
                // There will be twice as many reuploads if the publisher happens
587
                // to reupload every hour (as opposed to every 2h).
588
                let max_upload_count = 2 * min_upload_count;
589
                let publish_count_now = publish_count.load(Ordering::SeqCst);
590
                // This is the total number of reuploads (i.e. the number of times
591
                // we published the descriptor to an HsDir).
592
                let actual_reupload_count = publish_count_now - initial_publish_count;
593

            
594
                assert!((min_upload_count..=max_upload_count).contains(&actual_reupload_count));
595
            }
596
        });
597
    }
598

            
599
    /// Test that the publisher publishes the descriptor when the IPTs change.
600
    ///
601
    /// The `poll_read_responses` are returned by each HSDir, in order, in response to each POST
602
    /// request received from the publisher.
603
    ///
604
    /// The `multiplier` represents the multiplier by which to multiply the number of HSDirs to
605
    /// obtain the total expected number of uploads (this works because the test "HSDirs" all
606
    /// behave the same, so the number of uploads is the number of HSDirs multiplied by the number
607
    /// of retries).
608
    fn publish_after_ipt_change<I: PollReadIter>(
609
        temp_dir: &Path,
610
        poll_read_responses: I,
611
        multiplier: usize,
612
        republish_count: usize,
613
        expect_errors: bool,
614
    ) {
615
        let runtime = MockRuntime::new();
616
        let nickname = HsNickname::try_from(TEST_SVC_NICKNAME.to_string()).unwrap();
617
        let config = build_test_config(nickname.clone());
618
        let (_config_tx, config_rx) = watch::channel_with(Arc::new(config));
619

            
620
        let (mut mv, pv) = ipts_channel(&runtime, create_storage_handles(temp_dir).1).unwrap();
621
        let update_ipts = || {
622
            let ipts: Vec<IptInSet> = test_data::test_parsed_hsdesc()
623
                .unwrap()
624
                .intro_points()
625
                .iter()
626
                .enumerate()
627
                .map(|(i, ipt)| IptInSet {
628
                    ipt: ipt.clone(),
629
                    lid: [i.try_into().unwrap(); 32].into(),
630
                })
631
                .collect();
632

            
633
            mv.borrow_for_update(runtime.clone()).ipts = Some(IptSet {
634
                ipts,
635
                lifetime: Duration::from_secs(20),
636
            });
637
        };
638

            
639
        let netdir = testnet::construct_netdir().unwrap_if_sufficient().unwrap();
640
        let keystore_dir = tempdir().unwrap();
641

            
642
        let (_hsid, blind_id, keymgr) = init_keymgr(&keystore_dir, &nickname, &netdir);
643

            
644
        let hsdir_count = netdir
645
            .hs_dirs_upload(blind_id, netdir.hs_time_period())
646
            .unwrap()
647
            .collect::<Vec<_>>()
648
            .len();
649

            
650
        assert!(hsdir_count > 0);
651

            
652
        // If any of the uploads fail, they will be retried. Note that the upload failure will
653
        // affect _each_ hsdir, so the expected number of uploads is a multiple of hsdir_count.
654
        let expected_upload_count = hsdir_count * multiplier;
655
        let status_tx = StatusSender::new(OnionServiceStatus::new_shutdown());
656

            
657
        run_test(
658
            runtime.clone(),
659
            nickname,
660
            keymgr,
661
            pv,
662
            config_rx,
663
            status_tx,
664
            netdir,
665
            update_ipts,
666
            poll_read_responses,
667
            expected_upload_count,
668
            republish_count,
669
            expect_errors,
670
        );
671
    }
672

            
673
    #[test]
674
    fn publish_after_ipt_change_no_errors() {
675
        // The HSDirs always respond with 200 OK, so we expect to publish hsdir_count times.
676
        let poll_reads = [Ok(OK_RESPONSE.into())].into_iter();
677

            
678
        test_temp_dir!().used_by(|dir| publish_after_ipt_change(dir, poll_reads, 1, 0, false));
679
    }
680

            
681
    #[test]
682
    fn publish_after_ipt_change_with_errors() {
683
        let err_responses = vec![
684
            // The HSDir closed the connection without sending a response.
685
            Err(()),
686
            // The HSDir responded with an internal server error,
687
            Ok(ERR_RESPONSE.to_string()),
688
        ];
689

            
690
        for error_res in err_responses.into_iter() {
691
            let poll_reads = vec![
692
                // Each HSDir first responds with an error, which causes the publisher to retry the
693
                // upload. The HSDir then responds with "200 OK".
694
                //
695
                // We expect to publish hsdir_count * 2 times (for each HSDir, the first upload
696
                // attempt fails, but the second succeeds).
697
                error_res,
698
                Ok(OK_RESPONSE.to_string()),
699
            ]
700
            .into_iter();
701

            
702
            test_temp_dir!().used_by(|dir| publish_after_ipt_change(dir, poll_reads, 2, 0, true));
703
        }
704
    }
705

            
706
    #[test]
707
    fn reupload_after_publishing() {
708
        let poll_reads = [Ok(OK_RESPONSE.into())].into_iter();
709
        // Test that 4 reuploads happen after the initial upload
710
        const REUPLOAD_COUNT: usize = 4;
711

            
712
        test_temp_dir!()
713
            .used_by(|dir| publish_after_ipt_change(dir, poll_reads, 1, REUPLOAD_COUNT, false));
714
    }
715

            
716
    // TODO (#1120): test that the descriptor is republished when the config changes
717

            
718
    // TODO (#1120): test that the descriptor is reuploaded only to the HSDirs that need it (i.e. the
719
    // ones for which it's dirty)
720

            
721
    // TODO (#1120): test that rate-limiting works correctly
722

            
723
    // TODO (#1120): test that the uploaded descriptor contains the expected values
724

            
725
    // TODO (#1120): test that the publisher stops publishing if the IPT manager sets the IPTs to
726
    // `None`.
727
}