use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use axum::Router;
use axum::body::Body;
use axum::http::{Request, StatusCode, header};
use serde_json::{Value, json};
use tower::ServiceExt;
use super::{
DEFAULT_MESSAGE_PATH, DEFAULT_STREAM_PATH, LegacyRouter, RouteError, SESSION_PARAMETER,
};
use crate::adapters::primary::transport::http::limits::RateLimiter;
use crate::adapters::primary::transport::http::{
AuthPolicy, Authenticator, Credentials, Refusal, ServerConfig, SessionStore,
};
use crate::adapters::primary::transport::legacy_sse::{
LegacyDispatcher, LegacySession, LegacyTail, Origin, OriginPolicy,
};
use crate::application::session::IdSource;
use crate::domain::registry::auth::{AuthContext, ScopeSet};
use crate::domain::session::SessionId;
use crate::domain::{McpSessionId, RateLimit};
type TestError = Box<dyn std::error::Error>;
const SESSION_ONE: &str = "0000000000000001";
const UNISSUED: &str = "00000000000000ff";
fn fail(message: impl Into<String>) -> TestError {
message.into().into()
}
fn require<T: PartialEq + core::fmt::Debug>(
got: &T,
want: &T,
because: &str,
) -> Result<(), TestError> {
if got == want {
Ok(())
} else {
Err(fail(format!("{because}: got {got:?}, wanted {want:?}")))
}
}
fn require_str(got: &str, want: &str, because: &str) -> Result<(), TestError> {
if got == want {
Ok(())
} else {
Err(fail(format!("{because}:\n got {got:?}\n want {want:?}")))
}
}
struct Counting(AtomicU64);
impl IdSource for Counting {
fn next_session_id(&self) -> SessionId {
SessionId::new(self.0.fetch_add(1, Ordering::SeqCst) + 1)
}
}
#[derive(Debug, Default)]
struct Recorder {
calls: AtomicU64,
seen: std::sync::Mutex<Vec<String>>,
reply: std::sync::Mutex<Option<Result<Vec<Value>, String>>>,
}
impl Recorder {
fn calls(&self) -> u64 {
self.calls.load(Ordering::SeqCst)
}
fn seen(&self) -> Vec<String> {
self.seen.lock().map(|v| v.clone()).unwrap_or_default()
}
}
const TOKEN: &str = "Bearer good";
struct OneToken;
impl Authenticator for OneToken {
fn authenticate<'a>(
&'a self,
presented: &'a Credentials<'_>,
) -> Pin<Box<dyn Future<Output = AuthContext> + Send + 'a>> {
let context = if presented.authorization() == Some(TOKEN) {
AuthContext::new("holder", ScopeSet::all())
} else {
AuthContext::anonymous()
};
Box::pin(std::future::ready(context))
}
}
struct Echo(Arc<Recorder>);
impl LegacyDispatcher for Echo {
type Session = ();
fn open(&self, _id: SessionId, _tail: &LegacyTail) {}
fn dispatch(
&self,
auth: &AuthContext,
session: &McpSessionId,
_state: &(),
frame: &Value,
) -> impl core::future::Future<Output = Result<Vec<Value>, Refusal>> + Send {
core::future::ready({
self.0.calls.fetch_add(1, Ordering::SeqCst);
if let Ok(mut seen) = self.0.seen.lock() {
seen.push(format!("{session} {frame} {}", auth.principal()));
}
let scripted = self.0.reply.lock().ok().and_then(|r| r.clone());
match scripted {
Some(Ok(replies)) => Ok(replies),
Some(Err(message)) => Err(Refusal::Handler(message)),
None => Ok(vec![json!({"jsonrpc": "2.0", "id": 7, "result": {}})]),
}
})
}
}
struct Harness {
router: Router,
sessions: Arc<SessionStore<LegacySession<()>>>,
recorder: Arc<Recorder>,
}
impl Harness {
fn new() -> Result<Self, TestError> {
Self::build(OriginPolicy::strict(None), None, None)
}
fn with_origins() -> Result<Self, TestError> {
let policy = OriginPolicy::strict(Some(own_origin()?))
.allowing("https://app.example".parse::<Origin>()?);
Self::build(policy, None, None)
}
fn secured() -> Result<Self, TestError> {
let sessions = Arc::new(SessionStore::new(Arc::new(Counting(AtomicU64::new(0)))));
let recorder = Arc::new(Recorder::default());
let router = LegacyRouter::new(
ServerConfig::new()?,
Arc::clone(&sessions),
Echo(Arc::clone(&recorder)),
)
.with_origins(OriginPolicy::strict(None))
.with_auth(AuthPolicy::Require(ScopeSet::all()), Arc::new(OneToken))
.into_router()?;
Ok(Self {
router,
sessions,
recorder,
})
}
fn build(
origins: OriginPolicy,
stream_path: Option<&str>,
message_path: Option<&str>,
) -> Result<Self, TestError> {
let sessions = Arc::new(SessionStore::new(Arc::new(Counting(AtomicU64::new(0)))));
let recorder = Arc::new(Recorder::default());
let mut router = LegacyRouter::new(
ServerConfig::new()?,
Arc::clone(&sessions),
Echo(Arc::clone(&recorder)),
)
.with_origins(origins);
if let Some(path) = stream_path {
router = router.with_stream_path(path);
}
if let Some(path) = message_path {
router = router.with_message_path(path);
}
Ok(Self {
router: router.into_router()?,
sessions,
recorder,
})
}
fn open(&self) -> Result<McpSessionId, TestError> {
let capacity =
core::num::NonZeroUsize::new(8).ok_or("a capacity of zero is not a capacity")?;
Ok(self
.sessions
.open(Arc::new(LegacySession::new(LegacyTail::new(capacity), ())))?)
}
async fn send(&self, request: Request<Body>) -> Result<axum::response::Response, TestError> {
Ok(self.router.clone().oneshot(request).await?)
}
}
fn own_origin() -> Result<Origin, TestError> {
Ok("http://127.0.0.1:8931".parse::<Origin>()?)
}
fn get(path: &str) -> Result<Request<Body>, TestError> {
Ok(Request::builder()
.method("GET")
.uri(path)
.header(header::HOST, "127.0.0.1:8931")
.body(Body::empty())?)
}
fn post(path: &str, body: &str) -> Result<Request<Body>, TestError> {
Ok(Request::builder()
.method("POST")
.uri(path)
.header(header::HOST, "127.0.0.1:8931")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(body.to_owned()))?)
}
fn with_origin(request: Request<Body>, value: &str) -> Result<Request<Body>, TestError> {
let (mut parts, body) = request.into_parts();
parts.headers.insert(
header::ORIGIN,
value
.parse()
.map_err(|_| fail("a test wrote an Origin that is not a header value"))?,
);
Ok(Request::from_parts(parts, body))
}
fn message_uri(id: McpSessionId) -> String {
format!("{DEFAULT_MESSAGE_PATH}?{SESSION_PARAMETER}={id}")
}
async fn head(response: axum::response::Response) -> Result<String, TestError> {
use core::time::Duration;
use futures_util::StreamExt as _;
let mut stream = response.into_body().into_data_stream();
let mut collected = Vec::new();
while let Ok(Some(Ok(chunk))) =
tokio::time::timeout(Duration::from_millis(250), stream.next()).await
{
collected.extend_from_slice(&chunk);
}
Ok(String::from_utf8_lossy(&collected).into_owned())
}
#[tokio::test]
async fn the_first_frame_a_client_reads_is_the_endpoint_event() -> Result<(), TestError> {
let harness = Harness::new()?;
let response = harness.send(get(DEFAULT_STREAM_PATH)?).await?;
require(
&response.status(),
&StatusCode::OK,
"a GET on the stream path opens a stream",
)?;
require_str(
response
.headers()
.get(header::CONTENT_TYPE)
.ok_or("the stream response has no Content-Type")?
.to_str()?,
"text/event-stream",
"and the client is told it is an event stream",
)?;
let bytes = head(response).await?;
let first = bytes.split("\n\n").next().unwrap_or_default();
require_str(
first,
"event: endpoint\nid: 1\ndata: /messages?sessionId=0000000000000001",
"frame 1 is the endpoint event, naming the path and the session id this server just issued",
)
}
#[test]
fn the_session_parameter_is_named_as_the_transport_names_it() -> Result<(), TestError> {
require_str(
SESSION_PARAMETER,
"sessionId",
"the 2024-11-05 transport's own examples spell the parameter `sessionId`, and a client built from those examples sends that",
)
}
#[tokio::test]
async fn the_advertised_query_is_the_query_that_is_read() -> Result<(), TestError> {
let harness = Harness::new()?;
let response = harness.send(get(DEFAULT_STREAM_PATH)?).await?;
let bytes = head(response).await?;
let advertised = bytes
.lines()
.find_map(|line| line.strip_prefix("data: "))
.ok_or("the endpoint event carried no data line")?;
require(
&advertised.contains(&format!("?{SESSION_PARAMETER}=")),
&true,
"the URI this server advertises carries the parameter this server reads",
)?;
let id = harness.open()?;
let _ = id;
let response = harness
.send(
Request::builder()
.method("POST")
.uri(advertised.replace(
"sessionId=0000000000000001",
&format!("sessionId={SESSION_ONE}"),
))
.header(header::HOST, "127.0.0.1:8931")
.body(Body::from(r#"{"jsonrpc":"2.0","id":1}"#.to_owned()))?,
)
.await?;
require(
&response.status(),
&StatusCode::ACCEPTED,
"so a client that posts to exactly the URI it was given is not turned away by its own server",
)
}
#[tokio::test]
async fn a_message_never_precedes_the_endpoint_event() -> Result<(), TestError> {
let harness = Harness::new()?;
let id = harness.open()?;
let _ = harness
.send(post(
&message_uri(id),
r#"{"jsonrpc":"2.0","method":"ping"}"#,
)?)
.await?;
let response = harness.send(get(DEFAULT_STREAM_PATH)?).await?;
let bytes = head(response).await?;
let first = bytes.split("\n\n").next().unwrap_or_default();
require(
&first.starts_with("event: endpoint"),
&true,
"even after a message has been dispatched, the frame a client reads first on a new stream is still an endpoint event",
)
}
#[tokio::test]
async fn a_post_is_accepted_and_its_answer_goes_to_the_stream() -> Result<(), TestError> {
let harness = Harness::new()?;
let id = harness.open()?;
let response = harness
.send(post(
&message_uri(id),
r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#,
)?)
.await?;
require(
&response.status(),
&StatusCode::ACCEPTED,
"a well-formed POST is accepted",
)?;
require(
&harness.recorder.calls(),
&1,
"and it reached the dispatcher exactly once",
)?;
require(
&harness.recorder.seen(),
&vec![format!(
"{id} {{\"id\":1,\"jsonrpc\":\"2.0\",\"method\":\"ping\"}} anonymous"
)],
"carrying the session the query parameter named, not one the body could have chosen",
)
}
#[tokio::test]
async fn a_refusal_from_the_dispatcher_is_never_a_202() -> Result<(), TestError> {
let harness = Harness::new()?;
let id = harness.open()?;
if let Ok(mut reply) = harness.recorder.reply.lock() {
*reply = Some(Err("the tool is gone".to_owned()));
}
let response = harness
.send(post(&message_uri(id), r#"{"jsonrpc":"2.0","id":1}"#)?)
.await?;
require(
&(response.status() != StatusCode::ACCEPTED),
&true,
"a dispatcher that refuses does not produce a success status",
)
}
#[tokio::test]
async fn a_post_without_a_session_parameter_is_refused() -> Result<(), TestError> {
let harness = Harness::new()?;
let response = harness
.send(post(DEFAULT_MESSAGE_PATH, r#"{"jsonrpc":"2.0","id":1}"#)?)
.await?;
require(
&response.status(),
&StatusCode::BAD_REQUEST,
"this transport names the session in the query string and there is no header to fall back to, so no parameter is a `400`",
)?;
require(&harness.recorder.calls(), &0, "and nothing was dispatched")
}
#[tokio::test]
async fn a_post_naming_an_unissued_session_is_a_404() -> Result<(), TestError> {
let harness = Harness::new()?;
let uri = format!("{DEFAULT_MESSAGE_PATH}?{SESSION_PARAMETER}={UNISSUED}");
let response = harness
.send(post(&uri, r#"{"jsonrpc":"2.0","id":1}"#)?)
.await?;
require(
&response.status(),
&StatusCode::NOT_FOUND,
"an id this server never minted is a `404`, distinct from the `400` of an id that is not an id at all",
)
}
#[tokio::test]
async fn a_post_with_a_body_that_is_not_json_is_a_400() -> Result<(), TestError> {
let harness = Harness::new()?;
let id = harness.open()?;
let response = harness
.send(post(&message_uri(id), "not json at all")?)
.await?;
require(
&response.status(),
&StatusCode::BAD_REQUEST,
"the body is parsed at the boundary, so a client that sent prose is told so rather than reaching the dispatcher",
)
}
#[tokio::test]
async fn neither_route_accepts_the_other_s_method() -> Result<(), TestError> {
let harness = Harness::new()?;
require(
&harness.send(get(DEFAULT_MESSAGE_PATH)?).await?.status(),
&StatusCode::METHOD_NOT_ALLOWED,
"the message endpoint does not stream, so a GET on it is a `405`",
)?;
require(
&harness
.send(post(DEFAULT_STREAM_PATH, "{}")?)
.await?
.status(),
&StatusCode::METHOD_NOT_ALLOWED,
"and the stream endpoint does not receive, so a POST on it is a `405`",
)
}
#[tokio::test]
async fn a_foreign_origin_is_refused_on_both_routes() -> Result<(), TestError> {
let harness = Harness::with_origins()?;
let id = harness.open()?;
let stream = harness
.send(with_origin(
get(DEFAULT_STREAM_PATH)?,
"http://evil.example",
)?)
.await?;
require(
&stream.status(),
&StatusCode::FORBIDDEN,
"a GET from an origin this server does not serve is refused",
)?;
let posted = harness
.send(with_origin(
post(&message_uri(id), r#"{"jsonrpc":"2.0","id":1}"#)?,
"http://evil.example",
)?)
.await?;
require(
&posted.status(),
&StatusCode::FORBIDDEN,
"and so is a POST, which is the one that would have changed state",
)?;
require(
&harness.recorder.calls(),
&0,
"so nothing was dispatched on the strength of a hostile page's request",
)
}
#[tokio::test]
async fn a_permitted_origin_is_answered() -> Result<(), TestError> {
let harness = Harness::with_origins()?;
for allowed in ["http://127.0.0.1:8931", "https://app.example"] {
let response = harness
.send(with_origin(get(DEFAULT_STREAM_PATH)?, allowed)?)
.await?;
require(
&response.status(),
&StatusCode::OK,
"every origin on the allowlist is answered, not only the first",
)?;
}
Ok(())
}
#[tokio::test]
async fn a_request_with_no_origin_is_answered() -> Result<(), TestError> {
let harness = Harness::new()?;
require(
&harness.send(get(DEFAULT_STREAM_PATH)?).await?.status(),
&StatusCode::OK,
"no `Origin` at all is a non-browser client, and refusing it would refuse every CLI in existence",
)
}
#[test]
fn a_path_that_is_not_an_absolute_path_is_refused_at_assembly() -> Result<(), TestError> {
for (stream_path, message_path) in [
(Some("https://elsewhere.example/steal"), None),
(Some("//elsewhere.example/steal"), None),
(Some("relative/path"), None),
(None, Some("https://elsewhere.example/steal")),
(None, Some("//elsewhere.example/steal")),
(None, Some("relative/path")),
(None, Some("/messages?tenant=a")),
(None, Some("/messages#a")),
(None, Some("/messages#a?b=c")),
(Some("/sse?x=1"), None),
(Some("/sse#y"), None),
(Some("/sse"), Some("relative/path")),
] {
let Err(refused) = Harness::build(OriginPolicy::strict(None), stream_path, message_path)
else {
return Err(fail(format!(
"the paths {stream_path:?} and {message_path:?} were accepted"
)));
};
require(
&refused.downcast_ref::<RouteError>().is_some(),
&true,
"a path that is not on this server's own origin is refused by name, not mounted and not advertised",
)?;
}
Ok(())
}
#[tokio::test]
async fn an_unadvertisable_message_path_does_not_leak_the_session_it_opened()
-> Result<(), TestError> {
let harness = Harness::build(OriginPolicy::strict(None), None, Some("/ok"))?;
require(
&harness.sessions.len()?,
&0,
"the fixture starts with no sessions, so a leak below is this test's",
)
}
#[tokio::test]
async fn every_response_carries_the_security_headers() -> Result<(), TestError> {
let harness = Harness::new()?;
let cases = [
get(DEFAULT_STREAM_PATH)?,
post(DEFAULT_MESSAGE_PATH, "{}")?,
get(DEFAULT_MESSAGE_PATH)?,
];
for request in cases {
let response = harness.send(request).await?;
for name in [
"content-security-policy",
"x-frame-options",
"strict-transport-security",
] {
require(
&response.headers().contains_key(name),
&true,
"the header is present on whatever the route answered, including a refusal",
)?;
}
}
Ok(())
}
#[tokio::test]
async fn a_request_without_a_credential_is_refused_on_both_routes() -> Result<(), TestError> {
let harness = Harness::secured()?;
let id = harness.open()?;
require(
&harness.send(get(DEFAULT_STREAM_PATH)?).await?.status(),
&StatusCode::UNAUTHORIZED,
"a GET with no bearer token opens no stream",
)?;
require(
&harness
.send(post(&message_uri(id), r#"{"jsonrpc":"2.0","id":1}"#)?)
.await?
.status(),
&StatusCode::UNAUTHORIZED,
"and a POST with no bearer token is refused before the dispatcher is reached",
)?;
require(
&harness.recorder.calls(),
&0,
"so no request was dispatched on an unauthenticated connection",
)
}
#[tokio::test]
async fn a_request_carrying_the_credential_is_answered() -> Result<(), TestError> {
let harness = Harness::secured()?;
let id = harness.open()?;
let with_token =
|mut request: axum::http::Request<Body>| -> Result<axum::http::Request<Body>, TestError> {
request.headers_mut().insert(
header::AUTHORIZATION,
header::HeaderValue::from_static(TOKEN),
);
Ok(request)
};
require(
&harness
.send(with_token(get(DEFAULT_STREAM_PATH)?)?)
.await?
.status(),
&StatusCode::OK,
"the stream route answers a request carrying the token",
)?;
require(
&harness
.send(with_token(post(
&message_uri(id),
r#"{"jsonrpc":"2.0","id":1}"#,
)?)?)
.await?
.status(),
&StatusCode::ACCEPTED,
"and so does the message route, so the gate is on both and not only the one that is easy to remember",
)
}
#[tokio::test]
async fn the_authenticated_principal_reaches_the_dispatcher() -> Result<(), TestError> {
let harness = Harness::secured()?;
let id = harness.open()?;
let mut request = post(
&message_uri(id),
r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#,
)?;
request.headers_mut().insert(
header::AUTHORIZATION,
header::HeaderValue::from_static(TOKEN),
);
require(
&harness.send(request).await?.status(),
&StatusCode::ACCEPTED,
"a POST carrying the token is accepted",
)?;
let seen = harness.recorder.seen();
require(
&seen.len(),
&1,
"and it reached the dispatcher exactly once",
)?;
let reached = seen.first().ok_or("the dispatcher was reached")?;
require_str(
reached,
&format!("{id} {{\"id\":1,\"jsonrpc\":\"2.0\",\"method\":\"ping\"}} holder"),
"the dispatcher was reached as the principal the token authenticates to",
)
}
#[tokio::test]
async fn a_foreign_host_is_refused_on_both_routes() -> Result<(), TestError> {
let harness = Harness::new()?;
let id = harness.open()?;
for request in [get("/sse")?, post(&message_uri(id), "{}")?] {
let (mut parts, body) = request.into_parts();
parts.headers.insert(header::HOST, "evil.example".parse()?);
let response = harness.send(Request::from_parts(parts, body)).await?;
require(
&response.status(),
&StatusCode::FORBIDDEN,
"a request addressed to a host this server does not serve is refused",
)?;
}
Ok(())
}
#[tokio::test]
async fn a_request_with_no_host_is_refused() -> Result<(), TestError> {
let harness = Harness::new()?;
let request = Request::builder()
.method("GET")
.uri("/sse")
.body(Body::empty())?;
let response = harness.send(request).await?;
require(
&response.status(),
&StatusCode::FORBIDDEN,
"no Host header is no evidence either way, and is refused",
)?;
Ok(())
}
#[tokio::test]
async fn requests_beyond_the_allowance_are_answered_429() -> Result<(), TestError> {
let sessions = Arc::new(SessionStore::new(Arc::new(Counting(AtomicU64::new(0)))));
let limiter = RateLimiter::new(RateLimit::new(
core::num::NonZeroU32::new(2).ok_or("a zero limit")?,
core::time::Duration::from_secs(3600),
));
let router = LegacyRouter::new(
ServerConfig::new()?,
Arc::clone(&sessions),
Echo(Arc::new(Recorder::default())),
)
.with_rate_limiter(limiter)
.into_router()?;
let mut statuses = Vec::new();
for _ in 0..3 {
let response = router.clone().oneshot(get("/sse")?).await?;
statuses.push(response.status());
}
require(
&statuses,
&vec![
StatusCode::OK,
StatusCode::OK,
StatusCode::TOO_MANY_REQUESTS,
],
"the third request inside the window is limited",
)?;
Ok(())
}