Http responses contain body

This commit is contained in:
2021-05-16 12:23:34 +02:00
parent a8c6962b20
commit 782ad13e48
7 changed files with 175 additions and 30 deletions

View File

@ -89,3 +89,66 @@ pub static HTTP_STATUSES: [(u16, StatusClass, &str, &str); 41] = [
(505, ServerError, "HTTP Version Not Supported", (505, ServerError, "HTTP Version Not Supported",
"The server does not support, or refuses to support, the HTTP protocol version that was used in the request message."), "The server does not support, or refuses to support, the HTTP protocol version that was used in the request message."),
]; ];
pub static DEFAULT_DOCUMENT: &str = "\
<!DOCTYPE html>\n\
<html lang=\"en\">\n\
<head>\n\
\t<title>{status_code} {status_message} - Locutus - {hostname}</title>\n\
\t<meta charset=\"UTF-8\"/>\n\
\t<meta name=\"theme-color\" content=\"{theme_color}\"/>\n\
\t<meta name=\"color-scheme\" content=\"light dark\"/>\n\
\t<meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\"/>\n\
\t<meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\"/>\n\
\t<link rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/favicon.ico\"/>\n\
\t<style>\n\
\t\thtml{{font-family:\"Arial\",sans-serif;--error:#C00000;--warning:#E0C000;--success:#008000;--info:#606060;--color:var(--{color_name});}}\n\
\t\tbody{{background-color:#F0F0F0;margin:0;}}\n\
\t\tmain{{max-width:650px;margin:2em auto;}}\n\
\t\tsection{{margin:1em;background-color:#FFFFFF;border: 1px solid var(--color);border-radius:4px;padding:1em;}}\n\
\t\th1,h2,h3,h4,h5,h6,h7{{text-align:center;color:var(--color);font-weight:normal;}}\n\
\t\th1{{font-size:3em;margin:0.125em 0 0.125em 0;}}\n\
\t\th2{{font-size:1.5em;margin:0.25em 0 1em 0;}}\n\
\t\tp{{text-align:center;font-size:0.875em;}}\n\
\t\tdiv.footer{{color:#808080;font-size:0.75em;text-align:center;margin:2em 0 0.5em 0;}}\n\
\t\tdiv.footer a{{color:#808080;}}\n\
\t\t@media(prefers-color-scheme:dark){{\n\
\t\t\thtml{{color:#FFFFFF;}}\n\
\t\t\tbody{{background-color:#101010;}}\n\
\t\t\tsection{{background-color:#181818;}}\n\
\t\t}}\n\
\t</style>\n\
</head>\n\
<body>\n\
\t<main>\n\
\t\t<section>\n\
{doc}\
\t\t\t<div class=\"footer\"><a href=\"https://{hostname}/\">{hostname}</a> - {server_str}</div>\n\
\t\t</section>\n\
\t</main>\n\
</body>\n\
</html>\n";
pub static ERROR_DOCUMENT: &str = "\
\t\t\t<h1>{code}</h1>\n\
\t\t\t<h2>{message} :&#xFEFF;(</h2>\n\
\t\t\t<p>{desc}</p>\n\
\t\t\t<p>{info}</p>\n";
pub static WARNING_DOCUMENT: &str = "\
\t\t\t<h1>{code}</h1>\n\
\t\t\t<h2>{message} :&#xFEFF;o</h2>\n\
\t\t\t<p>{desc}</p>\n\
\t\t\t<p>{info}</p>\n";
pub static SUCCESS_DOCUMENT: &str = "\
\t\t\t<h1>{code}</h1>\n\
\t\t\t<h2>{message} :&#xFEFF;)</h2>\n\
\t\t\t<p>{desc}</p>\n\
\t\t\t<p>{info}</p>\n";
pub static INFO_DOCUMENT: &str = "\
\t\t\t<h1>{code}</h1>\n\
\t\t\t<h2>{message} :&#xFEFF;)</h2>\n\
\t\t\t<p>{desc}</p>\n\
\t\t\t<p>{info}</p>\n";

View File

