#[cfg(feature = "websocket")]
use axum::{
extract::ws::{WebSocket, WebSocketUpgrade},
http::StatusCode,
response::{IntoResponse, Response},
Router,
};
#[cfg(all(feature = "websocket", feature = "security"))]
use axum::http::header::AUTHORIZATION;
#[cfg(feature = "websocket")]
use futures_util::SinkExt;
#[cfg(feature = "websocket")]
use futures_util::StreamExt;
#[cfg(feature = "websocket")]
use std::pin::Pin;
#[cfg(feature = "websocket")]
use std::sync::Arc;
#[cfg(feature = "websocket")]
use crate::websocket::connection::{AppState, ConnectionManager};
#[cfg(feature = "websocket")]
use crate::websocket::message::{parse_websocket_message, WebSocketMessage, MAX_MESSAGE_SIZE};
#[cfg(feature = "websocket")]
use crate::core::ApiMetadata;
#[cfg(feature = "websocket")]
use crate::define_registration;
#[cfg(feature = "websocket")]
pub type BoxFuture<'a, T> = Pin<Box<dyn std::future::Future<Output = T> + Send + 'a>>;
#[cfg(feature = "websocket")]
pub trait WebSocketHandler: Send + Sync {
fn handle(&self, message: WebSocketMessage) -> BoxFuture<'static, WebSocketMessage>;
}
#[cfg(feature = "websocket")]
define_registration!(WebSocketRoute, Arc<dyn WebSocketHandler>, ApiMetadata);
#[cfg(feature = "websocket")]
pub struct DefaultWebSocketHandler;
#[cfg(feature = "websocket")]
impl WebSocketHandler for DefaultWebSocketHandler {
fn handle(&self, message: WebSocketMessage) -> BoxFuture<'static, WebSocketMessage> {
Box::pin(async move {
match message {
WebSocketMessage::Request { id, method, .. } => WebSocketMessage::Response {
id,
result: serde_json::json!({"status": "ok", "method": method}),
},
_ => message,
}
})
}
}
#[cfg(feature = "websocket")]
pub struct ValidatedWebSocketUpgrade {
ws: WebSocketUpgrade,
manager: Arc<ConnectionManager>,
}
#[cfg(feature = "websocket")]
impl IntoResponse for ValidatedWebSocketUpgrade {
fn into_response(self) -> Response {
self.ws
.on_upgrade(move |socket| handle_socket(socket, self.manager.clone()))
}
}
#[cfg(feature = "websocket")]
impl<S> axum::extract::FromRequest<S> for ValidatedWebSocketUpgrade
where
S: Clone + Send + Sync + 'static,
{
type Rejection = StatusCode;
async fn from_request(req: axum::extract::Request, state: &S) -> Result<Self, Self::Rejection> {
let req = req;
#[cfg(feature = "security")]
let bearer_token: Option<String> = req
.headers()
.get(AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.and_then(|h| h.strip_prefix("Bearer "))
.map(String::from);
let app_state = req.extensions().get::<Arc<AppState>>().cloned();
#[cfg(feature = "security")]
if let Some(ref state_ref) = app_state {
if let Some(ref auth) = state_ref.config.auth {
let token = bearer_token.ok_or(StatusCode::UNAUTHORIZED)?;
auth.validate_token(&token)
.ok_or(StatusCode::UNAUTHORIZED)?;
}
}
let ws = axum::extract::ws::WebSocketUpgrade::from_request(req, state)
.await
.map_err(|_| StatusCode::BAD_REQUEST)?;
let manager = app_state
.map(|s| s.manager.clone())
.unwrap_or_else(|| Arc::new(ConnectionManager::new()));
Ok(Self { ws, manager })
}
}
#[cfg(feature = "websocket")]
pub async fn websocket_upgrade(ws: ValidatedWebSocketUpgrade) -> impl IntoResponse {
ws }
#[cfg(feature = "websocket")]
async fn handle_socket(mut socket: WebSocket, manager: Arc<ConnectionManager>) {
let conn_id = uuid::Uuid::new_v4().to_string();
let (conn, _receiver) = WebSocketConnection::new(conn_id.clone());
manager.add_connection(conn_id.clone(), conn).await;
while let Some(result) = socket.next().await {
match result {
Ok(msg) => {
if let Ok(text) = msg.to_text() {
if text.len() > MAX_MESSAGE_SIZE {
let _ = socket.close().await;
return;
}
match parse_websocket_message(text) {
Ok(ws_msg) => {
let handler = DefaultWebSocketHandler;
let response = handler.handle(ws_msg).await;
let response_json = match serde_json::to_string(&response) {
Ok(json) => json,
Err(_) => {
let error_response = WebSocketMessage::Error {
id: String::new(),
error: "Internal serialization error".to_string(),
};
if let Ok(json) = serde_json::to_string(&error_response) {
json
} else {
r#"{"type":"error","id":"","error":"Internal error"}"#
.to_string()
}
}
};
let _ = socket
.send(axum::extract::ws::Message::Text(response_json.into()))
.await;
}
Err(e) => {
let error_msg = WebSocketMessage::Error {
id: String::new(),
error: e,
};
let response_json = match serde_json::to_string(&error_msg) {
Ok(json) => json,
Err(_) => {
r#"{"type":"error","id":"","error":"Internal error processing request"}"#
.to_string()
}
};
let _ = socket
.send(axum::extract::ws::Message::Text(response_json.into()))
.await;
}
}
}
}
Err(_) => {
break;
}
}
}
manager.remove_connection(&conn_id).await;
}
#[cfg(feature = "websocket")]
use crate::websocket::connection::WebSocketConnection;
#[cfg(feature = "websocket")]
pub fn build() -> Router {
let mut router = Router::new();
let manager = Arc::new(ConnectionManager::new());
let state = Arc::new(AppState::new(manager));
for route in inventory::iter::<WebSocketRoute> {
let path = format!("/{}", route.name);
router = router.route(
&path,
axum::routing::get(websocket_upgrade).with_state(state.clone()),
);
}
router
}
#[cfg(feature = "websocket")]
#[cfg(test)]
mod tests {
use super::*;
use futures_util::FutureExt;
use std::panic::AssertUnwindSafe;
#[tokio::test]
async fn handle_socket_closes_connection_on_oversized_message() {
let app = Router::new().route("/ws", axum::routing::get(websocket_upgrade));
let server = axum_test::TestServer::builder().http_transport().build(app);
let mut ws = server.get_websocket("/ws").await.into_websocket().await;
let oversized = "x".repeat(MAX_MESSAGE_SIZE + 1);
ws.send_text(&oversized).await;
tokio::time::sleep(tokio::time::Duration::from_millis(300)).await;
let receive_result = tokio::time::timeout(
tokio::time::Duration::from_millis(500),
AssertUnwindSafe(ws.receive_text()).catch_unwind(),
)
.await;
match receive_result {
Err(_) => { }
Ok(Err(_)) => { }
Ok(Ok(text)) => {
let parse_result: Result<WebSocketMessage, _> = serde_json::from_str(&text);
assert!(
parse_result.is_err(),
"Should not receive a valid WebSocketMessage, got: {}",
text
);
}
}
}
#[tokio::test]
async fn handle_socket_ignores_binary_messages() {
let app = Router::new().route("/ws", axum::routing::get(websocket_upgrade));
let server = axum_test::TestServer::builder().http_transport().build(app);
let mut ws = server.get_websocket("/ws").await.into_websocket().await;
ws.send_message(axum_test::WsMessage::Binary(vec![0xFF, 0xFE, 0xFD].into()))
.await;
let request = WebSocketMessage::Request {
id: "after-binary".to_string(),
method: "ping".to_string(),
params: serde_json::json!({}),
};
ws.send_json(&request).await;
let response: WebSocketMessage = ws.receive_json().await;
match response {
WebSocketMessage::Response { id, result } => {
assert_eq!(id, "after-binary");
assert_eq!(result["status"], "ok");
}
_ => panic!("Expected Response, got {:?}", response),
}
}
}