use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use claviron::edge::{
AnyAuthenticated, AuthContext, H3WebTransportBiStream,
H3WebTransportSession, Host, HostConfig, Routable, WebTransportAccept,
WebTransportHandler, WebTransportRegistry, WebTransportReject,
WebTransportResult,
};
use claviron::http::ext::PathPrefix;
use claviron::orbit::link::application::{MessageReader, write_message};
use claviron::runtime::AppState;
use claviron::runtime::registry::{self, Component, Error};
use http::StatusCode;
use tokio::io::AsyncWriteExt;
use tokio::sync::{mpsc, watch};
use tokio::task::JoinSet;
use crate::asgi::{
MessageTransport, PythonApplicationHandle, WebSocketDecision,
WebSocketInbound, WebSocketSession,
};
use crate::backend::declared;
use crate::service::Handle;
use crate::websocket::{DEFAULT_MESSAGE_LIMIT, Message};
pub(crate) fn register(
state: &AppState,
handle: Handle,
) -> registry::Result<()> {
claviron::edge::register_routable(state, Arc::new(Sessions { handle }))
}
struct Sessions {
handle: Handle,
}
impl Component<AppState> for Sessions {
type Deps = ();
fn name(&self) -> &str {
"PythonEdgeSessions"
}
}
impl Routable for Sessions {
fn match_host(
&self,
_: &Host,
host: &HostConfig,
) -> bool {
endpoints(host).is_ok_and(|endpoints| !endpoints.is_empty())
}
fn prepare_sessions(
&self,
state: &AppState,
key: &Host,
host: &HostConfig,
) -> registry::Result<()> {
let endpoints = endpoints(host).map_err(|error| {
Error::msg(format!("python directive of host '{key}': {error}"))
})?;
if endpoints.is_empty() {
return Ok(());
}
let registry = state
.registry_ref()
.resolve::<WebTransportRegistry>()
.ok_or_else(|| {
Error::msg("Python WebTransport requires the WebTransport registry")
})?;
for endpoint in endpoints {
registry.bind_handler(
key.clone(),
endpoint.path.clone(),
Arc::new(Handler {
handle: Arc::clone(&self.handle),
endpoint,
}),
)?;
}
Ok(())
}
}
struct Endpoint {
path: Arc<str>,
source: Arc<serde_yaml::Value>,
mount: Option<PathPrefix>,
}
impl Endpoint {
fn still_declared(
&self,
host: &claviron_pipeline::Site,
) -> bool {
let selected =
host.routes.find(&self.path).map(|route| &**route).unwrap_or(host);
selected.directive("python") == Some(&*self.source)
&& selected.files.mapping.as_ref().map(|mapping| mapping.prefix())
== self.mount.as_ref()
}
}
fn endpoints(host: &HostConfig) -> Result<Vec<Endpoint>, serde_yaml::Error> {
let mut endpoints = Vec::new();
for (_, scope) in host.scopes() {
let Some(route) = declared(scope)? else { continue };
if route.webtransport.is_empty() {
continue;
}
let source =
Arc::new(scope.directive("python").cloned().unwrap_or_default());
let mount = scope
.files
.mapping
.as_ref()
.map(|mapping| mapping.prefix().clone());
for path in &route.webtransport {
endpoints.push(Endpoint {
path: mounted(mount.as_ref(), path),
source: Arc::clone(&source),
mount: mount.clone(),
});
}
}
Ok(endpoints)
}
fn mounted(
mount: Option<&PathPrefix>,
path: &PathPrefix,
) -> Arc<str> {
match mount.map(PathPrefix::as_str) {
None | Some("/") => Arc::from(path.as_str()),
Some(mount) if path.as_str() == "/" => Arc::from(mount),
Some(mount) => Arc::from(format!("{mount}{}", path.as_str())),
}
}
struct Handler {
handle: Handle,
endpoint: Endpoint,
}
struct Opened {
inbound: WebSocketInbound,
outbound: mpsc::Receiver<Message>,
}
struct First(Mutex<Option<Opened>>);
impl Handler {
fn application(&self) -> Option<PythonApplicationHandle> {
self.handle.read().expect("python handle lock").clone()
}
}
async fn open(
python: &PythonApplicationHandle,
ctx: &AuthContext,
) -> Result<Opened, StatusCode> {
let mut request = http::Request::new(());
*request.method_mut() = http::Method::CONNECT;
*request.uri_mut() = ctx.uri.clone();
*request.version_mut() = http::Version::HTTP_3;
*request.headers_mut() = ctx.headers.clone();
request.extensions_mut().insert(Arc::clone(&ctx.conn));
let (parts, ()) = request.into_parts();
let scope = crate::routing::request_scope(&parts);
let WebSocketSession { decision, inbound, outbound } = python
.handle_message_session(scope, MessageTransport::WebTransport)
.map_err(|_| StatusCode::SERVICE_UNAVAILABLE)?;
match decision.await {
Ok(WebSocketDecision::Accept { .. }) => {
Ok(Opened { inbound, outbound })
}
_ => {
inbound.end();
Err(StatusCode::FORBIDDEN)
}
}
}
#[async_trait]
impl WebTransportHandler for Handler {
fn name(&self) -> &'static str {
"python_webtransport"
}
async fn accept(
&self,
_: &AppState,
ctx: &AuthContext,
) -> WebTransportResult {
if ctx.admission.is_none() {
return Err(WebTransportReject::new(
StatusCode::SERVICE_UNAVAILABLE,
));
}
if *ctx.path != *self.endpoint.path
|| !self.endpoint.still_declared(&ctx.host_config)
{
return Err(WebTransportReject::new(StatusCode::NOT_FOUND));
}
let python = self.application().ok_or_else(|| {
WebTransportReject::new(StatusCode::SERVICE_UNAVAILABLE)
})?;
let first =
open(&python, ctx).await.map_err(WebTransportReject::new)?;
Ok(WebTransportAccept::authenticated(First(Mutex::new(Some(first)))))
}
async fn handle(
&self,
state: AppState,
ctx: AuthContext,
accepted: Option<AnyAuthenticated>,
session: H3WebTransportSession,
) -> registry::Result<()> {
let mut first = accepted
.ok_or_else(|| {
Error::msg("Python WebTransport acceptance missing")
})?
.take::<First>()?
.0
.into_inner()
.expect("first stream lock");
let (stop, stopped) = watch::channel(false);
let mut streams = JoinSet::new();
let shutdown = async {
tokio::select! {
() = ctx.wait_graceful_shutdown() => {}
() = state.on_shutdown() => {}
_ = session.closed() => {}
}
};
tokio::pin!(shutdown);
loop {
tokio::select! {
biased;
() = &mut shutdown => break,
completed = streams.join_next(), if !streams.is_empty() => {
if let Some(Err(error)) = completed {
tracing::debug!(%error, "Python WebTransport stream ended");
}
}
stream = session.accept_bi(),
if streams.len() < session.incoming_capacity().get() =>
{
let Some(stream) = stream else { break };
let opened = match first.take() {
Some(opened) => Ok(opened),
None => match self.application() {
Some(python) => open(&python, &ctx).await,
None => Err(StatusCode::SERVICE_UNAVAILABLE),
},
};
match opened {
Ok(opened) => {
streams.spawn(serve(stream, opened, stopped.clone()));
}
Err(status) => {
tracing::debug!(%status, "Python WebTransport stream refused");
}
}
}
}
}
if let Some(opened) = first {
opened.inbound.end();
}
let _ = stop.send(true);
while let Some(result) = streams.join_next().await {
if let Err(error) = result {
tracing::debug!(%error, "Python WebTransport stream drain failed");
}
}
Ok(())
}
}
async fn serve(
stream: H3WebTransportBiStream,
opened: Opened,
stopped: watch::Receiver<bool>,
) {
let Opened { inbound, outbound } = opened;
let (mut incoming, mut outgoing) = tokio::io::split(stream);
relay(&mut incoming, &mut outgoing, &inbound, outbound, stopped).await;
}
async fn relay<R, W>(
incoming: &mut R,
outgoing: &mut W,
inbound: &WebSocketInbound,
mut outbound: mpsc::Receiver<Message>,
mut stopped: watch::Receiver<bool>,
) where
R: tokio::io::AsyncRead + Unpin,
W: tokio::io::AsyncWrite + Unpin,
{
let receive = async {
let mut reader = MessageReader::for_application(DEFAULT_MESSAGE_LIMIT);
loop {
let message = tokio::select! {
biased;
_ = stopped.wait_for(|stop| *stop) => break,
message = reader.read(incoming) => message,
};
let Ok(Some(message)) = message else { break };
let close = matches!(message, Message::Close { .. });
if !inbound.deliver(message).await || close {
break;
}
}
inbound.end();
};
let send = async {
while let Some(message) = outbound.recv().await {
let close = matches!(message, Message::Close { .. });
if write_message(outgoing, &message).await.is_err()
|| outgoing.flush().await.is_err()
|| close
{
break;
}
}
let _ = outgoing.shutdown().await;
};
tokio::pin!(receive, send);
tokio::select! {
() = &mut receive => send.await,
() = &mut send => inbound.end(),
}
}