use std::{ collections::{HashMap, hash_map::Entry}, env, fs::File, io::{BufReader, prelude::*}, net::{TcpListener, TcpStream}, path::{Path, PathBuf}, process::ExitCode, sync::{ Arc, Mutex, atomic::{AtomicUsize, Ordering}, }, thread, time::Duration, }; use ::chara::CharacterDefinition; use comment_system::{Comment, SiteInfo}; use data_stream::{default_settings::PortableSettings, from_stream, to_stream}; use header_config::parse_config; use maud::html; use percent_encoding::percent_decode_str; use pukram2html::convert; use threadpool::ThreadPool; mod chara; mod dialog; mod request; mod tabs; mod vn; use request::Request; use tabs::{Tab, TabInfo, write_tab_styles}; #[derive(Default)] struct SharedSiteInfo { comments: Mutex>, visits: AtomicUsize, up: AtomicUsize, down: AtomicUsize, } impl SharedSiteInfo { fn new(info: SiteInfo) -> Self { let SiteInfo { comments, visits, up, down, } = info; Self { comments: Mutex::new(comments), visits: visits.into(), up: up.into(), down: down.into(), } } fn update( self: &Arc, name: Option>, text: Option>, up: bool, down: bool, ) -> SiteInfo { SiteInfo { comments: { if let Ok(mut comments) = self.comments.lock() { if let (Some(name), Some(text)) = (name, text) { comments.push(Comment { name, text }); } comments.clone() } else { Vec::new() } }, visits: self.visits.fetch_add(1, Ordering::Relaxed) + 1, up: if up { self.up.fetch_add(1, Ordering::Relaxed) + 1 } else { self.up.load(Ordering::Relaxed) }, down: if down { self.down.fetch_add(1, Ordering::Relaxed) + 1 } else { self.down.load(Ordering::Relaxed) }, } } } #[derive(Default)] struct Context { infos: HashMap, Arc>, } #[derive(Copy, Clone)] struct DocumentPaths<'a> { pk: &'a Path, mld: &'a Path, pki: Option<&'a Path>, audio: Option<&'a Path>, data: &'a Path, } fn main() -> ExitCode { let Ok(path) = env::current_dir() else { eprintln!("Current directory does not exist!"); return ExitCode::FAILURE; }; let mut args = env::args(); args.next(); let address = args.next(); let address = address.as_deref().unwrap_or("127.0.0.1:8080"); let password = args.next(); start_server(path, address, password); ExitCode::FAILURE } fn get_thread_pool_size() -> usize { std::env::var("THREAD_POOL") .map(|s| s.parse().unwrap_or(4)) .unwrap_or(4) } fn start_server(path: PathBuf, address: &str, password: Option) { let Ok(listener) = TcpListener::bind(address) else { eprintln!("Invalid bind address {address:?}!"); return; }; eprintln!("Started server on {address}"); let context = Arc::new(Mutex::new(Context::default())); let mut pool = ThreadPool::new(get_thread_pool_size()); for stream in listener.incoming() { eprintln!("New connection!"); let Ok(stream) = stream else { eprintln!("Connection failed!"); continue; }; if pool.active_count() == pool.max_count() { thread::sleep(Duration::from_secs(1)); if pool.active_count() == pool.max_count() { pool = ThreadPool::new(pool.max_count()) } } let context = context.clone(); let path = path.clone(); let password = password.clone(); pool.execute(move || handle_connection(context, path, stream, password.as_deref())); } } fn fail(mut stream: TcpStream) { let _ = write!(stream, "HTTP/1.1 404 Not Found\r\n\r\n"); let _ = writeln!(stream, "Page not found!"); } fn handle_connection( context: Arc>, path: PathBuf, mut stream: TcpStream, password: Option<&str>, ) { let Some(request) = Request::from(&stream) else { eprintln!("Invalid request!"); return; }; eprintln!(); eprintln!("Request:"); eprintln!("- Method: {}", request.method); eprintln!("- Path: {}", request.path); eprintln!("- Version: {}", request.version); eprintln!("- Headers:"); for header in &request.headers { eprintln!(" - {header}"); } eprintln!("- Body: {}", request.body); eprintln!(); #[derive(PartialEq, Eq)] enum Access { None, Full, } let mut access = Access::None; let mut cookie = None; if let Some(password) = password { for entry in request.body.split('&') { let Some((key, input)) = entry.split_once('=') else { continue; }; if key == "password" { if input == password { access = Access::Full; cookie = Some(password); } break; } } if access == Access::None { for header in request.headers { let Some((key, values)) = header.split_once(':') else { continue; }; let key = key.trim(); if key.to_lowercase() != "cookie" { continue; } for cookie in values.split(',') { let cookie = cookie.trim(); let Some((name, state)) = cookie.split_once('=') else { continue; }; if name != "password" { continue; } if state == password { access = Access::Full; break; } } } } if access == Access::None { let _ = write!(stream, "HTTP/1.1 200 OK\r\n"); let _ = write!(stream, "Content-Type: text/html; charset=\"utf-8\"\r\n"); let _ = write!(stream, "\r\n"); let html = html! { h1 { "Authentification required" } form method="POST" { input type="password" name="password" placeholder="Password"; br; input type="submit" value="Authenticate!"; } }; let _ = stream.write_all(html.into_string().as_bytes()); return; } } else { access = Access::Full; } let (mut relative_path, _) = request .path .split_once('?') .unwrap_or_else(|| (&request.path, Default::default())); if relative_path.contains("//") { fail(stream); return; } if let Some(path) = relative_path.strip_prefix('/') { relative_path = path; } let mut pk_path = path.clone(); let mut mld_path = path.clone(); let mut data_path = path.clone(); let (pki_path, audio_path, relative_path) = if relative_path.is_empty() { if access == Access::Full { pk_path.push("index.pk"); data_path.push("index.dat"); } else { pk_path.push("partial.pk"); data_path.push("partial.dat"); } (None, None, String::new()) } else { let mut pki_path = path.clone(); let mut audio_path = path.clone(); let path = percent_decode_str(relative_path).decode_utf8_lossy(); { let (mut path, mut file) = path.rsplit_once('/').unwrap_or(("", relative_path)); let mut redirect = false; let replaced_path; if path.contains('_') { replaced_path = path.replace('_', " "); path = &replaced_path; redirect = true; } let replaced_file; if file.contains('_') && !file.contains('.') { replaced_file = file.replace('_', " "); file = &replaced_file; redirect = true; } if redirect { let _ = write!(stream, "HTTP/1.1 308 Permanent Redirect\r\n"); if path.is_empty() { let _ = write!(stream, "Location: /{file}\r\n\r\n"); } else { let _ = write!(stream, "Location: /{path}/{file}\r\n\r\n"); } return; } } pk_path.push(format!("{path}.pk")); mld_path.push(format!("{path}.mld")); if access == Access::Full { pki_path.push(format!("{path}.pki")); } else { pki_path.push(format!("{path}.pkc")); } audio_path.push(format!("{path}.mp3")); data_path.push(format!("{path}.dat")); (Some(pki_path), Some(audio_path), path.to_string()) }; let file_paths = DocumentPaths { pk: &pk_path, mld: &mld_path, pki: pki_path.as_ref().map(PathBuf::as_ref), audio: audio_path.as_ref().map(PathBuf::as_ref), data: &data_path, }; if let Some((_, ending)) = relative_path.rsplit_once('.') { let ending = ending.to_lowercase(); match ending.as_ref() { "jpg" => reply_binary(stream, &relative_path, "image", "jpeg", path), "png" | "tiff" | "gif" | "jpeg" => { reply_binary(stream, &relative_path, "image", &ending, path) } "mp3" | "wav" => reply_binary(stream, &relative_path, "audio", &ending, path), _ => fail(stream), } return; } if !Path::is_file(&pk_path) { fail(stream); return; } if let Some(pki_path) = &pki_path && !Path::is_file(pki_path) { fail(stream); return; } let info = if let Ok(mut context) = context.lock() { use Entry::*; match context.infos.entry(relative_path.clone().into_boxed_str()) { Occupied(o) => o.get().clone(), Vacant(v) => v .insert(Arc::new( File::open(&data_path) .map(|mut file| { SharedSiteInfo::new( from_stream::(&mut file) .unwrap_or_default(), ) }) .unwrap_or_default(), )) .clone(), } } else { return; }; handle_relative_connection( info, stream, &request.body, &relative_path, &path, file_paths, cookie, access != Access::Full, ) } fn reply_binary( mut stream: TcpStream, relative_path: &str, data_type: &str, file_type: &str, mut path: PathBuf, ) { path.push(relative_path); let Ok(mut file) = File::open(path) else { fail(stream); return; }; let _ = write!(stream, "HTTP/1.1 200 OK\r\n"); let _ = write!(stream, "Content-Type: {data_type}/{file_type}\r\n"); let _ = write!(stream, "\r\n"); let mut buf = [0; 0x400]; while let Ok(len) = file.read(&mut buf) { if len == 0 { break; } let _ = stream.write_all(&buf[0..len]); } } fn load_character_file(path: &Path) -> Option { std::fs::read_to_string(path) .ok() .map(|content| CharacterDefinition::parse(&content)) } struct RequestData<'a> { name: Option>, text: Option>, up: bool, down: bool, choice: usize, progress: &'a str, } impl<'a> RequestData<'a> { fn parse(body: &'a str) -> Self { let mut data = Self { name: None, text: None, up: false, down: false, choice: 0, progress: "", }; for entry in body.split('&') { let Some((key, input)) = entry.split_once('=') else { continue; }; match key { "name" => { let name_value = input.replace('+', " "); let decoded_name = percent_decode_str(&name_value).decode_utf8_lossy(); eprintln!("Received comment by \"{decoded_name}\":"); data.name = Some( decoded_name .replace('&', "&") .replace('<', "<") .replace('>', ">") .into(), ); } "text" => { let text_value = input.replace('+', " "); let decoded_text = percent_decode_str(&text_value).decode_utf8_lossy(); for line in decoded_text.lines() { eprintln!(" {line}"); } eprintln!(); let mut text_buf = Vec::new(); convert(decoded_text.lines(), &mut text_buf); data.text = Some( std::str::from_utf8(text_buf.as_slice()) .unwrap_or_default() .into(), ); } "up" => data.up = true, "down" => data.down = true, "choice" => data.choice = input.parse().unwrap_or_default(), "progress" => data.progress = input, _ => (), } } data } } fn handle_relative_connection( info: Arc, mut stream: TcpStream, body: &str, relative_path: &str, path: &Path, file_paths: DocumentPaths, cookie: Option<&str>, censored: bool, ) { let RequestData { name, text, up, down, choice, progress, } = RequestData::parse(body); let info = info.update(name, text, up, down); if let Ok(mut file) = File::create(file_paths.data) && to_stream::(&info, &mut file).is_err() { eprintln!("Error saving data!"); eprintln!(); } let SiteInfo { comments, visits, up, down, } = info; let _ = write!(stream, "HTTP/1.1 200 OK\r\n"); let _ = write!(stream, "Content-Type: text/html; charset=\"utf-8\"\r\n"); if let Some(password) = cookie { let _ = write!(stream, "Set-Cookie: password={password}\r\n"); } let _ = write!(stream, "\r\n"); let parent_path = relative_path .rsplit_once('/') .map(|(path, _)| path) .unwrap_or_default(); let title = relative_path .rsplit_once('/') .map_or(relative_path, |(_, title)| title); let back = !title.is_empty(); let section = |stream: &mut TcpStream| { let _ = writeln!(stream, "
"); if back { let _ = writeln!(stream, "<< Back"); } }; let _ = writeln!(stream, "

