1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-07-31 09:24:31 +03:00

Refactor listeners building

This commit is contained in:
Quentin Gliech
2022-10-05 13:19:02 +02:00
parent 014a8366ed
commit c548417752
10 changed files with 245 additions and 157 deletions

2
Cargo.lock generated
View File

@ -2419,6 +2419,7 @@ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
"atty", "atty",
"axum 0.6.0-rc.2",
"clap", "clap",
"dotenv", "dotenv",
"futures-util", "futures-util",
@ -2467,7 +2468,6 @@ dependencies = [
"figment", "figment",
"indoc", "indoc",
"lettre", "lettre",
"listenfd",
"mas-email", "mas-email",
"mas-iana", "mas-iana",
"mas-jose", "mas-jose",

View File

@ -6,6 +6,7 @@ edition = "2021"
license = "Apache-2.0" license = "Apache-2.0"
[dependencies] [dependencies]
axum = "0.6.0-rc.2"
tokio = { version = "1.21.2", features = ["full"] } tokio = { version = "1.21.2", features = ["full"] }
futures-util = "0.3.24" futures-util = "0.3.24"
anyhow = "1.0.65" anyhow = "1.0.65"

View File

@ -17,7 +17,7 @@ use std::{sync::Arc, time::Duration};
use anyhow::Context; use anyhow::Context;
use clap::Parser; use clap::Parser;
use futures_util::{ use futures_util::{
future::{FutureExt, OptionFuture}, future::FutureExt,
stream::{StreamExt, TryStreamExt}, stream::{StreamExt, TryStreamExt},
}; };
use hyper::Server; use hyper::Server;
@ -25,9 +25,9 @@ use mas_config::RootConfig;
use mas_email::Mailer; use mas_email::Mailer;
use mas_handlers::{AppState, MatrixHomeserver}; use mas_handlers::{AppState, MatrixHomeserver};
use mas_http::ServerLayer; use mas_http::ServerLayer;
use mas_listener::{maybe_tls::MaybeTlsAcceptor, unix_or_tcp::UnixOrTcpListener}; use mas_listener::maybe_tls::MaybeTlsAcceptor;
use mas_policy::PolicyFactory; use mas_policy::PolicyFactory;
use mas_router::{Route, UrlBuilder}; use mas_router::UrlBuilder;
use mas_storage::MIGRATOR; use mas_storage::MIGRATOR;
use mas_tasks::TaskQueue; use mas_tasks::TaskQueue;
use mas_templates::Templates; use mas_templates::Templates;
@ -213,8 +213,6 @@ impl Options {
&config.email.reply_to, &config.email.reply_to,
); );
let static_files = mas_static_files::service(&config.http.web_root);
let homeserver = MatrixHomeserver::new(config.matrix.homeserver.clone()); let homeserver = MatrixHomeserver::new(config.matrix.homeserver.clone());
let listeners_config = config.http.listeners.clone(); let listeners_config = config.http.listeners.clone();
@ -247,19 +245,11 @@ impl Options {
let signal = shutdown_signal().shared(); let signal = shutdown_signal().shared();
let shutdown_signal = signal.clone(); let shutdown_signal = signal.clone();
let mut fd_manager = listenfd::ListenFd::from_env(); let mut fd_manager = listenfd::ListenFd::from_env();
let listeners = listeners_config.into_iter().map(|listener_config| { let listeners = listeners_config.into_iter().map(|listener_config| {
// We have to borrow it here, not in the nested closure
let fd_manager = &mut fd_manager;
// Let's first grab all the listeners in a synchronous manner // Let's first grab all the listeners in a synchronous manner
// This helps with the fd_manager mutable borrow let listeners = crate::server::build_listeners(&mut fd_manager, &listener_config.binds);
let listeners: Result<Vec<UnixOrTcpListener>, _> = listener_config
.binds
.iter()
.map(move |bind_config| bind_config.listener(fd_manager))
.collect();
Ok((listener_config, listeners?)) Ok((listener_config, listeners?))
}); });
@ -269,10 +259,8 @@ impl Options {
.try_for_each_concurrent(None, move |(config, listeners)| { .try_for_each_concurrent(None, move |(config, listeners)| {
let signal = signal.clone(); let signal = signal.clone();
let mut router = mas_handlers::empty_router(state.clone());
let is_tls = config.tls.is_some(); let is_tls = config.tls.is_some();
let adresses: Vec<String> = listeners.iter().map(|listener| { let addresses: Vec<String> = listeners.iter().map(|listener| {
let addr = listener.local_addr(); let addr = listener.local_addr();
let proto = if is_tls { "https" } else { "http" }; let proto = if is_tls { "https" } else { "http" };
if let Ok(addr) = addr { if let Ok(addr) = addr {
@ -283,53 +271,15 @@ impl Options {
} }
}).collect(); }).collect();
info!("Listening on {adresses:?} with resources {resources:?}", resources = &config.resources); info!("Listening on {addresses:?} with resources {resources:?}", resources = &config.resources);
for resource in &config.resources { let router = crate::server::build_router(&state, &config.resources).layer(ServerLayer::new(config.name.clone()));
router = match resource {
mas_config::HttpResource::Health => {
router.merge(mas_handlers::healthcheck_router(state.clone()))
}
mas_config::HttpResource::Prometheus => {
router.route_service("/metrics", crate::telemetry::prometheus_service())
}
mas_config::HttpResource::Discovery => {
router.merge(mas_handlers::discovery_router(state.clone()))
}
mas_config::HttpResource::Human => {
router.merge(mas_handlers::human_router(state.clone()))
}
mas_config::HttpResource::Static => {
router.nest(mas_router::StaticAsset::route(), static_files.clone())
}
mas_config::HttpResource::OAuth => {
router.merge(mas_handlers::api_router(state.clone()))
}
mas_config::HttpResource::Compat => {
router.merge(mas_handlers::compat_router(state.clone()))
}
}
}
let router = router.layer(ServerLayer::default());
async move { async move {
let tls_config: OptionFuture<_> = config let tls_config = if let Some(tls_config) = config.tls.as_ref() {
.tls let tls_config = crate::server::build_tls_server_config(tls_config).await?;
.map(|tls_config| async move { Some(Arc::new(tls_config))
let (key, chain) = tls_config.load().await?; } else { None };
let key = rustls::PrivateKey(key);
let chain = chain.into_iter().map(rustls::Certificate).collect();
let mut config = rustls::ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_single_cert(chain, key)
.context("failed to build TLS server config")?;
config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
anyhow::Ok(Arc::new(config))
})
.into();
let tls_config = tls_config.await.transpose()?;
futures_util::stream::iter(listeners) futures_util::stream::iter(listeners)
.map(Ok) .map(Ok)

View File

@ -28,6 +28,7 @@ use tracing_subscriber::{
}; };
mod commands; mod commands;
mod server;
mod telemetry; mod telemetry;
#[tokio::main] #[tokio::main]

153
crates/cli/src/server.rs Normal file
View File

@ -0,0 +1,153 @@
// Copyright 2022 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, TcpListener, ToSocketAddrs},
os::unix::net::UnixListener,
sync::Arc,
};
use anyhow::Context;
use axum::{body::HttpBody, Router};
use listenfd::ListenFd;
use mas_config::{HttpBindConfig, HttpResource, HttpTlsConfig, UnixOrTcp};
use mas_handlers::AppState;
use mas_listener::unix_or_tcp::UnixOrTcpListener;
use mas_router::Route;
use rustls::ServerConfig;
#[allow(clippy::trait_duplication_in_bounds)]
pub fn build_router<B>(state: &Arc<AppState>, resources: &[HttpResource]) -> Router<AppState, B>
where
B: HttpBody + Send + 'static,
<B as HttpBody>::Data: Send,
<B as HttpBody>::Error: std::error::Error + Send + Sync,
{
let mut router = Router::with_state_arc(state.clone());
for resource in resources {
router = match resource {
mas_config::HttpResource::Health => {
router.merge(mas_handlers::healthcheck_router(state.clone()))
}
mas_config::HttpResource::Prometheus => {
router.route_service("/metrics", crate::telemetry::prometheus_service())
}
mas_config::HttpResource::Discovery => {
router.merge(mas_handlers::discovery_router(state.clone()))
}
mas_config::HttpResource::Human => {
router.merge(mas_handlers::human_router(state.clone()))
}
mas_config::HttpResource::Static { web_root } => {
let handler = mas_static_files::service(web_root);
router.nest(mas_router::StaticAsset::route(), handler)
}
mas_config::HttpResource::OAuth => {
router.merge(mas_handlers::api_router(state.clone()))
}
mas_config::HttpResource::Compat => {
router.merge(mas_handlers::compat_router(state.clone()))
}
}
}
router
}
pub async fn build_tls_server_config(
config: &HttpTlsConfig,
) -> Result<ServerConfig, anyhow::Error> {
let (key, chain) = config.load().await?;
let key = rustls::PrivateKey(key);
let chain = chain.into_iter().map(rustls::Certificate).collect();
let mut config = rustls::ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_single_cert(chain, key)
.context("failed to build TLS server config")?;
config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
Ok(config)
}
pub fn build_listeners(
fd_manager: &mut ListenFd,
configs: &[HttpBindConfig],
) -> Result<Vec<UnixOrTcpListener>, anyhow::Error> {
let mut listeners = Vec::with_capacity(configs.len());
for bind in configs {
let listener = match bind {
HttpBindConfig::Listen { host, port } => {
let addrs = match host.as_deref() {
Some(host) => (host, *port)
.to_socket_addrs()
.context("could not parse listener host")?
.collect(),
None => vec![
SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), *port),
SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), *port),
],
};
let listener = TcpListener::bind(&addrs[..]).context("could not bind address")?;
listener.set_nonblocking(true)?;
listener.try_into()?
}
HttpBindConfig::Address { address } => {
let addr: SocketAddr = address
.parse()
.context("could not parse listener address")?;
let listener = TcpListener::bind(addr).context("could not bind address")?;
listener.set_nonblocking(true)?;
listener.try_into()?
}
HttpBindConfig::Unix { socket } => {
let listener = UnixListener::bind(socket).context("could not bind socket")?;
listener.try_into()?
}
HttpBindConfig::FileDescriptor {
fd,
kind: UnixOrTcp::Tcp,
} => {
let listener = fd_manager
.take_tcp_listener(*fd)?
.context("no listener found on file descriptor")?;
listener.set_nonblocking(true)?;
listener.try_into()?
}
HttpBindConfig::FileDescriptor {
fd,
kind: UnixOrTcp::Unix,
} => {
let listener = fd_manager
.take_unix_listener(*fd)?
.context("no unix socket found on file descriptor")?;
listener.set_nonblocking(true)?;
listener.try_into()?
}
};
listeners.push(listener);
}
Ok(listeners)
}

View File

@ -24,8 +24,6 @@ serde_json = "1.0.85"
sqlx = { version = "0.6.2", features = ["runtime-tokio-rustls", "postgres"] } sqlx = { version = "0.6.2", features = ["runtime-tokio-rustls", "postgres"] }
lettre = { version = "0.10.1", default-features = false, features = ["serde", "builder"] } lettre = { version = "0.10.1", default-features = false, features = ["serde", "builder"] }
listenfd = "1.0.0"
pem-rfc7468 = "0.6.0" pem-rfc7468 = "0.6.0"
rustls-pemfile = "1.0.1" rustls-pemfile = "1.0.1"
rand = "0.8.5" rand = "0.8.5"

View File

@ -12,18 +12,10 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use std::{ use std::{borrow::Cow, io::Cursor, ops::Deref, path::PathBuf};
borrow::Cow,
io::Cursor,
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, TcpListener, ToSocketAddrs},
ops::Deref,
os::unix::net::UnixListener,
path::PathBuf,
};
use anyhow::{bail, Context}; use anyhow::bail;
use async_trait::async_trait; use async_trait::async_trait;
use listenfd::ListenFd;
use mas_keystore::PrivateKey; use mas_keystore::PrivateKey;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -48,32 +40,49 @@ fn http_address_example_4() -> &'static str {
"0.0.0.0:8080" "0.0.0.0:8080"
} }
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] /// Kind of socket
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, Copy)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum UnixOrTcp { pub enum UnixOrTcp {
/// UNIX domain socket
Unix, Unix,
/// TCP socket
Tcp, Tcp,
} }
impl UnixOrTcp { impl UnixOrTcp {
/// UNIX domain socket
#[must_use]
pub const fn unix() -> Self { pub const fn unix() -> Self {
Self::Unix Self::Unix
} }
/// TCP socket
#[must_use]
pub const fn tcp() -> Self { pub const fn tcp() -> Self {
Self::Tcp Self::Tcp
} }
} }
/// Configuration of a single listener
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] #[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
#[serde(untagged)] #[serde(untagged)]
pub enum BindConfig { pub enum BindConfig {
/// Listen on the specified host and port
Listen { Listen {
/// Host on which to listen.
///
/// Defaults to listening on all addresses
host: Option<String>, host: Option<String>,
/// Port on which to listen.
port: u16, port: u16,
}, },
/// Listen on the specified address
Address { Address {
/// Host and port on which to listen
#[schemars( #[schemars(
example = "http_address_example_1", example = "http_address_example_1",
example = "http_address_example_2", example = "http_address_example_2",
@ -83,85 +92,30 @@ pub enum BindConfig {
address: String, address: String,
}, },
/// Listen on a UNIX domain socket
Unix { Unix {
/// Path to the socket
socket: PathBuf, socket: PathBuf,
}, },
/// Accept connections on file descriptors passed by the parent process.
///
/// This is useful for grabbing sockets passed by systemd.
///
/// See <https://www.freedesktop.org/software/systemd/man/sd_listen_fds.html>
FileDescriptor { FileDescriptor {
/// Index of the file descriptor. Note that this is offseted by 3
/// because of the standard input/output sockets, so setting
/// here a value of `0` will grab the file descriptor `3`
fd: usize, fd: usize,
/// Whether the socket is a TCP socket or a UNIX domain socket. Defaults
/// to TCP.
#[serde(default = "UnixOrTcp::tcp")] #[serde(default = "UnixOrTcp::tcp")]
kind: UnixOrTcp, kind: UnixOrTcp,
}, },
} }
impl BindConfig {
// TODO: move this somewhere else
pub fn listener<T>(&self, fd_manager: &mut ListenFd) -> Result<T, anyhow::Error>
where
T: TryFrom<TcpListener> + TryFrom<UnixListener>,
<T as TryFrom<TcpListener>>::Error: std::error::Error + Sync + Send + 'static,
<T as TryFrom<UnixListener>>::Error: std::error::Error + Sync + Send + 'static,
{
match self {
BindConfig::Listen { host, port } => {
let addrs = match host.as_deref() {
Some(host) => (host, *port)
.to_socket_addrs()
.context("could not parse listener host")?
.collect(),
None => vec![
SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), *port),
SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), *port),
],
};
let listener = TcpListener::bind(&addrs[..]).context("could not bind address")?;
listener.set_nonblocking(true)?;
Ok(listener.try_into()?)
}
BindConfig::Address { address } => {
let addr: SocketAddr = address
.parse()
.context("could not parse listener address")?;
let listener = TcpListener::bind(addr).context("could not bind address")?;
listener.set_nonblocking(true)?;
Ok(listener.try_into()?)
}
BindConfig::Unix { socket } => {
let listener = UnixListener::bind(socket).context("could not bind socket")?;
listener.set_nonblocking(true)?;
Ok(listener.try_into()?)
}
BindConfig::FileDescriptor {
fd,
kind: UnixOrTcp::Tcp,
} => {
let listener = fd_manager
.take_tcp_listener(*fd)?
.context("no listener found on file descriptor")?;
listener.set_nonblocking(true)?;
Ok(listener.try_into()?)
}
BindConfig::FileDescriptor {
fd,
kind: UnixOrTcp::Unix,
} => {
let listener = fd_manager
.take_unix_listener(*fd)?
.context("no unix socket found on file descriptor")?;
listener.set_nonblocking(true)?;
Ok(listener.try_into()?)
}
}
}
}
#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)] #[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum KeyOrFile { pub enum KeyOrFile {
@ -176,19 +130,34 @@ pub enum CertificateOrFile {
CertificateFile(PathBuf), CertificateFile(PathBuf),
} }
/// Configuration related to TLS on a listener
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] #[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
pub struct TlsConfig { pub struct TlsConfig {
/// PEM-encoded X509 certificate chain
#[serde(flatten)] #[serde(flatten)]
pub certificate: CertificateOrFile, pub certificate: CertificateOrFile,
/// Private key
#[serde(flatten)] #[serde(flatten)]
pub key: KeyOrFile, pub key: KeyOrFile,
/// Password used to decode the private key
#[serde(flatten)] #[serde(flatten)]
pub password: Option<PasswordOrFile>, pub password: Option<PasswordOrFile>,
} }
impl TlsConfig { impl TlsConfig {
/// Load the TLS certificate chain and key file from disk
///
/// # Errors
///
/// Returns an error if an error was encountered either while:
/// - reading the certificate, key or password files
/// - decoding the key as PEM or DER
/// - decrypting the key if encrypted
/// - a password was provided but the key was not encrypted
/// - decoding the certificate chain as PEM
/// - the certificate chain is empty
pub async fn load(&self) -> Result<(Vec<u8>, Vec<Vec<u8>>), anyhow::Error> { pub async fn load(&self) -> Result<(Vec<u8>, Vec<Vec<u8>>), anyhow::Error> {
let password = match &self.password { let password = match &self.password {
Some(PasswordOrFile::Password(password)) => Some(Cow::Borrowed(password.as_str())), Some(PasswordOrFile::Password(password)) => Some(Cow::Borrowed(password.as_str())),
@ -267,12 +236,21 @@ pub enum Resource {
Compat, Compat,
/// Static files /// Static files
Static, Static {
/// Path from which to serve static files. If not specified, it will
/// serve the static files embedded in the server binary
#[serde(default)]
web_root: Option<PathBuf>,
},
} }
/// Configuration of a listener /// Configuration of a listener
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] #[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
pub struct ListenerConfig { pub struct ListenerConfig {
/// A unique name for this listener which will be shown in traces and in
/// metrics labels
pub name: Option<String>,
/// List of resources to mount /// List of resources to mount
pub resources: Vec<Resource>, pub resources: Vec<Resource>,
@ -290,11 +268,6 @@ pub struct HttpConfig {
#[serde(default)] #[serde(default)]
pub listeners: Vec<ListenerConfig>, pub listeners: Vec<ListenerConfig>,
/// Path from which to serve static files. If not specified, it will serve
/// the static files embedded in the server binary
#[serde(default)]
pub web_root: Option<PathBuf>,
/// Public URL base from where the authentication service is reachable /// Public URL base from where the authentication service is reachable
pub public_base: Url, pub public_base: Url,
} }
@ -302,15 +275,15 @@ pub struct HttpConfig {
impl Default for HttpConfig { impl Default for HttpConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
web_root: None,
listeners: vec![ listeners: vec![
ListenerConfig { ListenerConfig {
name: Some("web".to_owned()),
resources: vec![ resources: vec![
Resource::Discovery, Resource::Discovery,
Resource::Human, Resource::Human,
Resource::OAuth, Resource::OAuth,
Resource::Compat, Resource::Compat,
Resource::Static, Resource::Static { web_root: None },
], ],
tls: None, tls: None,
binds: vec![BindConfig::Address { binds: vec![BindConfig::Address {
@ -318,6 +291,7 @@ impl Default for HttpConfig {
}], }],
}, },
ListenerConfig { ListenerConfig {
name: Some("internal".to_owned()),
resources: vec![Resource::Health], resources: vec![Resource::Health],
tls: None, tls: None,
binds: vec![BindConfig::Address { binds: vec![BindConfig::Address {

View File

@ -32,7 +32,10 @@ pub use self::{
csrf::CsrfConfig, csrf::CsrfConfig,
database::DatabaseConfig, database::DatabaseConfig,
email::{EmailConfig, EmailSmtpMode, EmailTransportConfig}, email::{EmailConfig, EmailSmtpMode, EmailTransportConfig},
http::{HttpConfig, Resource as HttpResource}, http::{
BindConfig as HttpBindConfig, HttpConfig, ListenerConfig as HttpListenerConfig,
Resource as HttpResource, TlsConfig as HttpTlsConfig, UnixOrTcp,
},
matrix::MatrixConfig, matrix::MatrixConfig,
policy::PolicyConfig, policy::PolicyConfig,
secrets::SecretsConfig, secrets::SecretsConfig,

View File

@ -22,9 +22,20 @@ use super::otel::TraceLayer;
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct ServerLayer<ReqBody> { pub struct ServerLayer<ReqBody> {
listener_name: Option<String>,
_t: PhantomData<ReqBody>, _t: PhantomData<ReqBody>,
} }
impl<B> ServerLayer<B> {
#[must_use]
pub fn new(listener_name: Option<String>) -> Self {
Self {
listener_name,
_t: PhantomData,
}
}
}
impl<ReqBody, ResBody, S> Layer<S> for ServerLayer<ReqBody> impl<ReqBody, ResBody, S> Layer<S> for ServerLayer<ReqBody>
where where
S: Service<Request<ReqBody>, Response = Response<ResBody>> + Clone + Send + 'static, S: Service<Request<ReqBody>, Response = Response<ResBody>> + Clone + Send + 'static,

View File

@ -155,14 +155,11 @@ use tower_http::services::ServeDir;
pub fn service<B: HttpBody + Send + 'static>( pub fn service<B: HttpBody + Send + 'static>(
path: &Option<PathBuf>, path: &Option<PathBuf>,
) -> BoxCloneService<Request<B>, Response, Infallible> { ) -> BoxCloneService<Request<B>, Response, Infallible> {
let builtin = self::builtin::service();
let svc = if let Some(path) = path { let svc = if let Some(path) = path {
let handler = ServeDir::new(path) let handler = ServeDir::new(path).append_index_html_on_directories(false);
.append_index_html_on_directories(false)
.fallback(builtin);
on_service(MethodFilter::HEAD | MethodFilter::GET, handler) on_service(MethodFilter::HEAD | MethodFilter::GET, handler)
} else { } else {
let builtin = self::builtin::service();
on_service(MethodFilter::HEAD | MethodFilter::GET, builtin) on_service(MethodFilter::HEAD | MethodFilter::GET, builtin)
}; };