Change server configuration format

This commit is contained in:
Condorra 2024-10-27 18:05:16 +11:00
parent 958607eabb
commit 48619a9663
5 changed files with 462 additions and 89 deletions

10
.ci/build Normal file
View File

@ -0,0 +1,10 @@
#!/bin/bash -e
set -Eeuo pipefail
export HOME=$(pwd)
export CARGO_HOME=$(pwd)/.cargo
cd wwp-server-repo
ln -s ../target ./target
cargo build --release
cp ./target/release/worldwideportal-server ../wwpserver/

121
Cargo.lock generated
View File

@ -19,6 +19,29 @@ dependencies = [
"tracing",
]
[[package]]
name = "actix-files"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0773d59061dedb49a8aed04c67291b9d8cf2fe0b60130a381aab53c6dd86e9be"
dependencies = [
"actix-http",
"actix-service",
"actix-utils",
"actix-web",
"bitflags",
"bytes",
"derive_more",
"futures-core",
"http-range",
"log",
"mime",
"mime_guess",
"percent-encoding",
"pin-project-lite",
"v_htmlescape",
]
[[package]]
name = "actix-http"
version = "3.9.0"
@ -814,6 +837,12 @@ dependencies = [
"itoa",
]
[[package]]
name = "http-range"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
[[package]]
name = "httparse"
version = "1.9.4"
@ -973,6 +1002,16 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@ -1329,18 +1368,18 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]]
name = "serde"
version = "1.0.209"
version = "1.0.213"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09"
checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.209"
version = "1.0.213"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5"
dependencies = [
"proc-macro2",
"quote",
@ -1359,6 +1398,15 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@ -1436,9 +1484,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.76"
version = "2.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525"
checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56"
dependencies = [
"proc-macro2",
"quote",
@ -1544,6 +1592,40 @@ dependencies = [
"tokio",
]
[[package]]
name = "toml"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "tracing"
version = "0.1.40"
@ -1570,6 +1652,12 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "unicase"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df"
[[package]]
name = "unicode-bidi"
version = "0.3.15"
@ -1614,6 +1702,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "v_htmlescape"
version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c"
[[package]]
name = "version_check"
version = "0.9.5"
@ -1643,6 +1737,9 @@ name = "wildmatch"
version = "2.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3928939971918220fed093266b809d1ee4ec6c1a2d72692ff6876898f3b16c19"
dependencies = [
"serde",
]
[[package]]
name = "windows-sys"
@ -1717,10 +1814,20 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
dependencies = [
"memchr",
]
[[package]]
name = "worldwideportal-server"
version = "0.1.0"
dependencies = [
"actix-files",
"actix-web",
"actix-ws",
"anyhow",
@ -1729,8 +1836,10 @@ dependencies = [
"log",
"rustls",
"rustls-pemfile",
"serde",
"serde_json",
"tokio",
"toml",
"wildmatch",
]

View File

@ -6,6 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-files = "0.6.6"
actix-web = { version = "4.9.0", features = ["rustls-0_23"] }
actix-ws = "0.3.0"
anyhow = "1.0.86"
@ -14,6 +15,8 @@ futures-util = "0.3.30"
log = "0.4.22"
rustls = "0.23.12"
rustls-pemfile = "2.1.3"
serde = { version = "1.0.213", features = ["derive"] }
serde_json = "1.0.127"
tokio = { version = "1.39.3", features = ["net", "macros", "tokio-macros", "rt-multi-thread"] }
wildmatch = "2.3.4"
tokio = { version = "1.39.3", features = ["net", "macros", "tokio-macros", "rt-multi-thread", "signal"] }
toml = "0.8.19"
wildmatch = { version = "2.3.4", features = ["serde"] }

62
docs/example.toml Normal file
View File

@ -0,0 +1,62 @@
# The port to listen on. Typically 443 if you want https,
# otherwise 80. However, you can use another port and include it
# in the URL.
# Cannot be changed without a complete restart.
listen_port = 443
# :: to accept from anywhere, or select a particular IP to listen on.
# Cannot be changed without a complete restart.
bind_address = "::"
# Where to find the static worldwideportal frontend to serve up. This
# default value works in the Docker/podman container.
# Updated on SIGHUP.
static_root = "/frontendstatic"
# This entire section is optional. Leave it off if you want the server
# to speak HTTP and not HTTPS (e.g. because you are going to proxy it).
# Be aware most browsers don't support ws:// URLs for anything except
# localhost.
# Consider getting a certificate for free from LetsEncrypt.
# This whole section is updated on SIGHUP, except that it is not possible
# to change from HTTP to HTTPS or vice versa, only to update the key / cert.
# Sending a SIGHUP after each time the certificate is rotated by certbot is
# highly recommended, to ensure an expired certificate is not served up.
[tls]
private_key_file = "/etc/letsencrypt/live/blastmud.org/privkey.pem"
certificate_chain_file = "/etc/letsencrypt/live/blastmud.org/fullchain.pem"
# An arbitrary number of muds are allowed. Each one runs on a different host
# name as a virtual host (but with the same binding address and certificate).
[[muds]]
# The Host (including port number if not 80/443) to match.
# Note that you currently can't send a different certificate for different
# MUDs - if you have more than one, either use a wildcard certificate, or
# use one certificate with subject alt names (SANs) for all relevant hosts.
hostname = "wstest.blastmud.org"
# The username and port for where to connect over telnet for your MUD.
upstream_mud = "localhost:4000"
# The text to send to the MUD when you first connect. Two special symbols
# are supported: %i is substituted for the client's IP address. %n is
# substituted for \r\n. Note that no newlines are sent unless you explicitly
# say they should be sent with %n.
# Consider implementing a command in your MUD to accept this banner and update
# the user's IP - but be careful not to accept it from users connected directly
# over telnet, or more than once (there is nothing stopping users from sending
# further text identical to the banner a second time if they want to). The
# banner is guaranteed to send before any user-supplied input.
banner_to_mud = ""
# The path to a Lua script file to send to the client to execute.
startup_script_file = "localdata/startup.lua"
# Only browsers coming from an allowed Origin will be allowed to connect.
# This stops random web pages that a user is browsing from logging in to your
# MUD from the web user's IP.
# You should at a minimum allowlist the host users will use to connect to your
# MUD (hostname above) if you want them to be able to play through this
# worldwideportal frontend.
allowed_origins = [
"https://*.blastmud.org:*", "https://*.blastmud.org", "http://localhost:*"
]
# Repeat the section to add more MUDs...
# [[muds]]

View File

@ -1,52 +1,119 @@
use actix_files::Files;
use actix_web::{
self,
error::{ErrorForbidden, InternalError},
body::BoxBody,
dev::{
self, HttpServiceFactory, ResourceDef, Service, ServiceFactory, ServiceRequest,
ServiceResponse,
},
error::{
ErrorBadRequest, ErrorForbidden, ErrorInternalServerError, ErrorNotFound, InternalError,
},
get,
http::{header::ORIGIN, StatusCode},
http::{
header::{HOST, ORIGIN},
StatusCode,
},
middleware::Logger,
rt::{self, net::TcpStream},
web::{self, Data},
App, Error, HttpRequest, HttpResponse, HttpServer, Responder,
};
use actix_ws::{AggregatedMessage, CloseCode, Closed};
use anyhow::Context;
use futures_util::StreamExt;
use rustls::pki_types::CertificateDer;
use futures_util::{future::LocalBoxFuture, FutureExt, StreamExt};
use log::{info, warn};
use rustls::{
crypto::{aws_lc_rs, CryptoProvider},
pki_types::CertificateDer,
server::ResolvesServerCert,
sign::CertifiedKey,
};
use serde::Deserialize;
use serde_json::json;
use std::{
collections::BTreeMap,
env,
fs::{self, File},
future::{ready, Ready},
io::{self, BufReader},
num::ParseIntError,
sync::{Arc, RwLock},
};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
select,
signal::unix::{signal, SignalKind},
spawn,
sync::oneshot,
};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use wildmatch::WildMatch;
#[derive(Deserialize)]
pub struct TlsConfig {
pub private_key_file: String,
pub certificate_chain_file: String,
}
#[derive(Deserialize)]
pub struct ServerConfig {
pub upstream_mud: String,
pub banner_to_mud: String,
pub listen_port: u16,
pub bind_address: String,
pub tls: Option<TlsConfig>,
pub static_root: String,
pub muds: Vec<MudConfig<String>>,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
listen_port: 443,
bind_address: "::".to_owned(),
tls: None,
static_root: "/frontendstatic".to_owned(),
muds: vec![],
}
}
}
#[derive(Deserialize, Clone, Debug)]
pub struct MudConfig<WildType> {
pub hostname: String,
pub upstream_mud: String,
pub banner_to_mud: String,
pub startup_script_file: String,
pub tls_config: Option<TlsConfig>,
pub allowed_origins: Vec<WildMatch>,
pub allowed_origins: Vec<WildType>,
}
fn with_mudconfig(
req: &HttpRequest,
config_data: Data<DynamicConfigLock>,
) -> Result<MudConfig<WildMatch>, Error> {
match req.headers().get(&HOST) {
None => Err(ErrorBadRequest("Missing Host header"))?,
Some(host) => match config_data
.read()
.unwrap()
.host_map
.get(host.to_str().unwrap_or("invalid"))
{
None => Err(ErrorNotFound(
"No MUD matching your request is currently enabled",
))?,
Some(v) => Ok(v.clone()),
},
}
}
#[get("/ws")]
async fn ws(
config_data: Data<ServerConfig>,
config_data: Data<DynamicConfigLock>,
req: HttpRequest,
body: web::Payload,
) -> impl Responder {
let mud = with_mudconfig(&req, config_data)?;
match req.headers().get(&ORIGIN) {
None => Err(ErrorForbidden("Missing origin"))?,
Some(origin) => {
if !config_data
if !mud
.allowed_origins
.iter()
.any(|o| o.matches(origin.to_str().unwrap_or("invalid")))
@ -59,8 +126,8 @@ async fn ws(
let (response, mut session, stream) = actix_ws::handle(&req, body)?;
let mut stream = stream.aggregate_continuations().max_continuation_size(1024);
let mut tcp_stream: TcpStream = TcpStream::connect(&config_data.upstream_mud).await?;
let subst_banner = config_data
let mut tcp_stream: TcpStream = TcpStream::connect(&mud.upstream_mud).await?;
let subst_banner = mud
.banner_to_mud
.replace(
"%i",
@ -71,7 +138,7 @@ async fn ws(
.replace("%n", "\r\n");
tcp_stream.write_all(subst_banner.as_bytes()).await?;
let script = fetch_startup_script(&config_data.startup_script_file)
let script = fetch_startup_script(&mud.startup_script_file)
.map_err(|e| InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR))?;
rt::spawn(async move {
@ -128,99 +195,221 @@ async fn ws(
Ok::<HttpResponse, Error>(response)
}
fn extract_server_config_from_environment() -> anyhow::Result<ServerConfig> {
Ok(ServerConfig {
upstream_mud: env::var("UPSTREAM_MUD").map_err(|_| {
anyhow::Error::msg(
"Expected UPSTREAM_MUD environment variable specifying where to connect.",
)
})?,
banner_to_mud: env::var("BANNER_TO_MUD").map_err(|_| {
anyhow::Error::msg(
"Expected BANNER_TO_MUD environment variable specifying message to send to MUD.",
)
})?,
listen_port: env::var("LISTEN_PORT")
.map_err(|_| {
anyhow::Error::msg(
"Expected LISTEN_PORT environment variable specifying port to listen on.",
)
})
.and_then(|v| {
v.parse::<u16>().map_err(|_e: ParseIntError| {
anyhow::Error::msg("LISTEN_PORT should be a decimal port number")
})
})?,
bind_address: env::var("BIND_ADDRESS").unwrap_or_else(|_| "::".to_owned()),
startup_script_file: env::var("STARTUP_SCRIPT_FILE")
.map_err(|_| {
anyhow::Error::msg(
"Expected STARTUP_SCRIPT_FILE environment variable containing filename of script to send to client.",
)
})?,
tls_config: env::var("PRIVATE_KEY_FILE").ok().map(|pk| Ok::<TlsConfig, anyhow::Error>(TlsConfig {
private_key_file: pk,
certificate_chain_file: env::var("CERTIFICATE_CHAIN_FILE").map_err(|_| {
anyhow::Error::msg(
"Expected CERTIFICATE_CHAIN_FILE when PRIVATE_KEY_FILE is set."
)
})?,
})).transpose()?,
allowed_origins: env::var("ALLOWED_ORIGINS")
.map_err(|_| {
anyhow::Error::msg("Expected ALLOWED_ORIGINS with list of origins to accept") })?
.split(',').map(|v| WildMatch::new(v)).collect()
})
}
// We load this on each connection so it can change.
fn fetch_startup_script(filename: &str) -> anyhow::Result<String> {
Ok(fs::read_to_string(filename)?)
}
fn load_config_file(filename: &str) -> anyhow::Result<ServerConfig> {
info!("Reading config file {}", filename);
Ok(toml::from_str(&fs::read_to_string(filename)?)?)
}
fn validate_config_file(config: &ServerConfig) {
match fs::metadata(&config.static_root).map(|meta| meta.is_dir()) {
Err(e) => warn!(
"Error checking static_root {} is valid: {}",
&config.static_root, e
),
Ok(false) => warn!("static_root {} is not a directory", &config.static_root),
Ok(true) => {}
}
for mud in &config.muds {
match fetch_startup_script(&mud.startup_script_file) {
Err(e) => warn!(
"Error reading startup script file {} for MUD {}: {}",
mud.startup_script_file, mud.hostname, e
),
Ok(_) => {}
}
}
}
#[derive(Debug)]
struct DynamicConfigData {
host_map: BTreeMap<String, MudConfig<WildMatch>>,
certified_key: Option<Arc<CertifiedKey>>,
static_root: String,
}
type DynamicConfigLock = Arc<RwLock<DynamicConfigData>>;
#[derive(Debug)]
struct DynconfigCertResolver(DynamicConfigLock);
impl ResolvesServerCert for DynconfigCertResolver {
fn resolve(
&self,
_client_hello: rustls::server::ClientHello<'_>,
) -> Option<Arc<rustls::sign::CertifiedKey>> {
self.0.read().unwrap().certified_key.as_ref().cloned()
}
}
#[derive(Clone)]
struct DynStaticFiles(DynamicConfigLock);
impl HttpServiceFactory for DynStaticFiles {
fn register(self, config: &mut actix_web::dev::AppService) {
let rdef = if config.is_root() {
ResourceDef::root_prefix("/")
} else {
ResourceDef::prefix("/")
};
config.register_service(rdef, None, self, None)
}
}
impl ServiceFactory<ServiceRequest> for DynStaticFiles {
type Response = ServiceResponse;
type Error = Error;
type Config = ();
type Service = DynStaticFiles;
type InitError = ();
type Future = Ready<Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future {
ready(Ok(self.clone()))
}
}
impl Service<ServiceRequest> for DynStaticFiles {
type Response = ServiceResponse<BoxBody>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
dev::always_ready!();
fn call(&self, req: ServiceRequest) -> Self::Future {
let files = Files::new("/", &self.0.read().unwrap().static_root).index_file("index.html");
Box::pin(async move {
files
.new_service(())
.await
.map_err(|_| ErrorInternalServerError("Internal service error"))?
.call(req)
.await
})
}
}
fn make_dynamic_config_data(config: &ServerConfig) -> anyhow::Result<DynamicConfigData> {
Ok(DynamicConfigData {
host_map: config
.muds
.iter()
.map(|c| {
(
c.hostname.clone(),
MudConfig::<WildMatch> {
hostname: c.hostname.clone(),
upstream_mud: c.upstream_mud.clone(),
banner_to_mud: c.banner_to_mud.clone(),
startup_script_file: c.startup_script_file.clone(),
allowed_origins: c
.allowed_origins
.iter()
.map(|ao| WildMatch::new(ao))
.collect(),
},
)
})
.collect::<BTreeMap<String, MudConfig<WildMatch>>>(),
certified_key: match &config.tls {
None => None,
Some(tls) => {
let certs: Vec<CertificateDer> = rustls_pemfile::certs(&mut BufReader::new(
&mut File::open(&tls.certificate_chain_file)?,
))
.collect::<Result<Vec<CertificateDer>, io::Error>>()?;
let key = rustls_pemfile::private_key(&mut BufReader::new(&mut File::open(
&tls.private_key_file,
)?))?
.expect("No private key found in private key file");
let key = aws_lc_rs::default_provider()
.key_provider
.load_private_key(key)?;
CryptoProvider::get_default();
Some(CertifiedKey::new(certs, key).into())
}
},
static_root: config.static_root.clone(),
})
}
fn rehash_config(config_file: &str, dynconfig_lock: &DynamicConfigLock) -> anyhow::Result<()> {
let config: ServerConfig = load_config_file(config_file)?;
validate_config_file(&config);
let dynconfig: DynamicConfigData = make_dynamic_config_data(&config)?;
let dcref: &mut DynamicConfigData = &mut dynconfig_lock.write().unwrap();
*dcref = dynconfig;
Ok(())
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
env_logger::init();
env_logger::builder()
.filter_level(log::LevelFilter::Info)
.parse_default_env()
.init();
let args: Vec<String> = env::args().collect();
let config_file = match args.get(1) {
None => "/etc/worldwideportal_server.toml".to_owned(),
Some(cfg) => cfg.clone(),
};
if config_file == "-h" || config_file == "--help" {
println!("Usage: worldwideportal-server /path/to/config/file.toml");
return Ok(());
}
let config = extract_server_config_from_environment()?;
let config: ServerConfig = load_config_file(&config_file)?;
validate_config_file(&config);
let dynconfig: DynamicConfigLock = RwLock::new(make_dynamic_config_data(&config)?).into();
let data = Data::new(config);
let config = data.get_ref();
// Do this early so we fail fast if config is wrong.
fetch_startup_script(&config.startup_script_file)
.context("While checking STARTUP_SCRIPT_FILE can be read")?;
// This is a double Arc, but actix doesn't provide an alternative.
let data = Data::new(dynconfig.clone());
let server_data = data.clone();
let static_file_svc = DynStaticFiles(dynconfig.clone());
let server = HttpServer::new(move || {
let logger = Logger::default();
App::new()
.wrap(logger)
.app_data(server_data.clone())
.default_service(static_file_svc.clone())
.service(ws)
});
let server = match &config.tls_config {
let server = match &dynconfig.read().unwrap().certified_key {
None => server.bind((config.bind_address.clone(), config.listen_port))?,
Some(tls_config) => {
let certs: Vec<CertificateDer> = rustls_pemfile::certs(&mut BufReader::new(
&mut File::open(&tls_config.certificate_chain_file)?,
))
.collect::<Result<Vec<CertificateDer>, io::Error>>()?;
let key = rustls_pemfile::private_key(&mut BufReader::new(&mut File::open(
&tls_config.private_key_file,
)?))?
.expect("No private key found in private key file");
Some(_tls) => {
let tls_config = rustls::server::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, key)
.expect("Bad certificate/key");
.with_cert_resolver(Arc::new(DynconfigCertResolver(dynconfig.clone())));
server.bind_rustls_0_23(
(config.bind_address.clone(), config.listen_port),
tls_config,
)?
}
};
let mut sighup = signal(SignalKind::hangup())?;
let (exit_tx, exit_rx) = oneshot::channel::<()>();
let exit_rx = exit_rx.shared();
spawn(async move {
loop {
select! {
_ = sighup.recv() => {
let _ = rehash_config(&config_file, &dynconfig);
}
_ = exit_rx.clone() => {
return;
}
}
}
});
server.run().await?;
let _ = exit_tx.send(());
Ok(())
}