Change server configuration format
This commit is contained in:
parent
958607eabb
commit
48619a9663
10
.ci/build
Normal file
10
.ci/build
Normal 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
121
Cargo.lock
generated
@ -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",
|
||||
]
|
||||
|
||||
|
@ -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
62
docs/example.toml
Normal 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]]
|
351
src/main.rs
351
src/main.rs
@ -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(())
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user