use std::collections::HashMap;
use std::str::FromStr as _;
use std::sync::Arc;
use axum::Router as AxumRouter;
use axum::body::Body;
use axum::extract::{Query, State};
use axum::http::{HeaderMap, StatusCode, header};
use axum::response::{IntoResponse, Response};
use serde_json::Value;
use crate::adapters::primary::transport::http::limits::RateLimiter;
use crate::adapters::primary::transport::http::precondition::{authenticate, read_body};
use crate::adapters::primary::transport::http::{
Anonymous, AuthPolicy, Authenticator, EventTail, InvalidHeaderValue, Refusal, ServerConfig,
SessionStore, with_security,
};
use crate::adapters::primary::transport::legacy_sse::{
LegacyDispatcher, LegacyFrame, LegacySession, LegacyTail, OriginPolicy,
};
use crate::domain::registry::AuthContext;
use crate::domain::{McpSessionId, MessageEndpoint, MessageEndpointError, Verdict};
pub const SESSION_PARAMETER: &str = "sessionId";
const ACCEPTED: StatusCode = StatusCode::ACCEPTED;
pub const DEFAULT_STREAM_PATH: &str = "/sse";
pub const DEFAULT_MESSAGE_PATH: &str = "/messages";
#[derive(Debug, thiserror::Error)]
pub enum RouteError {
#[error("a configured security header is not a legal HTTP header value")]
Header(#[from] InvalidHeaderValue),
#[error("the {which} path `{path}` cannot be used: {reason}")]
Path {
which: &'static str,
path: String,
reason: MessageEndpointError,
},
}
struct LegacyState<D: LegacyDispatcher> {
message_path: MessageEndpoint,
config: ServerConfig,
sessions: Arc<SessionStore<LegacySession<D::Session>>>,
dispatcher: Arc<D>,
origins: Arc<OriginPolicy>,
policy: AuthPolicy,
authenticator: Arc<dyn Authenticator + Send + Sync>,
rate_limiter: RateLimiter,
}
impl<D: LegacyDispatcher> LegacyState<D> {
fn message_endpoint(&self, id: McpSessionId) -> MessageEndpoint {
self.message_path
.with_query_parameter(SESSION_PARAMETER, &id.to_string())
}
}
impl<D: LegacyDispatcher> Clone for LegacyState<D> {
fn clone(&self) -> Self {
Self {
message_path: self.message_path.clone(),
config: self.config.clone(),
sessions: Arc::clone(&self.sessions),
dispatcher: Arc::clone(&self.dispatcher),
origins: Arc::clone(&self.origins),
policy: self.policy,
authenticator: Arc::clone(&self.authenticator),
rate_limiter: self.rate_limiter.clone(),
}
}
}
pub struct LegacyRouter<D: LegacyDispatcher> {
message_path: String,
stream_path: String,
config: ServerConfig,
sessions: Arc<SessionStore<LegacySession<D::Session>>>,
dispatcher: Arc<D>,
origins: Arc<OriginPolicy>,
policy: AuthPolicy,
authenticator: Arc<dyn Authenticator + Send + Sync>,
rate_limiter: RateLimiter,
}
impl<D: LegacyDispatcher> core::fmt::Debug for LegacyRouter<D> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("LegacyRouter")
.field("stream_path", &self.stream_path)
.field("message_path", &self.message_path)
.field("permitted_origins", &self.origins.permitted().count())
.field("policy", &self.policy)
.finish_non_exhaustive()
}
}
impl<D: LegacyDispatcher> LegacyRouter<D> {
#[must_use]
pub fn new(
config: ServerConfig,
sessions: Arc<SessionStore<LegacySession<D::Session>>>,
dispatcher: D,
) -> Self {
Self {
message_path: DEFAULT_MESSAGE_PATH.to_owned(),
stream_path: DEFAULT_STREAM_PATH.to_owned(),
rate_limiter: RateLimiter::new(*config.rate_limit()),
config,
sessions,
dispatcher: Arc::new(dispatcher),
origins: Arc::new(OriginPolicy::strict(None)),
policy: AuthPolicy::Public,
authenticator: Arc::new(Anonymous),
}
}
#[must_use]
pub fn with_origins(mut self, origins: OriginPolicy) -> Self {
self.origins = Arc::new(origins);
self
}
#[must_use]
pub fn with_auth(
mut self,
policy: AuthPolicy,
authenticator: Arc<dyn Authenticator + Send + Sync>,
) -> Self {
self.policy = policy;
self.authenticator = authenticator;
self
}
#[must_use]
pub fn with_rate_limiter(mut self, limiter: RateLimiter) -> Self {
self.rate_limiter = limiter;
self
}
#[must_use]
pub fn with_stream_path(mut self, path: impl Into<String>) -> Self {
self.stream_path = path.into();
self
}
#[must_use]
pub fn with_message_path(mut self, path: impl Into<String>) -> Self {
self.message_path = path.into();
self
}
pub fn into_router(self) -> Result<AxumRouter, RouteError> {
let Self {
message_path: configured_message_path,
stream_path,
config,
sessions,
dispatcher,
origins,
policy,
authenticator,
rate_limiter,
} = self;
MessageEndpoint::bare_path(&stream_path).map_err(|reason| RouteError::Path {
which: "stream",
path: stream_path.clone(),
reason,
})?;
let message_path =
MessageEndpoint::bare_path(&configured_message_path).map_err(|reason| {
RouteError::Path {
which: "message",
path: configured_message_path.clone(),
reason,
}
})?;
let security = config.security().clone();
let state = Arc::new(LegacyState {
message_path,
config,
sessions,
dispatcher,
origins,
policy,
authenticator,
rate_limiter,
});
let router = AxumRouter::new()
.route(&stream_path, axum::routing::get(open_stream))
.route(
&configured_message_path,
axum::routing::post(receive_message),
)
.with_state(state);
Ok(with_security(router, &security)?)
}
}
async fn open_stream<D: LegacyDispatcher>(
State(state): State<Arc<LegacyState<D>>>,
headers: HeaderMap,
) -> Response {
if let Err(response) = check_common(&state, &headers, None).await {
return *response;
}
let tail: LegacyTail = EventTail::new(state.config.replay_capacity());
let taken = state.sessions.next_id();
let kept = state.dispatcher.open(taken, &tail);
let id = match state
.sessions
.open_with_id(Arc::new(LegacySession::new(tail.clone(), kept)), taken)
{
Ok(id) => id,
Err(error) => return refuse(Refusal::Handler(error.to_string())),
};
tail.push(LegacyFrame::Endpoint(state.message_endpoint(id)));
match tail.plan(None).into_response(state.config.keep_alive()) {
Ok(response) => response,
Err(error) => refuse(Refusal::Handler(error.to_string())),
}
}
async fn receive_message<D: LegacyDispatcher>(
State(state): State<Arc<LegacyState<D>>>,
headers: HeaderMap,
Query(query): Query<HashMap<String, String>>,
body: Body,
) -> Response {
let auth = match check_common(
&state,
&headers,
query.get(SESSION_PARAMETER).map(String::as_str),
)
.await
{
Ok(auth) => auth,
Err(response) => return *response,
};
let Some(raw) = query.get(SESSION_PARAMETER) else {
return refuse(Refusal::MissingSession);
};
let Ok(id) = McpSessionId::from_str(raw) else {
return refuse(Refusal::MalformedSessionId);
};
let session = match state.sessions.lookup(&id) {
Ok(Some(session)) => session,
Ok(None) => return refuse(Refusal::UnknownSession),
Err(error) => return refuse(Refusal::Handler(error.to_string())),
};
let Ok(limit) = usize::try_from(state.config.max_body().bytes()) else {
return refuse(Refusal::Handler(
"the configured body limit does not fit this platform".to_owned(),
));
};
let body = match read_body(body, limit).await {
Ok(body) => body,
Err(refusal) => return refuse(refusal),
};
let frame: Value = match serde_json::from_slice(&body) {
Ok(frame) => frame,
Err(error) => {
return refuse(Refusal::MalformedBody {
reason: error.to_string(),
});
}
};
match state
.dispatcher
.dispatch(&auth, &id, session.state(), &frame)
.await
{
Ok(replies) => {
for reply in replies {
session.tail().push(LegacyFrame::Message(reply));
}
(ACCEPTED, ()).into_response()
}
Err(refusal) => refuse(refusal),
}
}
async fn check_common<D: LegacyDispatcher>(
state: &LegacyState<D>,
headers: &HeaderMap,
subject: Option<&str>,
) -> Checked {
let origin = headers.get(header::ORIGIN).and_then(|v| v.to_str().ok());
if let Err(refusal) = state.origins.check(origin) {
return Err(Box::new(refuse(refusal)));
}
let host = headers.get(header::HOST).and_then(|v| v.to_str().ok());
if let Err(refusal) = state.origins.check_host(host) {
return Err(Box::new(refuse(refusal)));
}
if let Verdict::Limited { retry_after } = state
.rate_limiter
.check(subject.filter(|s| !s.is_empty()).unwrap_or("anonymous"))
{
return Err(Box::new(refuse(Refusal::RateLimited { retry_after })));
}
match authenticate(state.authenticator.as_ref(), &state.policy, headers).await {
Ok(auth) => Ok(auth),
Err(refusal) => Err(Box::new(refuse(refusal))),
}
}
fn refuse(refusal: Refusal) -> Response {
refusal.into_response()
}
type Checked = Result<AuthContext, Box<Response>>;
#[cfg(test)]
mod tests;