use std::str::FromStr;
use std::sync::Arc;
use axum::extract::ws::{WebSocket, WebSocketUpgrade};
use axum::extract::{DefaultBodyLimit, State};
use axum::response::IntoResponse;
use axum::routing::options;
use axum::{Extension, Router};
use axum_extra::TypedHeader;
use axum_extra::headers::Header;
use bytes::Bytes;
use http::HeaderMap;
use http::header::SEC_WEBSOCKET_PROTOCOL;
use surrealdb_core::dbs::Session;
use surrealdb_core::dbs::capabilities::RouteTarget;
use surrealdb_core::kvs::Datastore;
use surrealdb_core::mem::ALLOC;
use surrealdb_core::rpc::format::{Format, PROTOCOLS};
use surrealdb_core::rpc::{DbResponse, RpcProtocol};
use tokio::sync::RwLock;
use tower_http::limit::RequestBodyLimitLayer;
use tower_http::request_id::RequestId;
use uuid::Uuid;
use super::AppState;
use super::error::ResponseError;
use super::headers::{Accept, ContentType, SurrealId};
use crate::cnf;
use crate::cnf::HTTP_MAX_RPC_BODY_SIZE;
use crate::ntw::error::Error as NetError;
use crate::rpc::RpcState;
use crate::rpc::format::HttpFormat;
use crate::rpc::websocket::Websocket;
pub fn router() -> Router<Arc<RpcState>> {
Router::new()
.route("/rpc", options(|| async {}).get(get_handler).post(post_handler))
.route_layer(DefaultBodyLimit::disable())
.layer(RequestBodyLimitLayer::new(*HTTP_MAX_RPC_BODY_SIZE))
}
async fn get_handler(
ws: WebSocketUpgrade,
Extension(state): Extension<AppState>,
Extension(id): Extension<RequestId>,
Extension(mut session): Extension<Session>,
State(rpc_state): State<Arc<RpcState>>,
headers: HeaderMap,
) -> Result<impl IntoResponse, impl IntoResponse> {
let db = &state.datastore;
if !db.allows_http_route(&RouteTarget::Rpc) {
warn!("Capabilities denied HTTP route request attempt, target: '{}'", &RouteTarget::Rpc);
return Err(NetError::ForbiddenRoute(RouteTarget::Rpc.to_string()));
}
if headers.get(SEC_WEBSOCKET_PROTOCOL).is_none() {
warn!("A connection was made without a specified protocol.");
warn!(
"Automatic inference of the protocol format is deprecated in SurrealDB 2.0 and will be removed in SurrealDB 3.0."
);
warn!("Please upgrade any client to ensure that the connection format is specified.");
}
let id = match headers.get(SurrealId::name()) {
Some(id) => {
match id.to_str() {
Ok(id) => {
match Uuid::from_str(id) {
Ok(id) => id,
Err(_) => return Err(NetError::Request),
}
}
Err(_) => return Err(NetError::Request),
}
}
None => match id.header_value().is_empty() {
true => Uuid::new_v4(),
false => match id.header_value().to_str() {
Ok(id) => match Uuid::from_str(id) {
Ok(id) => id,
Err(_) => return Err(NetError::Request),
},
Err(_) => return Err(NetError::Request),
},
},
};
session.rt = true;
session.id = Some(id);
if rpc_state.web_sockets.read().await.contains_key(&id) {
return Err(NetError::Request);
}
Ok(ws
.protocols(PROTOCOLS)
.max_frame_size(*cnf::WEBSOCKET_MAX_MESSAGE_SIZE)
.max_message_size(*cnf::WEBSOCKET_MAX_MESSAGE_SIZE)
.read_buffer_size(*cnf::WEBSOCKET_READ_BUFFER_SIZE)
.write_buffer_size(*cnf::WEBSOCKET_WRITE_BUFFER_SIZE)
.max_write_buffer_size(*cnf::WEBSOCKET_MAX_WRITE_BUFFER_SIZE)
.on_failed_upgrade(|err| {
warn!("Failed to upgrade WebSocket connection: {err}");
})
.on_upgrade(move |socket| {
handle_socket(state.datastore.clone(), rpc_state, socket, session, id)
}))
}
async fn handle_socket(
datastore: Arc<Datastore>,
state: Arc<RpcState>,
ws: WebSocket,
session: Session,
id: Uuid,
) {
let format = match ws.protocol().and_then(|h| h.to_str().ok()) {
Some(protocol) => protocol.into(),
_ => Format::Json,
};
Websocket::serve(id, ws, format, session, datastore, state).await;
}
async fn post_handler(
Extension(state): Extension<AppState>,
Extension(session): Extension<Session>,
State(rpc_state): State<Arc<RpcState>>,
accept: Option<TypedHeader<Accept>>,
TypedHeader(content_type): TypedHeader<ContentType>,
body: Bytes,
) -> Result<impl IntoResponse, ResponseError> {
let db = &state.datastore;
if !db.allows_http_route(&RouteTarget::Rpc) {
warn!("Capabilities denied HTTP route request attempt, target: '{}'", &RouteTarget::Rpc);
return Err(NetError::ForbiddenRoute(RouteTarget::Rpc.to_string()).into());
}
let fmt: Format = (&content_type).into();
if matches!(fmt, Format::Unsupported) {
return Err(NetError::InvalidType.into());
}
let out: Option<Format> = accept.as_deref().map(Into::into);
if let Some(out) = out
&& fmt != out
{
return Err(NetError::InvalidType.into());
}
let rpc = &*rpc_state.http;
rpc.set_session(None, Arc::new(RwLock::new(session)));
if ALLOC.is_beyond_threshold() {
return Err(NetError::ServerOverloaded.into());
}
match fmt.req_http(body) {
Ok(req) => {
let res = RpcProtocol::execute(
rpc,
req.txn.map(Into::into),
req.session_id.map(Into::into),
req.method,
req.params,
)
.await;
Ok(fmt.res_http(match res {
Ok(result) => DbResponse::success(None, None, result),
Err(err) => DbResponse::failure(None, None, err),
})?)
}
Err(err) => Err(err.into()),
}
}