diff --git a/README.md b/README.md index 703eeff..3412a93 100644 --- a/README.md +++ b/README.md @@ -220,9 +220,9 @@ Useful controls: --pairing-issuer issue one-use Foregather live grants to devices --pairing-relay-url Foregather API base for --pairing-issuer --pairing-operator-token-file private file holding its operator token ---turn-issuer issue short-lived TURN credentials to devices +--turn-issuer hand out direct-send ICE servers: STUN alone, or TURN with short-lived credentials --turn-url stun:, turn: or turns: URL for --turn-issuer, 1 to 4 ---turn-secret-file private file holding the TURN static-auth-secret +--turn-secret-file private file holding the TURN static-auth-secret (only with a turn: or turns: URL) --service-profile opt-in public service listing (docs/SERVICE-DISCOVERY.md) --checkout-profile opt-in paid checkout, needs --no-tor (docs/CHECKOUT.md) --max-concurrent-writes concurrent upload/mirror limit, default 4 diff --git a/crates/wildbloomd/src/grants/tests.rs b/crates/wildbloomd/src/grants/tests.rs index 73991f3..0d1c3f6 100644 --- a/crates/wildbloomd/src/grants/tests.rs +++ b/crates/wildbloomd/src/grants/tests.rs @@ -1161,7 +1161,10 @@ async fn tokens_and_authorisation_events_never_reach_the_logs() { logs.contains("accepted an owner-signed device grant"), "{logs}" ); - assert!(logs.contains("issued a pair of TURN credentials"), "{logs}"); + assert!( + logs.contains("issued a pair of ICE configurations"), + "{logs}" + ); let mut secrets = vec![ URL_SAFE_NO_PAD.encode(OPERATOR_TOKEN_BYTES), URL_SAFE_NO_PAD.encode([9_u8; 32]), @@ -2066,7 +2069,7 @@ fn turn_app(node: &Node) -> Router { TurnIssuer::new( node.grants.clone(), urls, - TurnSecret::parse(TURN_SECRET).unwrap(), + Some(TurnSecret::parse(TURN_SECRET).unwrap()), ) .router() } @@ -2186,6 +2189,48 @@ async fn turn_issuer_returns_two_fresh_credentials_to_a_granted_device() { assert_eq!(distinct.len(), labels.len()); } +#[tokio::test] +async fn a_stun_only_issuer_needs_no_secret_and_hands_out_empty_credentials() { + let (_dir, node) = node().await; + let urls = vec!["stun:stun.example.net:3478".to_owned()]; + let urls = super::turn::ice_urls(&urls).unwrap(); + assert!(!super::turn::has_turn_url(&urls)); + let app = TurnIssuer::new(node.grants.clone(), urls, None).router(); + let now = node.now(); + node.call(post_grant(grant_event(&keys(1), &standard_grant(), now))) + .await; + let response = app + .oneshot(turn_request(&keys(10), now, b"")) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::CREATED); + assert_eq!(response.headers()[header::CACHE_CONTROL], "no-store"); + let bytes = to_bytes(response.into_body(), 1 << 20).await.unwrap(); + let text = String::from_utf8(bytes.to_vec()).unwrap(); + let body = serde_json::from_slice::(&bytes).unwrap(); + let expires = body["expires"].as_u64().unwrap(); + let entry = r#"{"credential":"","urls":["stun:stun.example.net:3478"],"username":""}"#; + assert_eq!( + text, + format!(r#"{{"expires":{expires},"own":{entry},"peer":{entry}}}"#) + ); +} + +#[tokio::test] +async fn a_turn_issuer_without_its_secret_is_unavailable_rather_than_unsigned() { + let (_dir, node) = node().await; + let urls = super::turn::ice_urls(&TURN_URLS.map(str::to_owned)).unwrap(); + let app = TurnIssuer::new(node.grants.clone(), urls, None).router(); + let now = node.now(); + node.call(post_grant(grant_event(&keys(1), &standard_grant(), now))) + .await; + let response = app + .oneshot(turn_request(&keys(10), now, b"")) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); +} + #[tokio::test] async fn turn_issuer_refuses_unauthorised_requests() { let (_dir, node) = node().await; diff --git a/crates/wildbloomd/src/grants/turn.rs b/crates/wildbloomd/src/grants/turn.rs index 4c1638f..29ceb76 100644 --- a/crates/wildbloomd/src/grants/turn.rs +++ b/crates/wildbloomd/src/grants/turn.rs @@ -1,6 +1,9 @@ -//! Opt-in TURN credential issuer: a currently granted device exchanges one -//! fresh NIP-98 request for two short-lived TURN REST credentials, one for -//! itself and one for the verified person at the other end of a direct send. +//! Opt-in ICE configuration issuer: a currently granted device exchanges one +//! fresh NIP-98 request for two entries, one for itself and one for the +//! verified person at the other end of a direct send. If any configured URL +//! is TURN, each entry holds short-lived TURN REST credentials. If every URL +//! is STUN, nothing is minted and no secret is held: both entries carry the +//! URLs with an empty username and credential. //! //! Credentials use coturn's `use-auth-secret` form: the username is //! `: