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}, }; 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 context = Context::default(); let pool = ThreadPool::new(4); let mut args = env::args(); args.next(); let address = args.next().unwrap_or("127.0.0.1:8080".to_string()); let listener = TcpListener::bind(address).expect("Invalid bind address!"); eprintln!("Strated server!"); for stream in listener.incoming() { eprintln!("New connection!"); let Ok(stream) = stream else { eprintln!("Connection failed!"); continue; }; context.handle_connection(&path, stream, &pool); } } 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>>, } impl Context { fn handle_connection(&mut self, path: &Path, mut stream: TcpStream, pool: &ThreadPool) { 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!(); 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, start_level, relative_path) = if relative_path.is_empty() { pk_path.push("index.pk"); data_path.push("index.dat"); (None, 0, String::new()) } else { let mut pki_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; } pk_path.push(format!("{path}.pk")); pki_path.push(format!("{path}.pki")); data_path.push(format!("{path}.dat")); (Some(pki_path), 1, path.to_string()) }; if relative_path.split('/').any(|name| name == "Images") { let path = path.to_path_buf(); pool.execute(move || reply_image(stream, &relative_path, path)); 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; } } use Entry::*; let info = match self.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(), }; let path = path.to_path_buf(); pool.execute(move || { handle_relative_connection( info, stream, &request.body, &relative_path, &path, &pk_path, pki_path.as_ref().map(|path| path.as_ref()), &data_path, start_level, ) }); } } fn reply_image(mut stream: TcpStream, relative_path: &str, mut path: PathBuf) { let Some((_, ending)) = relative_path.rsplit_once('.') else { fail(stream); return; }; let ending = ending.to_lowercase(); let image_type = match ending.as_ref() { "jpg" => "jpeg", "png" | "tiff" | "gif" | "jpeg" => &ending, _ => { fail(stream); return; } }; 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: image/{image_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 handle_relative_connection( info: Arc>, mut stream: TcpStream, body: &str, relative_path: &str, path: &Path, pk_path: &Path, pki_path: Option<&Path>, data_path: &Path, start_level: usize, ) { 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(data_path) { 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"); let _ = write!(stream, "\r\n"); let _ = writeln!( stream, "

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

", info.visits, info.up, info.down ); let title = relative_path .rsplit_once('/') .map(|(_, title)| title) .unwrap_or(relative_path); if !title.is_empty() { let _ = writeln!(stream, "

{title}

"); } let handle_entry = |mut entry: &str, output: &mut TcpStream, level: usize| { let level = level + 1; let mut path = path.to_path_buf(); if let Some((real_entry, _)) = entry.split_once(':') { entry = real_entry } path.push(if relative_path.is_empty() { format!("{entry}.pki") } else { format!("{relative_path}/{entry}.pki") }); let Ok(file) = File::open(path) else { return; }; let _ = writeln!( output, "{entry}" ); convert_subheader( BufReader::new(file) .lines() .map(|line| line.unwrap_or_default()), output, level, ); }; if let Ok(pk_file) = File::open(pk_path) { convert_extended( BufReader::new(pk_file) .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!(); } let parent_path = relative_path .rsplit_once('/') .map(|(path, _)| path) .unwrap_or_default(); let _ = writeln!(stream, "
"); let _ = writeln!(stream, "<< Back"); if let Some(pki_path) = pki_path { if let Ok(pki_file) = File::open(pki_path) { let _ = writeln!(stream, "

Description

"); convert_subheader( BufReader::new(pki_file) .lines() .map(|line| line.unwrap_or_default()), &mut stream, 1, ); let _ = writeln!(stream, "
"); let _ = writeln!(stream, "<< Back"); } 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}
"); } let _ = writeln!(stream, "
"); let _ = writeln!(stream, "<< Back"); }