use std::{ collections::{hash_map::Entry, HashMap}, env, fs::File, io::{prelude::*, BufReader, Error, ErrorKind, Result}, net::{TcpListener, TcpStream}, path::{Path, PathBuf}, sync::{Arc, Mutex}, thread, time::Duration, }; use data_stream::{ collections::SizeSettings, default_settings::PortableSettings, from_stream, to_stream, FromStream, ToStream, }; use maud::html; use percent_encoding::percent_decode_str; use pukram2html::{convert, convert_extended, convert_subheader, Settings}; use threadpool::ThreadPool; mod request; use request::Request; fn main() { let Ok(path) = env::current_dir() else { eprintln!("Current directory does not exist!"); return; }; let mut args = env::args(); args.next(); let address = args.next().unwrap_or_else(|| "127.0.0.1:8080".to_string()); let password = args.next(); let partial_password = args.next(); start_server(path, &address, password, partial_password); } fn start_server( path: PathBuf, address: &str, password: Option, partial_password: Option, ) { let listener = TcpListener::bind(address).expect("Invalid bind address!"); eprintln!("Strated server!"); let context: Arc> = Arc::default(); let mut pool = ThreadPool::new(4); 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(); let hidden_password = partial_password.clone(); pool.execute(move || { handle_connection( context, path, stream, password.as_deref(), hidden_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!"); } #[derive(Clone)] struct Comment { name: Box, text: Box, } #[derive(Default, Clone)] struct SiteInfo { comments: Vec, visits: usize, up: usize, down: usize, } impl ToStream for SiteInfo { fn to_stream(&self, stream: &mut W) -> Result<()> { S::size_to_stream(self.comments.len(), stream)?; for Comment { name, text } in &self.comments { let name_bytes = name.as_bytes(); S::size_to_stream(name_bytes.len(), stream)?; stream.write_all(name_bytes)?; let text_bytes = text.as_bytes(); S::size_to_stream(text_bytes.len(), stream)?; stream.write_all(text_bytes)?; } S::size_to_stream(self.visits, stream)?; S::size_to_stream(self.up, stream)?; S::size_to_stream(self.down, stream)?; Ok(()) } } impl FromStream for SiteInfo { fn from_stream(stream: &mut R) -> Result { let size = S::size_from_stream(stream)?; let comments = (0..size) .map(|_| { let name_bytes = as FromStream>::from_stream(stream)?; let name = std::str::from_utf8(&name_bytes) .map_err(|e| Error::new(ErrorKind::InvalidData, e))? .into(); let text_bytes = as FromStream>::from_stream(stream)?; let text = std::str::from_utf8(&text_bytes) .map_err(|e| Error::new(ErrorKind::InvalidData, e))? .into(); Ok(Comment { name, text }) }) .collect::>>()?; let visits = S::size_from_stream(stream)?; let up = S::size_from_stream(stream)?; let down = S::size_from_stream(stream)?; Ok(SiteInfo { comments, visits, up, down, }) } } #[derive(Default)] struct Context { infos: HashMap, Arc>>, } fn handle_connection( context: Arc>, path: PathBuf, mut stream: TcpStream, password: Option<&str>, partial_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, Partial, 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); } else if Some(input) == partial_password { access = Access::Partial; cookie = partial_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; } else if Some(state) == partial_password { access = Access::Partial; 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((&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.to_path_buf(); let mut data_path = path.to_path_buf(); let (pki_path, audio_path, start_level, relative_path, partial) = 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, 0, String::new(), None) } else { let mut pki_path = path.to_path_buf(); let mut audio_path = path.to_path_buf(); let path = percent_decode_str(relative_path).decode_utf8_lossy(); if path.contains('_') { let path = path.replace('_', " "); let _ = write!(stream, "HTTP/1.1 308 Permanent Redirect\r\n"); let _ = write!(stream, "Location: /{path}\r\n\r\n"); return; } let (path, num) = if let Some((path, num)) = path.rsplit_once('=') { (path, num.parse::().ok()) } else { (path.as_ref(), None) }; pk_path.push(format!("{path}.pk")); 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), 1, path.to_string(), num) }; let file_paths = DocumentPaths { pk: &pk_path, pki: pki_path.as_ref().map(|path| path.as_ref()), audio: audio_path.as_ref().map(|path| path.as_ref()), data: &data_path, }; if let Some((_, ending)) = relative_path.rsplit_once('.') { let path = path.to_path_buf(); 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 { if !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(Mutex::new( File::open(&data_path) .map(|mut file| { from_stream::(&mut file).unwrap_or_default() }) .unwrap_or_default(), ))) .clone(), } } else { return; }; let path = path.to_path_buf(); handle_relative_connection( info, stream, &request.body, &relative_path, &path, file_paths, partial, start_level, 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]); } } #[derive(Copy, Clone)] struct DocumentPaths<'a> { pk: &'a Path, pki: Option<&'a Path>, audio: Option<&'a Path>, data: &'a Path, } fn handle_relative_connection( info: Arc>, mut stream: TcpStream, body: &str, relative_path: &str, path: &Path, file_paths: DocumentPaths, partial: Option, start_level: usize, cookie: Option<&str>, censored: bool, ) { let mut name = None; let mut text = None; let mut up = false; let mut down = false; 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}\":"); 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); text = Some( std::str::from_utf8(text_buf.as_slice()) .unwrap_or_default() .into(), ); } "up" => up = true, "down" => down = true, _ => (), } } let info = if let Ok(mut info) = info.lock() { if let (Some(name), Some(text)) = (name, text) { info.comments.push(Comment { name, text }); } if up { info.up += 1; } if down { info.down += 1; } info.visits += 1; info.clone() } else { return; }; if let Ok(mut file) = File::create(file_paths.data) { if to_stream::(&info, &mut file).is_err() { eprintln!("Error saving data!"); eprintln!(); } } 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 section = |stream: &mut TcpStream| { let _ = writeln!(stream, "
"); let _ = writeln!(stream, "<< Back"); }; let _ = writeln!( stream, "

👁️{} 💖️{} 💔️{}

", info.visits, info.up, info.down ); section(&mut stream); let title = relative_path .rsplit_once('/') .map(|(_, title)| title) .unwrap_or(relative_path); let handle_entry = |mut entry: &str, output: &mut TcpStream, level: usize| { let level = level + 1; let mut pki_path = path.to_path_buf(); let mut audio_path = path.to_path_buf(); if let Some((real_entry, _)) = entry.split_once(':') { entry = real_entry } let pki_extension = if censored { "pkc" } else { "pki" }; if relative_path.is_empty() { pki_path.push(format!("{entry}.{pki_extension}")); audio_path.push(format!("{entry}.mp3")); } else { pki_path.push(format!("{relative_path}/{entry}.{pki_extension}")); audio_path.push(format!("{relative_path}/{entry}.mp3")); }; let Ok(file) = File::open(pki_path) else { return; }; let _ = writeln!( output, "{entry}" ); if Path::is_file(&audio_path) { let _ = writeln!( output, "

" ); } convert_subheader( BufReader::new(file) .lines() .map(|line| line.unwrap_or_default()), output, level, ); }; if let Ok(pk_file) = File::open(file_paths.pk) { let lines = BufReader::new(pk_file).lines(); if let Some(i) = partial { let mut last_empty = true; let mut block = 0; let lines: Vec<_> = lines.map(|line| line.unwrap_or_default()).collect(); let mut lines = lines.into_iter(); if i > 0 { for line in lines.by_ref() { let empty = line.trim().is_empty(); if empty == last_empty { continue; } if empty { block += 1; if block == i { break; } } last_empty = empty; } } for line in lines.clone() { let empty = line.trim().is_empty(); if empty == last_empty { continue; } if empty { block += 1; } last_empty = empty; } let _ = writeln!(stream, "

{title} ({}/{})

", i + 1, block); let _ = writeln!( stream, "< Stop >

" ); if i > 0 { let prev = i - 1; let _ = writeln!(stream, "< Prev"); } else { let _ = writeln!(stream, "< Prev"); } if i + 1 < block { let next = i + 1; let _ = writeln!(stream, "Next >"); } else { let _ = writeln!(stream, "Next >"); } let _ = writeln!(stream, "
"); let mut has_text = false; convert_extended( lines.take_while(|line| { let empty = line.trim().is_empty(); if !empty { has_text = true; } !empty || !has_text }), &mut stream, Settings::default() .with_handler(handle_entry) .with_start_level(start_level) .with_use_textboxes(true), ); } else { if !title.is_empty() { let _ = writeln!(stream, "

{title}

"); } let _ = writeln!(stream, "< Play >"); convert_extended( lines.map(|line| line.unwrap_or_default()), &mut stream, Settings::default() .with_handler(handle_entry) .with_start_level(start_level) .with_use_textboxes(true), ); } } else { unreachable!(); } section(&mut stream); if let Some(pki_path) = file_paths.pki { if let Ok(pki_file) = File::open(pki_path) { let _ = writeln!(stream, "

Description

"); if let Some(audio_path) = &file_paths.audio { if Path::is_file(audio_path) { let _ = writeln!( stream, "

" ); } } let lines = BufReader::new(pki_file).lines(); convert_subheader(lines.map(|line| line.unwrap_or_default()), &mut stream, 1); section(&mut stream); } else { unreachable!(); } } let html = html! { h1 { "Comments" } form method="POST" { input type="text" name="name" value="anon" placeholder="Name"; br; textarea rows="5" cols="60" name="text" placeholder="Enter comment..." {} br; input type="submit" value="Send!"; } form method="POST" { input type="submit" value="💖️" name="up"; input type="submit" value="💔️" name="down"; } }; let _ = stream.write_all(html.into_string().as_bytes()); for Comment { name, text } in &info.comments { let _ = writeln!(stream, "
{name}{text}
"); } section(&mut stream); }