1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
//! Tests for [`super`] — what `serve` is given, and how it refuses.
//!
//! Split out via `#[path]` so `serve.rs` stays inside the file-size
//! budget, the same way every other module in the crate does it.
use std::time::Duration;
use super::*;
use crate::token_policy::TokenPolicy;
/// Distinct from the sentinel `credential.rs` uses, so a leak names
/// the type it escaped through.
const SUPPLIED: &str = "sk-zzq-serve-options-sentinel";
const UPSTREAM: &str = "sk-zzq-serve-options-upstream-sentinel";
/// `ServeOptions` is the type an embedder is most likely to hold in a
/// struct of their own and derive `Debug` on, which is how a supplied
/// credential reaches a log without anyone deciding it should.
#[test]
fn debug_for_serve_options_never_renders_the_supplied_token() {
// A struct literal rather than the `Default`-then-mutate dance an
// out-of-crate embedder is forced into by `#[non_exhaustive]`:
// inside the crate the literal is legal, and clippy rejects the
// dance. What is under test is the `Debug` impl, which cannot tell
// how the value was built.
let opts = ServeOptions {
auth: TokenPolicy::Supplied(SUPPLIED.to_owned()),
backend_auth: Some(UPSTREAM.to_owned()),
relay: Some("https://relay.example.com/".to_owned()),
..Default::default()
};
let rendered = format!("{opts:?}");
assert!(
!rendered.contains(SUPPLIED),
"the token leaked through ServeOptions: {rendered}"
);
assert!(
!rendered.contains(UPSTREAM),
"the upstream bearer leaked through ServeOptions: {rendered}"
);
assert!(
rendered.contains("backend_auth: Some(\"<redacted>\")"),
"but its presence is legible: {rendered}"
);
assert!(
rendered.contains("relay.example.com"),
"the non-secret fields should still be visible: {rendered}"
);
}
/// Running out of `wait_online` is a slower pairing, not a failure.
///
/// The wait exists because iroh's own has no end: it is satisfied by a
/// relay handshake, so on a machine with no route to one it would never
/// return. That makes the expiry path the one that has to be right —
/// a listener that refused to start where the internet is unreachable
/// would be a worse bug than the stale ticket this option exists to fix.
/// One millisecond is a deadline nothing can beat, so this exercises that
/// path without needing a network to be absent.
#[tokio::test]
async fn a_wait_that_times_out_still_yields_a_listener() {
let backend = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("a loopback port");
let url = format!("http://{}", backend.local_addr().expect("bound"));
let opts = ServeOptions {
wait_online: Some(Duration::from_millis(1)),
..Default::default()
};
let serving = serve(&url, opts)
.await
.expect("the deadline is not an error");
// And the listener it returns is a real one: it has a ticket to hand
// out, whatever the address set behind that ticket had time to become.
let _ = serving.ticket();
serving.shutdown().await;
}