From c96ef6021440b886d8cf76dbd3ff38c19db0e23d Mon Sep 17 00:00:00 2001 From: p11 Date: Sun, 16 Jul 2023 10:15:15 +0200 Subject: [PATCH] Initial pukram server --- .gitignore | 1 + Cargo.lock | 124 ++++++++++++++++++++++++++ Cargo.toml | 9 ++ src/main.rs | 232 +++++++++++++++++++++++++++++++++++++++++++++++++ src/request.rs | 60 +++++++++++++ 5 files changed, 426 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/main.rs create mode 100644 src/request.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..98f9ca4 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,124 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "formatting" +version = "0.1.0" +source = "git+https://gitlab.com/porky11/formatting#7edec9731c042bd2cdd7f3a1674cb2e1b0303e42" + +[[package]] +name = "itoa" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" + +[[package]] +name = "maud" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0bab19cef8a7fe1c18a43e881793bfc9d4ea984befec3ae5bd0415abf3ecf00" +dependencies = [ + "itoa", + "maud_macros", +] + +[[package]] +name = "maud_macros" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be95d66c3024ffce639216058e5bae17a83ecaf266ffc6e4d060ad447c9eed2" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pukram-server" +version = "0.1.0" +dependencies = [ + "maud", + "percent-encoding", + "pukram2html", +] + +[[package]] +name = "pukram2html" +version = "0.1.0" +source = "git+https://gitlab.com/porky11/pukram2html#8845a8f101cb302856443a01dee615db107e6ef9" +dependencies = [ + "formatting", +] + +[[package]] +name = "quote" +version = "1.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..59f9ee2 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "pukram-server" +version = "0.1.0" +edition = "2021" + +[dependencies] +percent-encoding = "2.3" +maud = "0.25.0" +pukram2html = { git = "https://gitlab.com/porky11/pukram2html" } diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..922477e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,232 @@ +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 port = args.next().unwrap_or("8080".to_string()); + + let listener = TcpListener::bind(format!("127.0.0.1:{port}")).unwrap(); + + for stream in listener.incoming() { + let stream = stream.unwrap(); + + context.handle_connection(stream); + } +} + +fn fail(mut stream: TcpStream) { + let _ = write!(stream, "HTTP/1.1 404 Not Fonud\r\n\r\n"); + let _ = writeln!(stream, "Page not found!"); +} + +struct Comment { + name: String, + text: String, +} + +#[derive(Default)] +struct Context { + comments: HashMap>, +} + +impl Context { + fn handle_connection(&mut self, mut stream: TcpStream) { + let Some(request) = Request::from(&stream) else { + return; + }; + + 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 { + percent_decode_str(&relative_path[1..]) + .decode_utf8_lossy() + .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 local_comments = match self.comments.entry(relative_path.to_string()) { + Occupied(o) => o.into_mut(), + Vacant(v) => v.insert(Vec::new()), + }; + + 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(); + let name = decoded_name + .replace('&', "&") + .replace('<', "<") + .replace('>', ">"); + + let text_value = input_text.replace('+', " "); + let decoded_text = percent_decode_str(&text_value).decode_utf8_lossy(); + let mut text_buf = Vec::new(); + convert(decoded_text.lines(), &mut text_buf); + let text = std::str::from_utf8(text_buf.as_slice()) + .unwrap() + .to_string(); + + local_comments.push(Comment { name, text }); + } + } + + let _ = write!(stream, "HTTP/1.1 200 OK\r\n\r\n"); + + let title = relative_path + .rsplit_once('/') + .map(|(_, title)| title) + .unwrap_or(&relative_path); + if !title.is_empty() { + let _ = write!(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 local_comments { + let _ = writeln!(stream, "
{name}{text}
"); + } + + let _ = writeln!(stream, "
"); + let _ = writeln!(stream, "<< Back"); + } +} diff --git a/src/request.rs b/src/request.rs new file mode 100644 index 0000000..a3f6ca7 --- /dev/null +++ b/src/request.rs @@ -0,0 +1,60 @@ +use std::io::Read; +use std::net::TcpStream; + +pub struct Request { + pub method: String, + pub path: String, + pub version: String, + pub headers: Vec, + pub body: String, +} + +impl Request { + pub fn from(mut stream: &TcpStream) -> Option { + let mut buffer = [0; 16384]; + let _ = stream.read(&mut buffer).ok()?; + + let mut buffer_v = Vec::from_iter(buffer.iter().copied()); + + buffer_v.pop(); + let raw_req = std::str::from_utf8(&buffer_v).ok()?.to_string(); + let mut request_lines: Vec<&str> = raw_req.split("\r\n").collect(); + + let mut initial_line = request_lines[0].split(' '); + + let method = initial_line.next().unwrap_or_default().to_string(); + let path = initial_line.next().unwrap_or_default().to_string(); + + let version = if let Some(version) = initial_line.next() { + version + .split_once('/') + .map(|(_, version)| version) + .unwrap_or("1.1") + } else { + "1.1" + } + .to_string(); + + let body = request_lines + .pop() + .unwrap_or_default() + .split_once('\0') + .map(|(body, _)| body) + .unwrap_or_default() + .to_string(); + + request_lines.pop(); + + let mut request_lines = request_lines.into_iter(); + request_lines.next(); + let headers: Vec = request_lines.map(|header| header.to_string()).collect(); + + Some(Request { + method, + path, + version, + body, + headers, + }) + } +}