pub mod api;
mod auth;
pub mod client_ip;
pub mod error;
pub mod export;
#[cfg(feature = "graphql")]
pub mod gql;
pub(crate) mod headers;
pub mod health;
pub mod import;
mod input;
pub mod key;
#[cfg(feature = "mcp")]
pub mod mcp;
pub mod ml;
pub(crate) mod output;
mod params;
pub mod rpc;
mod signals;
pub mod signin;
pub mod signup;
pub mod sql;
pub mod sync;
mod tracer;
pub mod version;
use std::io;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use anyhow::Result;
use axum::response::Redirect;
use axum::routing::get;
use axum::{Extension, Router, middleware};
use axum_server::Handle;
use axum_server::tls_rustls::RustlsConfig;
use http::header;
use surrealdb::headers::{AUTH_DB, AUTH_NS, DB, ID, NS};
use surrealdb_core::CommunityComposer;
use surrealdb_core::channel::Receiver;
use surrealdb_core::kvs::{Datastore, TransactionBuilderFactory};
use surrealdb_types::Notification;
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use tower::ServiceBuilder;
use tower_http::ServiceBuilderExt;
use tower_http::add_extension::AddExtensionLayer;
use tower_http::compression::CompressionLayer;
use tower_http::compression::predicate::{NotForContentType, Predicate, SizeAbove};
use tower_http::cors::{AllowOrigin, Any, CorsLayer};
use tower_http::request_id::MakeRequestUuid;
use tower_http::sensitive_headers::{
SetSensitiveRequestHeadersLayer, SetSensitiveResponseHeadersLayer,
};
use tower_http::trace::TraceLayer;
use crate::cli::Config;
use crate::cnf;
use crate::ntw::signals::graceful_shutdown;
use crate::observe::{HttpMetricsLayer, MetricsState};
use crate::rpc::{RpcState, notifications};
const LOG: &str = "surrealdb::net";
pub trait RouterFactory: TransactionBuilderFactory {
fn configure_router(router_state: Self::RouterState) -> Router<Arc<RpcState>>;
}
impl RouterFactory for CommunityComposer {
fn configure_router(_router_state: Self::RouterState) -> Router<Arc<RpcState>> {
let router = Router::<Arc<RpcState>>::new()
.route("/", get(|| async { Redirect::temporary(cnf::APP_ENDPOINT) }))
.route("/status", get(|| async {}))
.merge(health::router())
.merge(export::router())
.merge(import::router())
.merge(rpc::router())
.merge(version::router())
.merge(sync::router())
.merge(sql::router())
.merge(signin::router())
.merge(signup::router())
.merge(key::router())
.merge(ml::router())
.merge(api::router());
#[cfg(feature = "graphql")]
let router = router.merge(gql::router());
#[cfg(feature = "mcp")]
let router = router.merge(mcp::router());
router
}
}
#[derive(Clone)]
pub struct AppState {
pub client_ip: client_ip::ClientIp,
pub datastore: Arc<Datastore>,
pub metrics_observer: Option<Arc<crate::observe::metrics::MetricsObserver>>,
}
#[derive(Clone, Debug)]
pub struct RouterOptions {
pub client_ip: client_ip::ClientIp,
pub no_identification_headers: bool,
pub allow_origin: Vec<String>,
}
impl Default for RouterOptions {
fn default() -> Self {
Self {
client_ip: client_ip::ClientIp::Socket,
no_identification_headers: false,
allow_origin: Vec::new(),
}
}
}
impl From<&Config> for RouterOptions {
fn from(cfg: &Config) -> Self {
Self {
client_ip: cfg.client_ip,
no_identification_headers: cfg.no_identification_headers,
allow_origin: cfg.allow_origin.clone(),
}
}
}
pub struct SurrealRouter {
router: Router,
rpc_state: Arc<RpcState>,
datastore: Arc<Datastore>,
notifications: Receiver<Notification>,
canceller: CancellationToken,
}
impl SurrealRouter {
pub async fn build<F: RouterFactory>(
opt: impl Into<RouterOptions>,
ds: Arc<Datastore>,
notifications: Receiver<Notification>,
ct: CancellationToken,
router_state: F::RouterState,
) -> Result<Self> {
Self::build_with_metrics::<F>(opt, ds, notifications, ct, router_state, None).await
}
pub async fn build_with_metrics<F: RouterFactory>(
opt: impl Into<RouterOptions>,
ds: Arc<Datastore>,
notifications: Receiver<Notification>,
ct: CancellationToken,
router_state: F::RouterState,
metrics: Option<MetricsState>,
) -> Result<Self> {
let opt = opt.into();
let app_state = AppState {
client_ip: opt.client_ip,
datastore: Arc::clone(&ds),
metrics_observer: metrics.as_ref().map(|m| Arc::clone(&m.observer)),
};
let headers: Arc<[_]> = Arc::new([
header::AUTHORIZATION,
header::PROXY_AUTHORIZATION,
header::COOKIE,
header::SET_COOKIE,
]);
let service = ServiceBuilder::new()
.catch_panic()
.set_x_request_id(MakeRequestUuid)
.propagate_x_request_id()
.concurrency_limit(*cnf::NET_MAX_CONCURRENT_REQUESTS);
let service = service.layer(
CompressionLayer::new().compress_when(
SizeAbove::new(512)
.and(NotForContentType::GRPC)
.and(NotForContentType::IMAGES),
),
);
let allow_origin: AllowOrigin = if opt.allow_origin.is_empty() {
Any.into()
} else {
let origins: Vec<http::HeaderValue> = opt
.allow_origin
.iter()
.map(|o| o.parse().map_err(|_| anyhow::anyhow!("invalid CORS origin: {o}")))
.collect::<Result<Vec<_>, _>>()?;
AllowOrigin::list(origins)
};
let allow_header = vec![
header::ACCEPT,
header::ACCEPT_ENCODING,
header::AUTHORIZATION,
header::CONTENT_TYPE,
header::ORIGIN,
NS.clone(),
DB.clone(),
ID.clone(),
AUTH_NS.clone(),
AUTH_DB.clone(),
];
#[cfg(feature = "mcp")]
let (allow_header, mcp_expose_headers) = {
let mut allow_header = allow_header;
allow_header.push(http::HeaderName::from_static("mcp-session-id"));
allow_header.push(http::HeaderName::from_static("mcp-protocol-version"));
allow_header.push(http::HeaderName::from_static("last-event-id"));
allow_header.push(http::HeaderName::from_static("x-custom-auth-headers"));
(
allow_header,
vec![
http::HeaderName::from_static("mcp-session-id"),
http::HeaderName::from_static("mcp-protocol-version"),
],
)
};
let prometheus_observer = metrics.as_ref().map(|m| Arc::clone(&m.observer));
let events_observer = Some(Arc::clone(ds.observer()));
let service = service
.layer(AddExtensionLayer::new(app_state))
.layer(middleware::from_fn(client_ip::client_ip_middleware))
.layer(SetSensitiveRequestHeadersLayer::from_shared(Arc::clone(&headers)))
.layer(
TraceLayer::new_for_http()
.make_span_with(tracer::HttpTraceLayerHooks)
.on_request(tracer::HttpTraceLayerHooks)
.on_response(tracer::HttpTraceLayerHooks)
.on_failure(tracer::HttpTraceLayerHooks),
)
.layer(HttpMetricsLayer::new(events_observer))
.layer(SetSensitiveResponseHeadersLayer::from_shared(headers))
.layer(auth::SurrealAuthLayer)
.layer(headers::add_server_header(!opt.no_identification_headers)?)
.layer(headers::add_version_header(!opt.no_identification_headers)?)
.layer({
let cors = CorsLayer::new()
.allow_methods([
http::Method::GET,
http::Method::PUT,
http::Method::POST,
http::Method::PATCH,
http::Method::DELETE,
http::Method::OPTIONS,
])
.allow_headers(allow_header)
.allow_origin(allow_origin)
.max_age(Duration::from_secs(86400));
#[cfg(feature = "mcp")]
let cors = cors.expose_headers(mcp_expose_headers);
cors
});
let axum_app = F::configure_router(router_state);
let axum_app = if let Some(ms) = metrics {
axum_app.merge(crate::observe::router::router()).layer(Extension(ms))
} else {
axum_app
};
let axum_app = axum_app.layer(service);
let rpc_state = Arc::new(RpcState::new_with_metrics(Arc::clone(&ds), prometheus_observer));
let axum_app = axum_app.with_state(Arc::clone(&rpc_state));
Ok(Self {
router: axum_app,
rpc_state,
datastore: ds,
notifications,
canceller: ct,
})
}
pub fn into_router(self) -> Router {
self.router
}
pub fn router(&self) -> &Router {
&self.router
}
pub fn rpc_state(&self) -> &Arc<RpcState> {
&self.rpc_state
}
pub fn datastore(&self) -> &Arc<Datastore> {
&self.datastore
}
pub fn canceller(&self) -> &CancellationToken {
&self.canceller
}
pub async fn shutdown(&self) {
self.datastore.shutdown().await.ok();
}
pub fn spawn_notifications(&self) -> JoinHandle<()> {
let notify = self.notifications.clone();
let state = Arc::clone(&self.rpc_state);
let ct = self.canceller.clone();
tokio::spawn(async move { notifications(notify, state, ct).await })
}
}
pub async fn init<F: RouterFactory>(
opt: &Config,
ds: Arc<Datastore>,
recv: Receiver<Notification>,
ct: CancellationToken,
router_state: F::RouterState,
) -> Result<()> {
init_with_metrics::<F>(opt, ds, recv, ct, router_state, None).await
}
pub async fn init_with_metrics<F: RouterFactory>(
opt: &Config,
ds: Arc<Datastore>,
recv: Receiver<Notification>,
ct: CancellationToken,
router_state: F::RouterState,
metrics: Option<MetricsState>,
) -> Result<()> {
let surreal =
SurrealRouter::build_with_metrics::<F>(opt, ds, recv, ct, router_state, metrics).await?;
let handle = Handle::new();
let shutdown_handler = graceful_shutdown(
Arc::clone(surreal.rpc_state()),
surreal.canceller().clone(),
handle.clone(),
);
surreal.spawn_notifications();
let axum_app = surreal.into_router();
let res = if let (Some(cert), Some(key)) = (&opt.crt, &opt.key) {
let tls = RustlsConfig::from_pem_file(cert, key).await?;
let server = axum_server::bind_rustls(opt.bind, tls);
info!(target: LOG, "Started web server on {}", &opt.bind);
server
.handle(handle)
.serve(axum_app.into_make_service_with_connect_info::<SocketAddr>())
.await
} else {
let server = axum_server::bind(opt.bind);
info!(target: LOG, "Started web server on {}", &opt.bind);
server
.handle(handle)
.serve(axum_app.into_make_service_with_connect_info::<SocketAddr>())
.await
};
if let Err(e) = res {
if opt.bind.port() < 1024
&& let io::ErrorKind::PermissionDenied = e.kind()
{
error!(target: LOG, "Binding to ports below 1024 requires privileged access or special permissions.");
}
return Err(e.into());
}
let _ = shutdown_handler.await;
info!(target: LOG, "Web server stopped. Bye!");
Ok(())
}