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",
|
"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]]
|
[[package]]
|
||||||
name = "actix-http"
|
name = "actix-http"
|
||||||
version = "3.9.0"
|
version = "3.9.0"
|
||||||
@ -814,6 +837,12 @@ dependencies = [
|
|||||||
"itoa",
|
"itoa",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-range"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httparse"
|
name = "httparse"
|
||||||
version = "1.9.4"
|
version = "1.9.4"
|
||||||
@ -973,6 +1002,16 @@ version = "0.3.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
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]]
|
[[package]]
|
||||||
name = "minimal-lexical"
|
name = "minimal-lexical"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@ -1329,18 +1368,18 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.209"
|
version = "1.0.213"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09"
|
checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.209"
|
version = "1.0.213"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
|
checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -1359,6 +1398,15 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "0.6.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_urlencoded"
|
name = "serde_urlencoded"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
@ -1436,9 +1484,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.76"
|
version = "2.0.85"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525"
|
checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -1544,6 +1592,40 @@ dependencies = [
|
|||||||
"tokio",
|
"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]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.40"
|
version = "0.1.40"
|
||||||
@ -1570,6 +1652,12 @@ version = "1.17.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicase"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.15"
|
version = "0.3.15"
|
||||||
@ -1614,6 +1702,12 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "v_htmlescape"
|
||||||
|
version = "0.15.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.5"
|
version = "0.9.5"
|
||||||
@ -1643,6 +1737,9 @@ name = "wildmatch"
|
|||||||
version = "2.3.4"
|
version = "2.3.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3928939971918220fed093266b809d1ee4ec6c1a2d72692ff6876898f3b16c19"
|
checksum = "3928939971918220fed093266b809d1ee4ec6c1a2d72692ff6876898f3b16c19"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
@ -1717,10 +1814,20 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.6.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "worldwideportal-server"
|
name = "worldwideportal-server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"actix-files",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"actix-ws",
|
"actix-ws",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@ -1729,8 +1836,10 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-pemfile",
|
"rustls-pemfile",
|
||||||
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"toml",
|
||||||
"wildmatch",
|
"wildmatch",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ edition = "2021"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
actix-files = "0.6.6"
|
||||||
actix-web = { version = "4.9.0", features = ["rustls-0_23"] }
|
actix-web = { version = "4.9.0", features = ["rustls-0_23"] }
|
||||||
actix-ws = "0.3.0"
|
actix-ws = "0.3.0"
|
||||||
anyhow = "1.0.86"
|
anyhow = "1.0.86"
|
||||||
@ -14,6 +15,8 @@ futures-util = "0.3.30"
|
|||||||
log = "0.4.22"
|
log = "0.4.22"
|
||||||
rustls = "0.23.12"
|
rustls = "0.23.12"
|
||||||
rustls-pemfile = "2.1.3"
|
rustls-pemfile = "2.1.3"
|
||||||
|
serde = { version = "1.0.213", features = ["derive"] }
|
||||||
serde_json = "1.0.127"
|
serde_json = "1.0.127"
|
||||||
tokio = { version = "1.39.3", features = ["net", "macros", "tokio-macros", "rt-multi-thread"] }
|
tokio = { version = "1.39.3", features = ["net", "macros", "tokio-macros", "rt-multi-thread", "signal"] }
|
||||||
wildmatch = "2.3.4"
|
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::{
|
use actix_web::{
|
||||||
self,
|
self,
|
||||||
error::{ErrorForbidden, InternalError},
|
body::BoxBody,
|
||||||
|
dev::{
|
||||||
|
self, HttpServiceFactory, ResourceDef, Service, ServiceFactory, ServiceRequest,
|
||||||
|
ServiceResponse,
|
||||||
|
},
|
||||||
|
error::{
|
||||||
|
ErrorBadRequest, ErrorForbidden, ErrorInternalServerError, ErrorNotFound, InternalError,
|
||||||
|
},
|
||||||
get,
|
get,
|
||||||
http::{header::ORIGIN, StatusCode},
|
http::{
|
||||||
|
header::{HOST, ORIGIN},
|
||||||
|
StatusCode,
|
||||||
|
},
|
||||||
middleware::Logger,
|
middleware::Logger,
|
||||||
rt::{self, net::TcpStream},
|
rt::{self, net::TcpStream},
|
||||||
web::{self, Data},
|
web::{self, Data},
|
||||||
App, Error, HttpRequest, HttpResponse, HttpServer, Responder,
|
App, Error, HttpRequest, HttpResponse, HttpServer, Responder,
|
||||||
};
|
};
|
||||||
use actix_ws::{AggregatedMessage, CloseCode, Closed};
|
use actix_ws::{AggregatedMessage, CloseCode, Closed};
|
||||||
use anyhow::Context;
|
use futures_util::{future::LocalBoxFuture, FutureExt, StreamExt};
|
||||||
use futures_util::StreamExt;
|
use log::{info, warn};
|
||||||
use rustls::pki_types::CertificateDer;
|
use rustls::{
|
||||||
|
crypto::{aws_lc_rs, CryptoProvider},
|
||||||
|
pki_types::CertificateDer,
|
||||||
|
server::ResolvesServerCert,
|
||||||
|
sign::CertifiedKey,
|
||||||
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::BTreeMap,
|
||||||
env,
|
env,
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
|
future::{ready, Ready},
|
||||||
io::{self, BufReader},
|
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;
|
use wildmatch::WildMatch;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
pub struct TlsConfig {
|
pub struct TlsConfig {
|
||||||
pub private_key_file: String,
|
pub private_key_file: String,
|
||||||
pub certificate_chain_file: String,
|
pub certificate_chain_file: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
pub struct ServerConfig {
|
pub struct ServerConfig {
|
||||||
pub upstream_mud: String,
|
|
||||||
pub banner_to_mud: String,
|
|
||||||
pub listen_port: u16,
|
pub listen_port: u16,
|
||||||
pub bind_address: String,
|
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 startup_script_file: String,
|
||||||
pub tls_config: Option<TlsConfig>,
|
pub allowed_origins: Vec<WildType>,
|
||||||
pub allowed_origins: Vec<WildMatch>,
|
}
|
||||||
|
|
||||||
|
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")]
|
#[get("/ws")]
|
||||||
async fn ws(
|
async fn ws(
|
||||||
config_data: Data<ServerConfig>,
|
config_data: Data<DynamicConfigLock>,
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
body: web::Payload,
|
body: web::Payload,
|
||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
|
let mud = with_mudconfig(&req, config_data)?;
|
||||||
match req.headers().get(&ORIGIN) {
|
match req.headers().get(&ORIGIN) {
|
||||||
None => Err(ErrorForbidden("Missing origin"))?,
|
None => Err(ErrorForbidden("Missing origin"))?,
|
||||||
Some(origin) => {
|
Some(origin) => {
|
||||||
if !config_data
|
if !mud
|
||||||
.allowed_origins
|
.allowed_origins
|
||||||
.iter()
|
.iter()
|
||||||
.any(|o| o.matches(origin.to_str().unwrap_or("invalid")))
|
.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 (response, mut session, stream) = actix_ws::handle(&req, body)?;
|
||||||
let mut stream = stream.aggregate_continuations().max_continuation_size(1024);
|
let mut stream = stream.aggregate_continuations().max_continuation_size(1024);
|
||||||
|
|
||||||
let mut tcp_stream: TcpStream = TcpStream::connect(&config_data.upstream_mud).await?;
|
let mut tcp_stream: TcpStream = TcpStream::connect(&mud.upstream_mud).await?;
|
||||||
let subst_banner = config_data
|
let subst_banner = mud
|
||||||
.banner_to_mud
|
.banner_to_mud
|
||||||
.replace(
|
.replace(
|
||||||
"%i",
|
"%i",
|
||||||
@ -71,7 +138,7 @@ async fn ws(
|
|||||||
.replace("%n", "\r\n");
|
.replace("%n", "\r\n");
|
||||||
tcp_stream.write_all(subst_banner.as_bytes()).await?;
|
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))?;
|
.map_err(|e| InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR))?;
|
||||||
|
|
||||||
rt::spawn(async move {
|
rt::spawn(async move {
|
||||||
@ -128,99 +195,221 @@ async fn ws(
|
|||||||
Ok::<HttpResponse, Error>(response)
|
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.
|
// We load this on each connection so it can change.
|
||||||
fn fetch_startup_script(filename: &str) -> anyhow::Result<String> {
|
fn fetch_startup_script(filename: &str) -> anyhow::Result<String> {
|
||||||
Ok(fs::read_to_string(filename)?)
|
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]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
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);
|
// This is a double Arc, but actix doesn't provide an alternative.
|
||||||
let config = data.get_ref();
|
let data = Data::new(dynconfig.clone());
|
||||||
|
|
||||||
// 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")?;
|
|
||||||
|
|
||||||
let server_data = data.clone();
|
let server_data = data.clone();
|
||||||
|
let static_file_svc = DynStaticFiles(dynconfig.clone());
|
||||||
let server = HttpServer::new(move || {
|
let server = HttpServer::new(move || {
|
||||||
let logger = Logger::default();
|
let logger = Logger::default();
|
||||||
App::new()
|
App::new()
|
||||||
.wrap(logger)
|
.wrap(logger)
|
||||||
.app_data(server_data.clone())
|
.app_data(server_data.clone())
|
||||||
|
.default_service(static_file_svc.clone())
|
||||||
.service(ws)
|
.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))?,
|
None => server.bind((config.bind_address.clone(), config.listen_port))?,
|
||||||
Some(tls_config) => {
|
Some(_tls) => {
|
||||||
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");
|
|
||||||
let tls_config = rustls::server::ServerConfig::builder()
|
let tls_config = rustls::server::ServerConfig::builder()
|
||||||
.with_no_client_auth()
|
.with_no_client_auth()
|
||||||
.with_single_cert(certs, key)
|
.with_cert_resolver(Arc::new(DynconfigCertResolver(dynconfig.clone())));
|
||||||
.expect("Bad certificate/key");
|
|
||||||
server.bind_rustls_0_23(
|
server.bind_rustls_0_23(
|
||||||
(config.bind_address.clone(), config.listen_port),
|
(config.bind_address.clone(), config.listen_port),
|
||||||
tls_config,
|
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?;
|
server.run().await?;
|
||||||
|
let _ = exit_tx.send(());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user