use super::*;
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub(super) struct TestLane {
run_id: String,
control_plane: String,
gateway: String,
source_digest: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
relay_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
ticket_lifetime_ms: Option<u64>,
}
impl TestLane {
pub(super) fn parse(arguments: &[String]) -> Result<Option<Self>> {
if arguments.is_empty() {
return Ok(None);
}
if arguments.len() != 2 || arguments[0] != "--test-lane" || arguments[1].len() > 2048 {
bail!("invalid Node host test lane arguments");
}
let lane: Self =
serde_json::from_str(&arguments[1]).context("decode local Node test lane")?;
lane.validate(option_env!("OPENRTC_NODE_HOST_TEST_SOURCE_DIGEST").unwrap_or("unbound"))?;
Ok(Some(lane))
}
fn validate(&self, compiled_digest: &str) -> Result<()> {
if self.ticket_lifetime_ms.is_some_and(|value| value != 90_000) {
bail!("local ticket renewal fixture requires exactly 90 seconds");
}
bounded_id("runId", self.run_id.clone(), 80)?;
if self.source_digest.len() != 64
|| !self.source_digest.bytes().all(|b| b.is_ascii_hexdigit())
|| self.source_digest != compiled_digest
{
bail!("Node host test source is stale or unbound");
}
for value in [&self.control_plane, &self.gateway] {
let url = reqwest::Url::parse(value)?;
if url.scheme() != "http"
|| !url
.host_str()
.is_some_and(|host| matches!(host, "127.0.0.1" | "[::1]"))
|| url.port().is_none_or(|port| port == 0)
|| !url.username().is_empty()
|| url.password().is_some()
|| url.query().is_some()
|| url.fragment().is_some()
|| url.path() != "/"
{
bail!("Node host test endpoints must be explicit loopback HTTP origins");
}
}
if let Some(value) = &self.relay_url {
let url = reqwest::Url::parse(value)?;
if url.scheme() != "https"
|| !matches!(url.host_str(), Some("127.0.0.1" | "localhost" | "[::1]"))
|| url.port().is_none_or(|port| port == 0)
|| !url.username().is_empty()
|| url.password().is_some()
|| url.query().is_some()
|| url.fragment().is_some()
|| url.path() != "/"
{
bail!("Node host test relay must be an explicit loopback HTTPS origin");
}
}
Ok(())
}
pub(super) fn configure(
&self,
control: ControlPlane,
config: &HostConfig,
) -> Result<ControlPlane> {
let expected_secret = if self.relay_url.is_some() {
"sk_test_integration_plutonium"
} else {
"sk_test_node_host_local"
};
if config.api_key != "pk_test_0000000000000000000000000000000000000000"
|| config.secret_key != expected_secret
|| config.iroh_relay != self.relay_url.is_some()
{
bail!(
"local Node tests require disposable credentials and matching local relay policy"
);
}
control.with_testing_endpoints(&self.control_plane, &self.gateway)
}
pub(super) fn prepare_ticket(&self, client: &Client, ticket: String) -> Result<String> {
let Some(lifetime) = self.ticket_lifetime_ms else {
return Ok(ticket);
};
use openrtc::session_token::{decode_token_payload, expiring_ticket, split_ticket};
let (endpoint, suffix) = split_ticket(&ticket);
let payload = decode_token_payload(suffix.context("missing local ticket payload")?)
.context("invalid local ticket payload")?;
let expires = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_millis() as u64
+ lifetime;
client.register_token_until(
payload.token.clone(),
payload.scope.to_string(),
payload.max_connections,
expires,
);
Ok(expiring_ticket(
endpoint,
&payload.token,
payload.scope,
payload.max_connections,
Some(expires),
))
}
pub(super) async fn endpoint(&self, client: &Client, secret: &[u8; 32]) -> Result<String> {
let builder = iroh::Endpoint::builder(iroh::endpoint::presets::Minimal)
.secret_key(iroh::SecretKey::from_bytes(secret))
.relay_mode(iroh::RelayMode::Disabled)
.alpns(vec![openrtc::native_node::PlutoniumProtocol::ALPN.to_vec()])
.bind_addr("127.0.0.1:0".parse::<std::net::SocketAddr>()?)?;
let builder = if let Some(url) = &self.relay_url {
#[cfg(not(feature = "test-relay-client"))]
bail!("local relay requires the test-relay-client feature: {url}");
#[cfg(feature = "test-relay-client")]
builder
.relay_mode(iroh::RelayMode::custom([url.parse::<iroh::RelayUrl>()?]))
.ca_tls_config(iroh::tls::CaTlsConfig::insecure_skip_verify())
} else {
builder
};
let endpoint = builder.bind().await?;
if self.relay_url.is_some() {
tokio::time::timeout(std::time::Duration::from_secs(10), endpoint.online())
.await
.context("local Node relay did not become ready before publication")?;
}
let id = endpoint.id().to_string();
client
.adopt_endpoint_with_router_mode(endpoint, true)
.await?;
Ok(id)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn local_lane_rejects_remote_origins_and_stale_source() {
let mut lane = TestLane {
run_id: "run-1".into(),
control_plane: "http://127.0.0.1:5004".into(),
gateway: "http://127.0.0.1:8787".into(),
source_digest: "a".repeat(64),
relay_url: None,
ticket_lifetime_ms: None,
};
lane.validate(&"a".repeat(64)).unwrap();
lane.ticket_lifetime_ms = Some(90_000);
lane.validate(&"a".repeat(64)).unwrap();
lane.ticket_lifetime_ms = Some(0);
assert!(lane.validate(&"a".repeat(64)).is_err());
lane.ticket_lifetime_ms = None;
assert!(lane.validate("unbound").is_err());
for origin in [
"http://example.test:8080",
"https://127.0.0.1:8080",
"http://localhost:8080",
"http://127.0.0.1",
"http://u:p@127.0.0.1:8080",
"http://127.0.0.1:8080/?token=x",
] {
lane.gateway = origin.into();
assert!(lane.validate(&"a".repeat(64)).is_err(), "{origin}");
}
}
#[test]
fn local_lane_rejects_public_or_credentialed_relays() {
let mut lane = TestLane {
run_id: "run-1".into(),
control_plane: "http://127.0.0.1:5004".into(),
gateway: "http://127.0.0.1:8787".into(),
source_digest: "a".repeat(64),
relay_url: Some("https://localhost:9444".into()),
ticket_lifetime_ms: None,
};
lane.validate(&"a".repeat(64)).unwrap();
for url in [
"https://relay.example:9444",
"http://127.0.0.1:9444",
"https://localhost",
"https://u:p@localhost:9444",
"https://localhost:9444/?token=x",
] {
lane.relay_url = Some(url.into());
assert!(lane.validate(&"a".repeat(64)).is_err(), "{url}");
}
}
}