275 lines
8.5 KiB
Rust
275 lines
8.5 KiB
Rust
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<str>,
|
|
text: Box<str>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct SiteInfo {
|
|
comments: Vec<Comment>,
|
|
visits: usize,
|
|
}
|
|
|
|
#[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, 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, "<meta charset=\"utf-8\">");
|
|
|
|
let _ = writeln!(stream, "<p>👁️{}</p>", info.visits);
|
|
|
|
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}\"><< 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}\"><< 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!";
|
|
}
|
|
};
|
|
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}\"><< Back</a>");
|
|
}
|
|
}
|