first commit
This commit is contained in:
1
stdsrv/.gitignore
vendored
Normal file
1
stdsrv/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
target
|
||||
9
stdsrv/Cargo.toml
Normal file
9
stdsrv/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "stdsrv"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
# local dependencies
|
||||
[dependencies]
|
||||
cracked_md = { path = "../cracked_md" }
|
||||
fstools = { path = "../fstools" }
|
||||
58
stdsrv/src/args.rs
Normal file
58
stdsrv/src/args.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
use crate::error::ErrorKind;
|
||||
|
||||
pub struct ProgramArgs {
|
||||
pub outdir: PathBuf,
|
||||
pub indir: PathBuf,
|
||||
pub generate: bool,
|
||||
pub force: bool,
|
||||
pub addr: String,
|
||||
}
|
||||
|
||||
impl Default for ProgramArgs {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
indir: PathBuf::from("./web"),
|
||||
outdir: PathBuf::from("./html"),
|
||||
generate: false,
|
||||
force: false,
|
||||
addr: "0.0.0.0:8080".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<std::env::Args> for ProgramArgs {
|
||||
type Error = crate::error::Error;
|
||||
fn try_from(mut value: std::env::Args) -> Result<Self, Self::Error> {
|
||||
let mut a = Self::default();
|
||||
let _ = value.next(); // ignore executable path
|
||||
while let Some(v) = value.next() {
|
||||
match v.as_str() {
|
||||
"-i" => {
|
||||
a.indir = value
|
||||
.next()
|
||||
.ok_or(Error::new(
|
||||
ErrorKind::CommandLineArgsParse,
|
||||
"Expected input directory after option `-i`",
|
||||
))?
|
||||
.into();
|
||||
}
|
||||
"-a" => {
|
||||
a.addr = value.next().ok_or(Error::new(
|
||||
ErrorKind::CommandLineArgsParse,
|
||||
"Expected listener address after option `-a`",
|
||||
))?;
|
||||
}
|
||||
"-g" => a.generate = true,
|
||||
"-f" => a.force = true,
|
||||
_ => {
|
||||
a.outdir = v.into();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(a)
|
||||
}
|
||||
}
|
||||
53
stdsrv/src/error.rs
Normal file
53
stdsrv/src/error.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
//! Custom Error type and Result enum and their most standard trait implementations.
|
||||
|
||||
use std::fmt::{Debug, Display};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub enum ErrorKind {
|
||||
StreamReadFailed,
|
||||
CommandLineArgsParse,
|
||||
UnsupportedHttpMethod,
|
||||
UnsupportedHttpVersion,
|
||||
RequestParse,
|
||||
FileNotFound,
|
||||
DirNotFound,
|
||||
Io,
|
||||
TcpBind,
|
||||
NotImplemented,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct Error {
|
||||
kind: ErrorKind,
|
||||
msg: String,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn new(kind: ErrorKind, msg: &str) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
msg: msg.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}: {}", self.kind, self.msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(value: std::io::Error) -> Self {
|
||||
Self {
|
||||
kind: ErrorKind::Io,
|
||||
msg: value.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
74
stdsrv/src/fileserver.rs
Normal file
74
stdsrv/src/fileserver.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
//! FileServer
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::{
|
||||
error::*,
|
||||
request::{HttpMethod, HttpRequest},
|
||||
responder::Responder,
|
||||
response::{HttpResponse, HttpStatus},
|
||||
};
|
||||
|
||||
pub struct FileServer {
|
||||
root: PathBuf,
|
||||
}
|
||||
|
||||
impl FileServer {
|
||||
pub fn new(root: &PathBuf) -> Result<FileServer> {
|
||||
if !root.is_dir() {
|
||||
return Err(Error::new(
|
||||
ErrorKind::DirNotFound,
|
||||
&root.display().to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
root: root.to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_contents(&self, path: PathBuf) -> Option<Vec<u8>> {
|
||||
let mut fullpath = self.root.as_path().join(path);
|
||||
// default to index.html
|
||||
if fullpath.is_dir() {
|
||||
fullpath.push("index.html");
|
||||
}
|
||||
fs::read(fullpath).ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl Responder for FileServer {
|
||||
fn respond(&self, req: HttpRequest) -> HttpResponse {
|
||||
if req.version != "HTTP/1.1" {
|
||||
return HttpResponse::new_empty(HttpStatus::HTTPVersionNotSupported);
|
||||
}
|
||||
|
||||
if req.method != HttpMethod::GET {
|
||||
return HttpResponse::new_empty(HttpStatus::MethodNotAllowed);
|
||||
}
|
||||
|
||||
let content_type = match req.path.extension() {
|
||||
Some(s) => match s.as_encoded_bytes() {
|
||||
b"html" | b"htm" => "text/html",
|
||||
b"txt" => "text/plain",
|
||||
b"css" => "text/css",
|
||||
b"js" => "text/javascript",
|
||||
b"pdf" => "application/pdf",
|
||||
b"json" => "application/json",
|
||||
b"xml" => "application/xml",
|
||||
b"gif" => "image/gif",
|
||||
b"jpeg" | b"jpg" => "image/jpg",
|
||||
b"png" => "image/png",
|
||||
_ => "text/plain",
|
||||
},
|
||||
None => "text/html",
|
||||
};
|
||||
|
||||
if let Some(content) = self.get_contents(req.path) {
|
||||
HttpResponse::new(HttpStatus::Ok, content).add_header("Content-Type", content_type)
|
||||
} else {
|
||||
HttpResponse::new_empty(HttpStatus::NotFound).add_header("Connection", "close")
|
||||
}
|
||||
}
|
||||
}
|
||||
37
stdsrv/src/http_header.rs
Normal file
37
stdsrv/src/http_header.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use std::{collections::HashMap, fmt::Display};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HttpHeaders {
|
||||
_inner: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl HttpHeaders {
|
||||
pub fn new() -> Self {
|
||||
HttpHeaders {
|
||||
_inner: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, k: &str, v: &str) {
|
||||
self._inner.insert(k.to_string(), v.to_string());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn get(&self, k: &str) -> Option<&String> {
|
||||
self._inner.get(k)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn len(&self) -> usize {
|
||||
self._inner.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for HttpHeaders {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
for (k, v) in self._inner.iter() {
|
||||
write!(f, "{}: {}\r\n", k, v)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
43
stdsrv/src/http_stream.rs
Normal file
43
stdsrv/src/http_stream.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use crate::error::ErrorKind;
|
||||
use crate::log;
|
||||
use crate::logger::Level;
|
||||
use std::io::Read;
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
|
||||
use crate::{error::Error, request::HttpRequest};
|
||||
|
||||
pub struct HttpStream {
|
||||
tcp_listener: TcpListener,
|
||||
}
|
||||
|
||||
impl HttpStream {
|
||||
pub fn new(addr: &str) -> Self {
|
||||
let tcp_listener = TcpListener::bind(addr)
|
||||
.unwrap_or_else(|e| panic!("Failed to bind on address `{}`: {}", addr, e));
|
||||
log!(Level::Info, "Listening on `{}`", addr);
|
||||
|
||||
Self { tcp_listener }
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for HttpStream {
|
||||
type Item = (HttpRequest, TcpStream);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
// safe to unwrap, because Incoming never returns None
|
||||
let mut stream = self.tcp_listener.incoming().next().unwrap().ok()?;
|
||||
|
||||
let mut buf = [0; 1024];
|
||||
let _read_bytes = stream
|
||||
.read(&mut buf)
|
||||
.or(Err(Error::new(
|
||||
ErrorKind::StreamReadFailed,
|
||||
"Reading from TCP stream failed",
|
||||
)))
|
||||
.ok()?;
|
||||
Some((
|
||||
String::from_utf8_lossy(&buf[..]).trim().try_into().ok()?,
|
||||
stream,
|
||||
))
|
||||
}
|
||||
}
|
||||
39
stdsrv/src/logger.rs
Normal file
39
stdsrv/src/logger.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub enum Level {
|
||||
Error,
|
||||
Warn,
|
||||
Info,
|
||||
Debug,
|
||||
}
|
||||
|
||||
impl Display for Level {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}]\x1b[0m",
|
||||
match self {
|
||||
Self::Error => "\x1b[1;31m[ERROR",
|
||||
Self::Warn => "\x1b[1;33m[WARN",
|
||||
Self::Info => "\x1b[0;32m[INFO",
|
||||
Self::Debug => "\x1b[0;36m[DEBUG",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log {
|
||||
($level:expr, $($arg:tt)*) => {{
|
||||
println!(
|
||||
"{} {}:{}:{}: {}",
|
||||
$level,
|
||||
std::module_path!(),
|
||||
std::file!(),
|
||||
std::line!(),
|
||||
format!($($arg)*)
|
||||
);
|
||||
}};
|
||||
}
|
||||
88
stdsrv/src/main.rs
Normal file
88
stdsrv/src/main.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
//! This is my very monolithic file server with zero dependencies (other than rust
|
||||
//! stdlib).
|
||||
//!
|
||||
//! Documentation is a work in progress, go see my webpage at [jlux.dev](https://jlux.dev).
|
||||
|
||||
use std::{
|
||||
io::{BufReader, BufWriter},
|
||||
net::TcpListener,
|
||||
process,
|
||||
};
|
||||
|
||||
use args::ProgramArgs;
|
||||
use cracked_md::generate;
|
||||
use fileserver::FileServer;
|
||||
use logger::Level;
|
||||
use request::HttpRequest;
|
||||
use responder::Responder;
|
||||
|
||||
mod args;
|
||||
mod error;
|
||||
mod fileserver;
|
||||
mod http_header;
|
||||
//mod http_stream;
|
||||
mod logger;
|
||||
mod request;
|
||||
mod responder;
|
||||
mod response;
|
||||
|
||||
/// Entrypoint to the program.
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: ProgramArgs = std::env::args().try_into()?;
|
||||
|
||||
if args.generate {
|
||||
match generate(&args.indir, &args.outdir, args.force) {
|
||||
Ok(_) => log!(
|
||||
Level::Info,
|
||||
"HTML generation from `{}` to `{}` successful",
|
||||
args.indir.display(),
|
||||
args.outdir.display()
|
||||
),
|
||||
Err(cracked_md::Error::OutDirIsNotEmpty) => {
|
||||
log!(
|
||||
Level::Error,
|
||||
"HTML generation failed, run `rm -r {}` and retry",
|
||||
args.outdir.display()
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
Err(e) => {
|
||||
log!(Level::Error, "HTML generation failed with error: {}", e,);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let listener = TcpListener::bind(&args.addr)?;
|
||||
log!(Level::Info, "Listening on addr `{}`", &args.addr);
|
||||
|
||||
for stream in listener.incoming() {
|
||||
match stream {
|
||||
Ok(stream) => {
|
||||
let outdir = args.outdir.to_owned();
|
||||
std::thread::spawn(move || {
|
||||
log!(Level::Debug, "TcpStream handler spawned");
|
||||
let mut reader = BufReader::new(&stream);
|
||||
let mut writer = BufWriter::new(&stream);
|
||||
let server = match FileServer::new(&outdir) {
|
||||
Ok(s) => s,
|
||||
Err(_e) => return,
|
||||
};
|
||||
|
||||
while let Ok(req) = HttpRequest::try_from(&mut reader) {
|
||||
let _ = server
|
||||
.respond(req)
|
||||
.add_header("Server", "stdsrv")
|
||||
.add_header("Connection", "keep-alive")
|
||||
.send(&mut writer)
|
||||
.map_err(|e| log!(Level::Error, "{}", e));
|
||||
}
|
||||
log!(Level::Debug, "TcpStream handler exited");
|
||||
});
|
||||
}
|
||||
Err(e) => log!(Level::Warn, "Connection failed: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
243
stdsrv/src/request.rs
Normal file
243
stdsrv/src/request.rs
Normal file
@@ -0,0 +1,243 @@
|
||||
//! An abstraction layer for parsing HTTP requests. As simple and high level as I managed.
|
||||
|
||||
use crate::error::{Error, ErrorKind, Result};
|
||||
use crate::http_header::HttpHeaders;
|
||||
use crate::log;
|
||||
use crate::logger::Level;
|
||||
use std::fmt::Display;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::net::TcpStream;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Supports just GET methods for now.
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[allow(dead_code, clippy::upper_case_acronyms)]
|
||||
pub enum HttpMethod {
|
||||
GET,
|
||||
//PUT,
|
||||
//POST,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for HttpMethod {
|
||||
type Error = Error;
|
||||
fn try_from(value: &str) -> Result<Self> {
|
||||
match value {
|
||||
"GET" => Ok(Self::GET),
|
||||
//"PUT" => Ok(Self::PUT),
|
||||
//"POST" => Ok(Self::POST),
|
||||
_ => Err(Error::new(ErrorKind::UnsupportedHttpMethod, value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for HttpMethod {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Type for HTTP requests.
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct HttpRequest {
|
||||
pub method: HttpMethod,
|
||||
pub path: PathBuf,
|
||||
pub version: String,
|
||||
pub headers: HttpHeaders,
|
||||
}
|
||||
|
||||
impl Display for HttpRequest {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{} {} {}\n{}",
|
||||
self.method,
|
||||
self.path.display(),
|
||||
self.version,
|
||||
self.headers
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&mut BufReader<&TcpStream>> for HttpRequest {
|
||||
type Error = Error;
|
||||
fn try_from(value: &mut BufReader<&TcpStream>) -> std::result::Result<Self, Self::Error> {
|
||||
let mut request_line = String::new();
|
||||
|
||||
value
|
||||
.read_line(&mut request_line)
|
||||
.map_err(|_e| Error::new(ErrorKind::RequestParse, "expected request line"))?;
|
||||
let mut parts = request_line.split_whitespace();
|
||||
let method: HttpMethod = parts
|
||||
.next()
|
||||
.ok_or(Error::new(
|
||||
ErrorKind::RequestParse,
|
||||
"expected request method",
|
||||
))?
|
||||
.try_into()?;
|
||||
let path_with_prefix: PathBuf = parts
|
||||
.next()
|
||||
.ok_or(Error::new(ErrorKind::RequestParse, "expected request path"))?
|
||||
.into();
|
||||
let path = if path_with_prefix.starts_with("/") {
|
||||
path_with_prefix.strip_prefix("/").unwrap().into()
|
||||
} else {
|
||||
path_with_prefix
|
||||
};
|
||||
|
||||
let version = parts
|
||||
.next()
|
||||
.ok_or(Error::new(
|
||||
ErrorKind::RequestParse,
|
||||
"expected request version",
|
||||
))?
|
||||
.into();
|
||||
|
||||
let mut req = Self {
|
||||
method,
|
||||
path,
|
||||
version,
|
||||
headers: HttpHeaders::new(),
|
||||
};
|
||||
|
||||
loop {
|
||||
let mut line = String::new();
|
||||
value.read_line(&mut line)?;
|
||||
if line == "\r\n" || line.is_empty() {
|
||||
break;
|
||||
}
|
||||
if let Some((header, val)) = line.split_once(": ") {
|
||||
req.headers.add(header, val);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(req)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for HttpRequest {
|
||||
type Error = Error;
|
||||
fn try_from(s: &str) -> Result<Self> {
|
||||
let mut lines = s.split("\r\n");
|
||||
|
||||
let request_line = lines
|
||||
.next()
|
||||
.ok_or(Error::new(ErrorKind::RequestParse, "expected request line"))?;
|
||||
let mut parts = request_line.split_whitespace();
|
||||
|
||||
let method = parts
|
||||
.next()
|
||||
.ok_or(Error::new(
|
||||
ErrorKind::RequestParse,
|
||||
"expected request method",
|
||||
))?
|
||||
.try_into()?;
|
||||
let path_with_prefix: PathBuf = parts
|
||||
.next()
|
||||
.ok_or(Error::new(ErrorKind::RequestParse, "expected request path"))?
|
||||
.into();
|
||||
let path = if path_with_prefix.starts_with("/") {
|
||||
path_with_prefix.strip_prefix("/").unwrap().into()
|
||||
} else {
|
||||
path_with_prefix
|
||||
};
|
||||
|
||||
let version = parts
|
||||
.next()
|
||||
.ok_or(Error::new(
|
||||
ErrorKind::RequestParse,
|
||||
"expected request version",
|
||||
))?
|
||||
.into();
|
||||
|
||||
let mut headers = HttpHeaders::new();
|
||||
|
||||
for line in lines {
|
||||
if let Some(v) = line.split_once(": ") {
|
||||
headers.add(v.0, v.1)
|
||||
}
|
||||
}
|
||||
|
||||
let req = Self {
|
||||
method,
|
||||
path,
|
||||
version,
|
||||
headers,
|
||||
};
|
||||
|
||||
log!(Level::Info, "\n{}", req);
|
||||
|
||||
Ok(req)
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
// TESTS
|
||||
// ------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod request_test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn http_parse_method_get() {
|
||||
let s = "GET / HTTP/1.1\r\nHost: localhost:8080\r\nUser-Agent: curl/8.14.1\r\nAccept: */*\r\n\r\n";
|
||||
let req: HttpRequest = s.try_into().unwrap();
|
||||
|
||||
assert_eq!(req.method, HttpMethod::GET);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_parse_path_indexhtml() {
|
||||
let s = "GET /index.html HTTP/1.1\r\nHost: localhost:8080\r\nUser-Agent: curl/8.14.1\r\nAccept: */*\r\n\r\n";
|
||||
let req: HttpRequest = s.try_into().unwrap();
|
||||
|
||||
assert_eq!(req.path, PathBuf::from("index.html"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_parse_version() {
|
||||
let s = "GET / HTTP/1.1\r\nHost: localhost:8080\r\nUser-Agent: curl/8.14.1\r\nAccept: */*\r\n\r\n";
|
||||
let req: HttpRequest = s.try_into().unwrap();
|
||||
|
||||
assert_eq!(req.version, "HTTP/1.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_parse_headers_len() {
|
||||
let s = "GET / HTTP/1.1\r\nHost: localhost:8080\r\nUser-Agent: curl/8.14.1\r\nAccept: */*\r\n\r\n";
|
||||
let req: HttpRequest = s.try_into().unwrap();
|
||||
|
||||
assert_eq!(req.headers.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_parse_useragent_header_curl() {
|
||||
let s = "GET / HTTP/1.1\r\nHost: localhost:8080\r\nUser-Agent: curl/8.14.1\r\nAccept: */*\r\n\r\n";
|
||||
let req: HttpRequest = s.try_into().unwrap();
|
||||
|
||||
assert_eq!(req.headers.get("User-Agent").unwrap(), "curl/8.14.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_parse_empty_should_fail() {
|
||||
let s = "";
|
||||
let req: Result<HttpRequest> = s.try_into();
|
||||
|
||||
assert!(req.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_parse_unsupported_method_delete() {
|
||||
let s = "DELETE / HTTP/1.1\r\nHost: localhost:8080\r\nUser-Agent: curl/8.14.1\r\nAccept: */*\r\n\r\n";
|
||||
let req: Result<HttpRequest> = s.try_into();
|
||||
|
||||
assert!(req.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_method_display() {
|
||||
let method = HttpMethod::GET;
|
||||
assert_eq!(method.to_string(), "GET")
|
||||
}
|
||||
}
|
||||
55
stdsrv/src/responder.rs
Normal file
55
stdsrv/src/responder.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
//! Traits helping HTTP connections
|
||||
|
||||
use crate::request::HttpRequest;
|
||||
use crate::response::HttpResponse;
|
||||
|
||||
/// Responder trait. Just a respond method that turns a HttpRequest to a HttpResponse.
|
||||
pub trait Responder {
|
||||
fn respond(&self, req: HttpRequest) -> HttpResponse;
|
||||
}
|
||||
|
||||
/*
|
||||
/// Size trait. Number of bytes when encoded.
|
||||
pub trait Size {
|
||||
fn size(&self) -> usize;
|
||||
}
|
||||
|
||||
// Standard implementations for Size trait
|
||||
|
||||
impl Size for u8 {
|
||||
fn size(&self) -> usize {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Size for Vec<T>
|
||||
where
|
||||
T: Size,
|
||||
{
|
||||
fn size(&self) -> usize {
|
||||
if let Some(elem) = self.first() {
|
||||
elem.size() * self.len()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Size for Option<T>
|
||||
where
|
||||
T: Size,
|
||||
{
|
||||
fn size(&self) -> usize {
|
||||
match self {
|
||||
Some(t) => t.size(),
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Size for String {
|
||||
fn size(&self) -> usize {
|
||||
self.len()
|
||||
}
|
||||
}
|
||||
*/
|
||||
189
stdsrv/src/response.rs
Normal file
189
stdsrv/src/response.rs
Normal file
@@ -0,0 +1,189 @@
|
||||
//! An abstraction layer for building and sending HTTP responses.
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::http_header::HttpHeaders;
|
||||
use crate::log;
|
||||
use crate::logger::Level;
|
||||
use std::{fmt::Display, io::Write};
|
||||
|
||||
/// Macro for generating Http status codes (AI generated).
|
||||
macro_rules! http_statuses {
|
||||
($($name:ident => ($code:expr, $reason:expr)),+ $(,)?) => {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[allow(unused_attributes, dead_code)]
|
||||
pub enum HttpStatus {
|
||||
$($name),+
|
||||
}
|
||||
|
||||
impl HttpStatus {
|
||||
pub fn code(&self) -> u16 {
|
||||
match self {
|
||||
$(HttpStatus::$name => $code,)+
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reason(&self) -> &'static str {
|
||||
match self {
|
||||
$(HttpStatus::$name => $reason,)+
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
http_statuses!(
|
||||
Ok => (200, "OK"),
|
||||
|
||||
NotFound => (404, "Not Found"),
|
||||
MethodNotAllowed => (405, "Method Not Allowed"),
|
||||
|
||||
ImATeapot => (418, "I'm a teapot"),
|
||||
|
||||
InternalServerError => (500, "Internal Server Error"),
|
||||
HTTPVersionNotSupported => (505, "HTTP Version Not Supported"),
|
||||
);
|
||||
|
||||
impl Display for HttpStatus {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{} {}", self.code(), self.reason())
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP response structure
|
||||
#[derive(Debug)]
|
||||
pub struct HttpResponse {
|
||||
version: String,
|
||||
status: HttpStatus,
|
||||
headers: HttpHeaders,
|
||||
body: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
/*
|
||||
impl Size for HttpResponse {
|
||||
fn size(&self) -> usize {
|
||||
self.as_bytes().len()
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
impl HttpResponse {
|
||||
pub fn new_empty(status: HttpStatus) -> Self {
|
||||
let mut headers = HttpHeaders::new();
|
||||
headers.add("Content-Length", "0");
|
||||
Self {
|
||||
version: "HTTP/1.1".into(),
|
||||
status,
|
||||
headers,
|
||||
body: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(status: HttpStatus, body: Vec<u8>) -> Self {
|
||||
let mut headers = HttpHeaders::new();
|
||||
headers.add("Content-Length", &body.len().to_string());
|
||||
Self {
|
||||
version: "HTTP/1.1".into(),
|
||||
status,
|
||||
headers,
|
||||
body: Some(body),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_header(mut self, key: &str, value: &str) -> Self {
|
||||
self.headers.add(key, value);
|
||||
self
|
||||
}
|
||||
|
||||
fn add_header_inner(&mut self, key: &str, value: &str) {
|
||||
self.headers.add(key, value);
|
||||
}
|
||||
|
||||
/// sending images "cuts off"
|
||||
pub fn send(&mut self, stream: &mut impl Write) -> Result<()> {
|
||||
let body_len = match &self.body {
|
||||
Some(v) => format!("{}", v.len()),
|
||||
None => "0".to_string(),
|
||||
};
|
||||
self.add_header_inner("Content-Length", &body_len);
|
||||
stream.write_all(self.start_bytes())?;
|
||||
if let Some(b) = &self.body {
|
||||
stream.write_all(b)?;
|
||||
}
|
||||
//stream.write_all(b"\r\n")?;
|
||||
stream.flush()?;
|
||||
//sleep(Duration::from_millis(100));
|
||||
/*
|
||||
stream.shutdown(std::net::Shutdown::Write)?;
|
||||
|
||||
// hack?
|
||||
let _ = std::io::Read::read(stream, &mut [0u8; 1]);
|
||||
*/
|
||||
|
||||
log!(Level::Info, "\n{}", &self);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start_bytes(&self) -> &[u8] {
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
bytes.extend_from_slice(self.version.as_bytes());
|
||||
bytes.extend_from_slice(b" ");
|
||||
bytes.extend_from_slice(self.status.to_string().as_bytes());
|
||||
bytes.extend_from_slice(b"\r\n");
|
||||
bytes.extend_from_slice(self.headers.to_string().as_bytes());
|
||||
bytes.extend_from_slice(b"\r\n");
|
||||
bytes.leak()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for HttpResponse {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{} {}\r\n{}\r\n",
|
||||
self.version, self.status, self.headers,
|
||||
)?;
|
||||
if let Some(s) = &self.body {
|
||||
write!(
|
||||
f,
|
||||
"{}\r\n",
|
||||
String::from_utf8(s.to_vec()).unwrap_or("<binary data>".to_string())
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------
|
||||
// TESTS
|
||||
// --------------------
|
||||
#[cfg(test)]
|
||||
mod response_test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn http_status_macro_display() {
|
||||
let stat = HttpStatus::ImATeapot;
|
||||
|
||||
assert_eq!(stat.to_string(), "418 I'm a teapot");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_response_new_empty() {
|
||||
let resp = HttpResponse::new_empty(HttpStatus::ImATeapot);
|
||||
|
||||
assert_eq!(
|
||||
resp.to_string(),
|
||||
"HTTP/1.1 418 I'm a teapot\r\nContent-Length: 0\r\n\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_response_new_with_body() {
|
||||
let resp = HttpResponse::new(HttpStatus::ImATeapot, b"teapot".into());
|
||||
|
||||
assert_eq!(
|
||||
resp.to_string(),
|
||||
"HTTP/1.1 418 I'm a teapot\r\nContent-Length: 6\r\n\r\nteapot\r\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user