use std::net::SocketAddr; use std::path::Path; use bus::Bus; mod config; mod util; mod app; mod system; mod protocol; mod manager; use app::App; use hyper::body::Bytes; use hyper_util::rt::TokioIo; use tokio::sync::mpsc::{self, Sender}; use system::{ cache::WebCache, net::{ Server, Stream, tcp::*, tls::*, }, }; use protocol::QRPacket; struct HttpServiceArgs { tx:Sender, cache:WebCache, } impl HttpServiceArgs { pub async fn clone(&mut self) -> Self { Self { tx:self.tx.clone(), cache:self.cache.clone(), } } } async fn service_http(mut request:hyper::Request, args:HttpServiceArgs) -> Result>, std::convert::Infallible> // Serve cached files and upgrade websocket connections. // { use hyper::{Response, body::Bytes, header::{CONTENT_TYPE, CACHE_CONTROL, ACCEPT_LANGUAGE}}; use http_body_util::Full; println!("Serving: {}", request.uri().path()); /* ** Get client language. */ let mut language_code = 0; if let Some(languages) = request.headers().get(ACCEPT_LANGUAGE) { if let Ok(languages) = languages.to_str() { for decl in languages.split(",") { let decl = decl.trim().to_lowercase(); if decl.len() >= 2 { let code = &decl[..2]; language_code = match code { "en" => 0, "ja" => 1, _ => { continue; } }; break; } } } } /* ** Upgrade to websocket, if reqeusted. */ if hyper_tungstenite::is_upgrade_request(&request) { if let Ok((response, websocket)) = hyper_tungstenite::upgrade(&mut request, None) { tokio::task::spawn(async move { match websocket.await { Ok(websocket) => manager::handle_ws(websocket, args).await, Err(_) => Err(()), }.ok() }); Ok(response) } else { Ok(Response::builder() .status(401) .body(Full::new(Bytes::new())) .unwrap()) } } /* ** Otherwise, serve cached assets. */ else { match args.cache.fetch(request.uri().path()) { Some(data) => { let mut output = data.data; match request.uri().path() { "/.js" => { output = [ format!("let CONFIG_VERSION = \"{}\";", config::VERSION).as_bytes().to_vec(), format!("let CONFIG_LANGUAGE = {};", language_code).as_bytes().to_vec(), output, ].concat(); } _ => { } } Ok(Response::builder() .header(CONTENT_TYPE, &data.mime) .header(CACHE_CONTROL, "no-cache") .body(Full::new(Bytes::from(output))).unwrap()) } None => Ok(Response::builder() .header(CONTENT_TYPE, "text/html") .header(CACHE_CONTROL, "no-cache") .body(Full::new(Bytes::from(args.cache.fetch("/.html").unwrap().data))).unwrap()), } } } async fn handle_http(stream:S, addr:SocketAddr, mut args:HttpServiceArgs) -> Result<(),()> where S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static // Hand off socket connection to Hyper server. // { use hyper::server::conn::http1; use hyper::service::service_fn; println!("Connection from {}", addr.to_string()); let io = TokioIo::new(stream); let args = args.clone().await; let conn = http1::Builder::new() .serve_connection(io, service_fn(move |req| { service_http(req, args) })); conn.with_upgrades().await.ok(); Ok(()) } async fn handle_tcp(stream:TcpStream, addr:SocketAddr, args:HttpServiceArgs) -> Result<(),()> { handle_http(stream.to_stream(), addr, args).await } async fn handle_tls(stream:TlsStream, addr:SocketAddr, args:HttpServiceArgs) -> Result<(),()> { handle_http(stream.to_stream(), addr, args).await } /* ** [multi_thread, worker_threads] ** Workaround for single core computer, where listening tasks blocked ** client-handling and data-processing tasks. */ #[tokio::main(flavor = "multi_thread", worker_threads = 12)] async fn main() { println!("Server Version: {}", config::VERSION); /* ** Initialize application data. */ let app = if let Ok(result) = App::init() { result } else { println!("fatal: failed to initialize server."); return; }; /* ** Initialize central bus and data serivce. */ let (data_tx, data_rx) = mpsc::channel::(64); tokio::spawn(async move { manager::thread_system(app, data_rx).await; }); /* ** Load image assets. */ let mut js_asset_data = String::from("const GAME_ASSET={Image:{"); let asset_path = Path::new("www/asset/"); for name in [ "Promote", "Militia", "Lance", "Knight", "Tower", "Castle", "Dragon", "Behemoth", "Heart", ] { if let Ok(output) = util::imager::load(asset_path.join(&format!("{}.svg", name))) { js_asset_data += &format!("{}:{},", name, output); } else { println!("error: failed to load asset: {}", name); } } js_asset_data += "}};"; match b_main.connect().await { Ok(mut bus) => { /* ** Cache source files. */ let cache = WebCache::new(); cache.cache("text/html", "/.html", &[ WebCache::file("www/.html"), ]).ok(); cache.cache_whitespace_minimize("/.html").ok(); cache.cache("text/css", "/.css", &[ WebCache::file("www/css/main.css"), WebCache::file("www/css/ui.css"), WebCache::file("www/css/form.css"), WebCache::file("www/css/game.css"), WebCache::file("www/css/text.css"), WebCache::file("www/css/profile.css"), WebCache::file("www/css/util.css"), ]).ok(); cache.cache("text/javascript", "/.js", &[ WebCache::file("www/js/const.js"), WebCache::file("www/js/language.js"), WebCache::file("www/js/util.js"), WebCache::file("www/js/badge.js"), WebCache::file("www/js/game_asset.js"), WebCache::string(&js_asset_data), WebCache::file("www/js/game.js"), WebCache::file("www/js/interface.js"), WebCache::file("www/js/ui.js"), WebCache::file("www/js/scene.js"), WebCache::file("www/js/system.js"), WebCache::file("www/js/main.js"), ]).ok(); cache.cache("image/png", "/favicon.png", &[ WebCache::file("www/asset/favicon.png"), ]).ok(); cache.cache("image/png", "/favicon_notify.png", &[ WebCache::file("www/asset/favicon_notify.png"), ]).ok(); let about_path = std::path::Path::new("www/pages/about"); for doc in [ "main", ] { if cache.cache("text/html", &format!("/about/{}.html", doc), &[ WebCache::markdown(about_path.join(format!("{}.md", doc))) ]).is_err() { println!("error: failed to load: {}", doc); } } let guide_path = std::path::Path::new("www/pages/guide"); for doc in [ "game", "pieces", "interface", ] { if cache.cache("text/html", &format!("/guide/{}.html", doc), &[ WebCache::markdown(guide_path.join(format!("{}.md", doc))), ]).is_err() { println!("error: failed to load: {}", doc); } } /* ** Initialize network services. */ let mut tcp_server = TcpServer::new(); match tcp_server.bind("127.0.0.1:38611").await { Ok(_) => { let mut b = bus.connect().await.unwrap(); let c = cache.clone(); let data_tx = data_tx.clone(); tokio::spawn(async move { while tcp_server.accept(handle_tcp, HttpServiceArgs { tx:data_tx, cache:c.clone(), }).await.is_ok() { } }); } Err(_) => { println!("error: failed to bind TCP port 38611."); } } let mut tls_server = TlsServer::new(); for domain in [ "omen.kirisame.com", "dzura.com", ] { if tls_server.add_cert(domain, &format!("cert/{}/fullchain.pem", domain), &format!("cert/{}/privkey.pem", domain)).await.is_err() { println!("error: failed to load TLS certificates for {}.", domain); } } match tls_server.bind("0.0.0.0:38612").await { Ok(_) => { let (tx, rx) = tokio::sync::mpsc::channel::<>(24); let mut b = bus.connect().await.unwrap(); let c = cache.clone(); let data_tx = data_tx.clone(); tokio::spawn(async move { while tls_server.accept(handle_tls, HttpServiceArgs { tx:data_tx, cache:c.clone(), }).await.is_ok() { } }); } Err(_) => { println!("error: failed to bind TLS port 38612."); } } } Err(_) => { println!("error: failed to initialize HTTPS service."); } } loop { tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; } }