pukram-server/src/main.rs

377 lines
12 KiB
Rust

use std::{
collections::{hash_map::Entry, HashMap},
env,
fs::File,
io::{prelude::*, BufReader, Error, ErrorKind, Result},
net::{TcpListener, TcpStream},
};
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};
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<str>,
text: Box<str>,
}
#[derive(Default)]
struct SiteInfo {
comments: Vec<Comment>,
visits: usize,
up: usize,
down: usize,
}
impl<S: SizeSettings> ToStream<S> for SiteInfo {
fn to_stream<W: Write>(&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<S: SizeSettings> FromStream<S> for SiteInfo {
fn from_stream<R: Read>(stream: &mut R) -> Result<Self> {
let size = S::size_from_stream(stream)?;
let comments = (0..size)
.map(|_| {
let name_bytes = <Vec<_> as FromStream<S>>::from_stream(stream)?;
let name = std::str::from_utf8(&name_bytes)
.map_err(|e| Error::new(ErrorKind::InvalidData, e))?
.into();
let text_bytes = <Vec<_> as FromStream<S>>::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::<Result<Vec<_>>>()?;
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<Box<str>, 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, data_path, start_level) = if !relative_path.is_empty() {
path.push(&relative_path);
let (pk_extension, pki_extension, data_extension) =
if let Some(extension) = path.extension() {
(
format!("{extension:?}.pk"),
format!("{extension:?}.pki"),
format!("{extension:?}.dat"),
)
} else {
("pk".to_string(), "pki".to_string(), "dat".to_string())
};
let pk_path = path.with_extension(pk_extension);
let pki_path = path.with_extension(pki_extension);
let data_path = path.with_extension(data_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), data_path, 1)
} else {
let mut pk_path = path.clone();
pk_path.push("index.pk");
let mut data_path = path.clone();
data_path.push("index.dat");
let Ok(pk_file) = File::open(pk_path) else {
fail(stream);
return;
};
(pk_file, None, data_path, 0)
};
use Entry::*;
let info = match self.infos.entry(relative_path.clone().into_boxed_str()) {
Occupied(o) => o.into_mut(),
Vacant(v) => v.insert(
File::open(&data_path)
.map(|mut file| {
from_stream::<PortableSettings, _, _>(&mut file).unwrap_or_default()
})
.unwrap_or_default(),
),
};
let mut name = None;
let mut text = None;
for entry in request.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('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.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" => info.up += 1,
"down" => info.down += 1,
_ => (),
}
}
if let (Some(name), Some(text)) = (name, text) {
info.comments.push(Comment { name, text });
}
info.visits += 1;
if let Ok(mut file) = File::create(data_path) {
if to_stream::<PortableSettings, _, _>(info, &mut file).is_err() {
eprintln!("Error saving data!");
eprintln!();
}
}
let _ = write!(stream, "HTTP/1.1 200 OK\r\n\r\n");
let _ = writeln!(stream, "<meta charset=\"utf-8\">");
let _ = writeln!(
stream,
"<p>👁️{} 💖️{} 💔️{}</p>",
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, "<h1>{title}</h1>");
}
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,
"<h{level}><a href=\"{relative_path}/{entry}\">{entry}</a></h{level}>"
);
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, "<hr>");
let _ = writeln!(stream, "<a href=\"/{parent_path}\">&lt;&lt; Back</a>");
if let Some(pki_file) = pki_file {
let _ = writeln!(stream, "<h1>Description</h1>");
convert_subheader(
BufReader::new(pki_file)
.lines()
.map(|line| line.unwrap_or_default()),
&mut stream,
1,
);
let _ = writeln!(stream, "<hr>");
let _ = writeln!(stream, "<a href=\"/{parent_path}\">&lt;&lt; Back</a>");
}
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, "<fieldset><legend>{name}</legend>{text}</fieldset>");
}
let _ = writeln!(stream, "<hr>");
let _ = writeln!(stream, "<a href=\"/{parent_path}\">&lt;&lt; Back</a>");
}
}