👁️{visits} 💖️{up} 💔️{down}

"); section(&mut stream); if back { let _ = writeln!(stream, "

{title}

"); } let mut sections = Vec::new(); let check_path: &Path = relative_path.as_ref(); let config_map = check_path .parent() .map(|parent| parse_config(&parent.with_extension("vng")).ok()) .unwrap_or_default(); let mlc_path: Option = check_path .parent() .map(|parent| parent.with_extension("mlc")); if let Some(config_map) = config_map { sections.push(Tab { title: "Game".into(), info: TabInfo::Game(config_map), }); } else { let Ok(pk_file) = File::open(file_paths.pk) else { unreachable!(); }; let mut current_title: Box = "".into(); let mut current_lines = Vec::new(); let mut chara = false; for line in BufReader::new(pk_file).lines() { let Ok(line) = line else { continue; }; if let Some(title) = line.strip_prefix("# ") { if !current_title.is_empty() { sections.push(Tab { title: current_title, info: if chara { TabInfo::Chara(current_lines) } else { TabInfo::Lines(current_lines) }, }) } chara = title == "Characters"; current_title = title.into(); current_lines = vec![line]; continue; } if chara && line.starts_with('#') { let mut count = 0; for c in line.chars() { if c == '#' { count += 1; continue; } break; } current_lines.push(line.clone()); current_lines.push(format!("+ {}", line[count..].trim())); } else { current_lines.push(line); } } sections.push(Tab { title: current_title, info: TabInfo::Lines(current_lines), }) } if file_paths.pki.is_some() { sections.push(Tab { title: "Description".into(), info: TabInfo::Description, }); } sections.push(Tab { title: "Comments".into(), info: TabInfo::Comment(comments), }); let count = sections.len(); write_tab_styles(&mut stream, count); let _ = write!(stream, r#"
"#); let _ = write!( stream, r#""# ); for i in 2..=count { let _ = write!( stream, r#""# ); } let _ = write!(stream, r#"
"#); for (i, Tab { title, .. }) in sections.iter().enumerate() { let index = i + 1; let _ = write!( stream, r#""# ); } let _ = write!(stream, "
"); for (i, Tab { info, .. }) in sections.into_iter().enumerate() { let index = i + 1; let _ = write!(stream, r#"
"#); info.render_tab_content( &mut stream, path, relative_path, censored, choice, progress, mlc_path.as_deref(), &file_paths, ); let _ = write!(stream, "
"); } let _ = write!(stream, "
"); section(&mut stream); }