@ -1,13 +1,14 @@
use super::Method; use super::Method;
use crate::usimp; use crate::usimp;
use crate::websocket;
use chrono; use chrono;
use json; use json;
pub struct HttpStream { pub struct HttpStream {
stream: super::Stream, pub stream: super::Stream,
request_num: u32, pub request_num: u32,
client_keep_alive: bool, pub client_keep_alive: bool,
server_keep_alive: bool, pub server_keep_alive: bool,
} }
pub fn connection_handler(client: super::Stream) { pub fn connection_handler(client: super::Stream) {
@ -29,20 +30,10 @@ pub fn connection_handler(client: super::Stream) {
fn request_handler(client: &mut super::HttpStream) { fn request_handler(client: &mut super::HttpStream) {
let mut res = super::Response::new(); let mut res = super::Response::new();
res.add_header("Server", "Locutus");
res.add_header(
"Date",
chrono::Utc::now()
.format("%a, %d %b %Y %H:%M:%S GMT")
.to_string()
.as_str(),
);
let req = super::parser::parse_request(&mut client.stream).unwrap(); let req = super::parser::parse_request(&mut client.stream).unwrap();
println!("{} {}", req.method, req.uri); println!("{} {}", req.method, req.uri);
let doc = "<!DOCTYPE html><html><head><title>Locutus Server</title></head><body><h1>Hello World! :D</h1></body></html>";
if !req.uri.starts_with("/") if !req.uri.starts_with("/")
|| req.uri.contains("/./") || req.uri.contains("/./")
|| req.uri.contains("/../") || req.uri.contains("/../")
@ -53,16 +44,22 @@ fn request_handler(client: &mut super::HttpStream) {
res.status(404); res.status(404);
} else if req.uri.eq("/") { } else if req.uri.eq("/") {
res.status(200); res.status(200);
res.add_header("Content-Length", doc.len().to_string().as_str()); } else if req.uri.eq("/_usimp/websocket") {
res.add_header("Content-Type", "text/html; charset=utf-8"); return websocket::connection_handler(client, &req);
} else if req.uri.starts_with("/_usimp/") { } else if req.uri.starts_with("/_usimp/") {
let parts: Vec<&str> = req.uri.split('/').collect(); let parts: Vec<&str> = req.uri.split('/').collect();
match parts[2..] { match parts[2..] {
["entity", entity] => res.status(501), ["entity", entity] => res.status(501),
[func] => match usimp::is_valid(func) { [endpoint] => match usimp::is_valid_endpoint(endpoint) {
true => match req.method { true => match req.method {
Method::POST => res.status(200), Method::POST => {
_ => res.status(405), // TODO
res.status(200)
}
_ => {
res.status(405);
res.add_header("Allow", "POST");
}
}, },
false => res.status(400), false => res.status(400),
}, },
@ -73,6 +70,5 @@ fn request_handler(client: &mut super::HttpStream) {
} }
res.send(&mut client.stream).unwrap(); res.send(&mut client.stream).unwrap();
client.stream.write_all(doc.as_bytes()).unwrap();
client.server_keep_alive = false; client.server_keep_alive = false;
} }

View File

@ -95,16 +95,18 @@ impl StatusClass {
pub struct Status { pub struct Status {
code: u16, code: u16,
message: String, message: String,
desc: &'static str,
class: StatusClass, class: StatusClass,
} }
impl Status { impl Status {
pub fn from_code(status_code: u16) -> Status { pub fn from_code(status_code: u16) -> Status {
for (code, class, msg, _desc) in &consts::HTTP_STATUSES { for (code, class, msg, desc) in &consts::HTTP_STATUSES {
if *code == status_code { if *code == status_code {
return Status { return Status {
code: status_code, code: status_code,
message: msg.to_string(), message: msg.to_string(),
desc,
class: class.clone(), class: class.clone(),
}; };
} }
@ -116,10 +118,12 @@ impl Status {
if status_code < 100 || status_code > 599 { if status_code < 100 || status_code > 599 {
panic!("invalid status code"); panic!("invalid status code");
} }
let status = Status::from_code(status_code);
Status { Status {
code: status_code, code: status_code,
message: message.to_string(), message: message.to_string(),
class: StatusClass::from_code(status_code), desc: status.desc,
class: status.class,
} }
} }
} }
@ -137,8 +141,8 @@ impl std::fmt::Display for HeaderField {
pub struct Request { pub struct Request {
version: String, version: String,
method: Method, pub method: Method,
uri: String, pub uri: String,
header_fields: Vec<HeaderField>, header_fields: Vec<HeaderField>,
} }
@ -149,7 +153,7 @@ pub struct Response {
} }
impl Response { impl Response {
fn new() -> Response { pub fn new() -> Response {
Response { Response {
version: "1.1".to_string(), version: "1.1".to_string(),
status: Status::from_code(200), status: Status::from_code(200),
@ -157,18 +161,73 @@ impl Response {
} }
} }
fn status(&mut self, status_code: u16) { pub fn status(&mut self, status_code: u16) {
self.status = Status::from_code(status_code) self.status = Status::from_code(status_code)
} }
fn add_header(&mut self, name: &str, value: &str) { pub fn add_header(&mut self, name: &str, value: &str) {
self.header_fields.push(HeaderField { self.header_fields.push(HeaderField {
name: String::from(name), name: String::from(name),
value: String::from(value), value: String::from(value),
}); });
} }
fn send(&self, stream: &mut Stream) -> Result<(), std::io::Error> { pub fn find_header(&self, header_name: &str) -> Option<String> {
for field in &self.header_fields {
if field
.name
.to_lowercase()
.eq(header_name.to_ascii_lowercase().as_str())
{
return Some(field.value.clone());
}
}
return None;
}
pub fn send(&mut self, stream: &mut Stream) -> Result<(), std::io::Error> {
self.add_header("Server", "Locutus");
self.add_header(
"Date",
chrono::Utc::now()
.format("%a, %d %b %Y %H:%M:%S GMT")
.to_string()
.as_str(),
);
let mut buf = None;
if let None = self.find_header("Content-Length") {
let (doc, color_name, color) = match self.status.class {
StatusClass::Informational => (consts::INFO_DOCUMENT, "info", "#606060"),
StatusClass::Success => (consts::SUCCESS_DOCUMENT, "success", "#008000"),
StatusClass::Redirection => (consts::WARNING_DOCUMENT, "warning", "#E0C000"),
StatusClass::ClientError => (consts::ERROR_DOCUMENT, "error", "#C00000"),
StatusClass::ServerError => (consts::ERROR_DOCUMENT, "error", "#C00000"),
};
let new_buf = consts::DEFAULT_DOCUMENT
.replace("{status_code}", self.status.code.to_string().as_str())
.replace("{status_message}", self.status.message.as_str())
.replace("{hostname}", "localhost") // TODO hostname
.replace("{theme_color}", color)
.replace("{color_name}", color_name)
.replace("{server_str}", "Locutus server") // TODO server string
.replace(
"{doc}",
doc.replace("{code}", self.status.code.to_string().as_str())
.replace("{message}", self.status.message.as_str())
.replace("{desc}", self.status.desc)
.replace("{info}", "") // TODO info string
.as_str(),
)
.replace("{{", "{")
.replace("}}", "}");
self.add_header("Content-Length", new_buf.len().to_string().as_str());
self.add_header("Content-Type", "text/html; charset=utf-8");
buf = Some(new_buf);
}
let mut header = format!( let mut header = format!(
"HTTP/{} {:03} {}\r\n", "HTTP/{} {:03} {}\r\n",
self.version, self.status.code, self.status.message self.version, self.status.code, self.status.message
@ -177,7 +236,12 @@ impl Response {
header.push_str(format!("{}: {}\r\n", header_field.name, header_field.value).as_str()); header.push_str(format!("{}: {}\r\n", header_field.name, header_field.value).as_str());
} }
header.push_str("\r\n"); header.push_str("\r\n");
stream.write_all(header.as_bytes())
stream.write_all(header.as_bytes())?;
if let Some(buf) = buf {
stream.write_all(buf.as_bytes());
}
Ok(())
} }
} }

View File

@ -1,6 +1,7 @@
mod http; mod http;
mod udp; mod udp;
mod usimp; mod usimp;
mod websocket;
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod, SslStream}; use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod, SslStream};
use std::net::{TcpListener, UdpSocket}; use std::net::{TcpListener, UdpSocket};

View File

@ -1,3 +1,7 @@
pub fn is_valid(evt: &str) -> bool { use json;
pub fn is_valid_endpoint(endpoint: &str) -> bool {
false false
} }
pub fn endpoint(endpoint: &str, input: json::object::Object) {}

17
src/websocket/mod.rs Normal file
View File

@ -0,0 +1,17 @@
use crate::http;
pub fn connection_handler(client: &mut http::HttpStream, req: &http::Request) {
client.server_keep_alive = false;
let mut res = http::Response::new();
if let http::Method::GET = req.method {
res.status(501);
} else {
res.status(405);
res.add_header("Allow", "GET");
res.send(&mut client.stream);
return;
}
res.send(&mut client.stream);
}