use std::{ collections::{hash_map::Entry, HashMap}, env, fs::File, io::{prelude::*, BufReader}, net::{TcpListener, TcpStream}, }; use maud::html; use percent_encoding::percent_decode_str; use pukram2html::{convert, convert_extended, convert_subheader, Settings}; mod request; use request::Request; fn main() { let mut context = Context::default(); 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(stream); } } fn fail(mut stream: TcpStream) { let _ = write!(stream, "HTTP/1.1 404 Not Found\r\n\r\n"); let _ = writeln!(stream, "Page not found!"); } struct Comment { name: Box, text: Box, } #[derive(Default)] struct SiteInfo { comments: Vec, visits: usize, } #[derive(Default)] struct Context { infos: HashMap, SiteInfo>, } impl Context { fn handle_connection(&mut self, mut stream: TcpStream) { 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 Ok(current_dir) = env::current_dir() else { fail(stream); return; }; let mut path = current_dir; let (relative_path, _) = request .path .split_once('?') .unwrap_or((&request.path, Default::default())); if relative_path.contains("//") { fail(stream); return; } let relative_path = if relative_path.is_empty() { String::new() } else { let path = percent_decode_str(&relative_path[1..]).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; } path.to_string() }; let (pk_file, pki_file, start_level) = if !relative_path.is_empty() { path.push(&relative_path); let (pk_extension, pki_extension) = if let Some(extension) = path.extension() { (format!("{extension:?}.pk"), format!("{extension:?}.pki")) } else { ("pk".to_string(), "pki".to_string()) }; let pk_path = path.with_extension(pk_extension); let pki_path = path.with_extension(pki_extension); let (Ok(pk_file), Ok(pki_file)) = (File::open(pk_path), File::open(pki_path)) else { fail(stream); return; }; (pk_file, Some(pki_file), 1) } else { let mut pk_path = path.clone(); pk_path.push("index.pk"); let Ok(pk_file) = File::open(pk_path) else { fail(stream); return; }; (pk_file, None, 0) }; use Entry::*; let info = match self.infos.entry(relative_path.clone().into_boxed_str()) { Occupied(o) => o.into_mut(), Vacant(v) => v.insert(SiteInfo::default()), }; let mut entries = request.body.split('&'); if let (Some(name_entry), Some(text_entry), None) = (entries.next(), entries.next(), entries.next()) { if let (Some(("name", input_name)), Some(("text", input_text))) = (name_entry.split_once('='), text_entry.split_once('=')) { let name_value = input_name.replace('+', " "); let decoded_name = percent_decode_str(&name_value).decode_utf8_lossy(); eprintln!("Received comment by \"{decoded_name}\":"); let name = decoded_name .replace('&', "&") .replace('<', "<") .replace('>', ">") .into(); let text_value = input_text.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); let text = std::str::from_utf8(text_buf.as_slice()) .unwrap_or_default() .into(); info.comments.push(Comment { name, text }); } } info.visits += 1; let _ = write!(stream, "HTTP/1.1 200 OK\r\n\r\n"); let _ = writeln!(stream, ""); let _ = writeln!(stream, "

👁️{}

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

{title}

"); } let path_ref = &path; let handle_entry = |entry: &str, output: &mut TcpStream, level: usize| { let level = level + 1; let mut path = path_ref.clone(); let entry = if let Some((entry, _)) = entry.split_once(':') { entry } else { entry }; path.push(entry); let extension = if let Some(extension) = path.extension() { format!("{extension:?}.pki") } else { "pki".to_string() }; path.set_extension(extension); 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, ); }; 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), ); let parent_path = relative_path .rsplit_once('/') .map(|(path, _)| path) .unwrap_or_default(); let _ = writeln!(stream, "
"); let _ = writeln!(stream, "<< Back"); if let Some(pki_file) = pki_file { 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"); } 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!"; } }; 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"); } }