use std::sync::Arc;
use axum::Json;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode};
use scp_core::well_known::{RelayConfig, WellKnownContext, WellKnownScp};
const QUERY_VALUE: &AsciiSet = &CONTROLS
.add(b'%')
.add(b'&')
.add(b'=')
.add(b'#')
.add(b'+')
.add(b' ');
use crate::http::NodeState;
#[must_use]
fn advertised_transports() -> Vec<String> {
#[allow(unused_mut)]
let mut transports = vec!["websocket".to_owned()];
#[cfg(feature = "quic")]
transports.push("quic".to_owned());
#[cfg(feature = "http3")]
transports.push("webtransport".to_owned());
#[cfg(feature = "udp")]
transports.push("udp-dtls".to_owned());
#[cfg(feature = "coap")]
transports.push("coap".to_owned());
transports
}
pub async fn build_well_known_scp(state: &NodeState) -> WellKnownScp {
let contexts = {
let guard = state.broadcast_contexts.read().await;
if guard.is_empty() {
None
} else {
Some(
guard
.values()
.map(|ctx| {
let encoded_relay = utf8_percent_encode(&state.relay_url, QUERY_VALUE);
let name_param = ctx
.name
.as_ref()
.map(|n| {
let encoded = utf8_percent_encode(n, QUERY_VALUE);
format!("&name={encoded}")
})
.unwrap_or_default();
let uri = format!(
"scp://context/{}?relay={encoded_relay}&mode=broadcast{name_param}",
ctx.id,
);
WellKnownContext {
id: ctx.id.clone(),
name: ctx.name.clone(),
mode: Some("broadcast".to_owned()),
uri: Some(uri),
}
})
.collect::<Vec<_>>(),
)
}
};
let rc = &state.relay_config;
let relay_config = RelayConfig {
max_blob_size: Some(rc.max_blob_size as u64),
max_blob_ttl: Some(u64::from(rc.max_blob_ttl)),
rate_limit_publish: Some(rc.rate_limit_publishes_per_second.saturating_mul(60)),
rate_limit_subscribe: Some(
u32::try_from(rc.max_subscriptions_per_connection).unwrap_or(u32::MAX),
),
transports: Some(advertised_transports()),
economic: None,
};
WellKnownScp {
version: 1,
did: state.did.clone(),
relay: state.relay_url.clone(),
contexts,
relay_config: Some(relay_config),
handles: None,
}
}
pub async fn well_known_handler(State(state): State<Arc<NodeState>>) -> impl IntoResponse {
let doc = build_well_known_scp(&state).await;
(StatusCode::OK, Json(doc))
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn advertised_transports_always_includes_websocket() {
let transports = advertised_transports();
assert!(
transports.contains(&"websocket".to_owned()),
"websocket must always be present in advertised transports"
);
assert_eq!(transports[0], "websocket");
}
#[test]
#[cfg(not(feature = "quic"))]
fn advertised_transports_excludes_quic_without_feature() {
let transports = advertised_transports();
assert!(
!transports.contains(&"quic".to_owned()),
"quic must not be advertised without the quic feature flag"
);
}
#[test]
#[cfg(feature = "quic")]
fn advertised_transports_includes_quic_with_feature() {
let transports = advertised_transports();
assert!(
transports.contains(&"quic".to_owned()),
"quic must be advertised when the quic feature flag is enabled"
);
}
#[test]
#[cfg(not(feature = "http3"))]
fn advertised_transports_excludes_webtransport_without_feature() {
let transports = advertised_transports();
assert!(
!transports.contains(&"webtransport".to_owned()),
"webtransport must not be advertised without the http3 feature flag"
);
}
#[test]
#[cfg(feature = "http3")]
fn advertised_transports_includes_webtransport_with_feature() {
let transports = advertised_transports();
assert!(
transports.contains(&"webtransport".to_owned()),
"webtransport must be advertised when the http3 feature flag is enabled"
);
}
#[test]
#[cfg(not(feature = "udp"))]
fn advertised_transports_excludes_udp_dtls_without_feature() {
let transports = advertised_transports();
assert!(
!transports.contains(&"udp-dtls".to_owned()),
"udp-dtls must not be advertised without the udp feature flag"
);
}
#[test]
#[cfg(feature = "udp")]
fn advertised_transports_includes_udp_dtls_with_feature() {
let transports = advertised_transports();
assert!(
transports.contains(&"udp-dtls".to_owned()),
"udp-dtls must be advertised when the udp feature flag is enabled"
);
}
#[test]
#[cfg(not(any(feature = "quic", feature = "http3", feature = "udp")))]
fn advertised_transports_default_is_websocket_only() {
let transports = advertised_transports();
assert_eq!(
transports,
vec!["websocket".to_owned()],
"without any transport feature flags, only websocket should be advertised"
);
}
}