169 lines
5.3 KiB
Rust
169 lines
5.3 KiB
Rust
use actix_web::{
|
|
self,
|
|
error::InternalError,
|
|
get,
|
|
http::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 serde_json::json;
|
|
use std::{env, fs, num::ParseIntError};
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
|
|
pub struct ServerConfig {
|
|
pub upstream_mud: String,
|
|
pub banner_to_mud: String,
|
|
pub listen_port: u16,
|
|
pub bind_address: String,
|
|
pub startup_script_file: String,
|
|
}
|
|
|
|
#[get("/ws")]
|
|
async fn ws(
|
|
config_data: Data<ServerConfig>,
|
|
req: HttpRequest,
|
|
body: web::Payload,
|
|
) -> impl Responder {
|
|
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
|
|
.banner_to_mud
|
|
.replace(
|
|
"%i",
|
|
&req.peer_addr()
|
|
.map(|a| a.to_string())
|
|
.unwrap_or_else(|| "unknown".to_owned()),
|
|
)
|
|
.replace("%n", "\r\n");
|
|
tcp_stream.write_all(subst_banner.as_bytes()).await?;
|
|
|
|
let script = fetch_startup_script(&config_data.startup_script_file)
|
|
.map_err(|e| InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR))?;
|
|
|
|
rt::spawn(async move {
|
|
if session
|
|
.text(
|
|
json!({
|
|
"RunLua": script
|
|
})
|
|
.to_string(),
|
|
)
|
|
.await
|
|
.is_err()
|
|
{
|
|
let _ = session.close(Some(CloseCode::Normal.into())).await;
|
|
return;
|
|
}
|
|
let mut readbuf: [u8; 1024] = [0; 1024];
|
|
loop {
|
|
tokio::select! {
|
|
ws_msg = stream.next() => {
|
|
match ws_msg {
|
|
None => break,
|
|
Some(Err(_e)) => break,
|
|
Some(Ok(AggregatedMessage::Binary(bin))) => {
|
|
if tcp_stream.write_all(&bin).await.is_err() {
|
|
break
|
|
}
|
|
}
|
|
|
|
Some(Ok(AggregatedMessage::Ping(msg))) => {
|
|
if let Err(Closed) = session.pong(&msg).await {
|
|
break
|
|
}
|
|
}
|
|
Some(Ok(_)) => {},
|
|
}
|
|
},
|
|
tcp_data_len = tcp_stream.read(&mut readbuf) => {
|
|
match tcp_data_len {
|
|
Err(_e) => break,
|
|
Ok(0) => break,
|
|
Ok(n) =>
|
|
if let Err(Closed) = session.binary(readbuf[0..n].to_vec()).await {
|
|
break;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
let _ = session.close(Some(CloseCode::Normal.into())).await;
|
|
});
|
|
|
|
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.",
|
|
)
|
|
})?
|
|
,
|
|
})
|
|
}
|
|
|
|
// 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)?)
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> anyhow::Result<()> {
|
|
env_logger::init();
|
|
|
|
let config = extract_server_config_from_environment()?;
|
|
|
|
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")?;
|
|
|
|
let server_data = data.clone();
|
|
HttpServer::new(move || {
|
|
let logger = Logger::default();
|
|
App::new()
|
|
.wrap(logger)
|
|
.app_data(server_data.clone())
|
|
.service(ws)
|
|
})
|
|
.bind((config.bind_address.clone(), config.listen_port))?
|
|
.run()
|
|
.await?;
|
|
Ok(())
|
|
}
|