mirror of https://github.com/vinc/moros.git
527 lines
17 KiB
Rust
527 lines
17 KiB
Rust
use crate::api::clock;
|
|
use crate::api::clock::DATE_TIME_ZONE;
|
|
use crate::api::console::Style;
|
|
use crate::api::fs;
|
|
use crate::api::process::ExitCode;
|
|
use crate::api::syscall;
|
|
use crate::api::time;
|
|
use crate::sys;
|
|
use crate::sys::console;
|
|
|
|
use alloc::collections::btree_map::BTreeMap;
|
|
use alloc::collections::vec_deque::VecDeque;
|
|
use alloc::format;
|
|
use alloc::string::{String, ToString};
|
|
use alloc::vec;
|
|
use alloc::vec::Vec;
|
|
use core::fmt;
|
|
use smoltcp::iface::SocketSet;
|
|
use smoltcp::phy::Device;
|
|
use smoltcp::socket::tcp;
|
|
use smoltcp::time::Instant;
|
|
use smoltcp::wire::IpAddress;
|
|
|
|
const MAX_CONNECTIONS: usize = 32;
|
|
const POLL_DELAY_DIV: usize = 128;
|
|
const INDEX: [&str; 4] = ["", "/index.html", "/index.htm", "/index.txt"];
|
|
|
|
#[derive(Clone)]
|
|
struct Request {
|
|
addr: IpAddress,
|
|
verb: String,
|
|
path: String,
|
|
body: Vec<u8>,
|
|
headers: BTreeMap<String, String>,
|
|
}
|
|
|
|
impl Request {
|
|
pub fn new(addr: IpAddress) -> Self {
|
|
Self {
|
|
addr,
|
|
verb: String::new(),
|
|
path: String::new(),
|
|
body: Vec::new(),
|
|
headers: BTreeMap::new(),
|
|
}
|
|
}
|
|
|
|
pub fn from(addr: IpAddress, buf: &[u8]) -> Option<Self> {
|
|
let msg = String::from_utf8_lossy(buf);
|
|
if !msg.is_empty() {
|
|
let mut req = Request::new(addr);
|
|
let mut is_header = true;
|
|
for (i, line) in msg.lines().enumerate() {
|
|
if i == 0 {
|
|
// Request line
|
|
let fields: Vec<_> = line.split(' ').collect();
|
|
if fields.len() >= 2 {
|
|
req.verb = fields[0].to_string();
|
|
req.path = fields[1].to_string();
|
|
}
|
|
} else if is_header {
|
|
// Message header
|
|
if let Some((key, val)) = line.split_once(':') {
|
|
let k = key.trim().to_string();
|
|
let v = val.trim().to_string();
|
|
req.headers.insert(k, v);
|
|
} else if line.is_empty() {
|
|
is_header = false;
|
|
}
|
|
} else if !is_header {
|
|
// Message body
|
|
let s = format!("{}\n", line);
|
|
req.body.extend_from_slice(s.as_bytes());
|
|
}
|
|
}
|
|
Some(req)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct Response {
|
|
req: Request,
|
|
buf: Vec<u8>,
|
|
mime: String,
|
|
time: String,
|
|
code: usize,
|
|
size: usize,
|
|
body: Vec<u8>,
|
|
headers: BTreeMap<String, String>,
|
|
real_path: String,
|
|
}
|
|
|
|
impl Response {
|
|
pub fn new(req: Request) -> Self {
|
|
let mut headers = BTreeMap::new();
|
|
headers.insert(
|
|
"Date".to_string(),
|
|
time::now_utc().format("%a, %d %b %Y %H:%M:%S GMT"),
|
|
);
|
|
headers.insert(
|
|
"Server".to_string(),
|
|
format!("MOROS/{}", env!("CARGO_PKG_VERSION")),
|
|
);
|
|
Self {
|
|
req,
|
|
buf: Vec::new(),
|
|
mime: String::new(),
|
|
time: time::now().format(DATE_TIME_ZONE),
|
|
code: 0,
|
|
size: 0,
|
|
body: Vec::new(),
|
|
headers,
|
|
real_path: String::new(),
|
|
}
|
|
}
|
|
|
|
pub fn end(&mut self) {
|
|
self.size = self.body.len();
|
|
self.headers.insert(
|
|
"Content-Length".to_string(),
|
|
self.size.to_string()
|
|
);
|
|
self.headers.insert(
|
|
"Connection".to_string(),
|
|
if self.is_persistent() {
|
|
"keep-alive".to_string()
|
|
} else {
|
|
"close".to_string()
|
|
}
|
|
);
|
|
self.headers.insert(
|
|
"Content-Type".to_string(),
|
|
if self.mime.starts_with("text/") {
|
|
format!("{}; charset=utf-8", self.mime)
|
|
} else {
|
|
format!("{}", self.mime)
|
|
}
|
|
);
|
|
self.write();
|
|
}
|
|
|
|
fn write(&mut self) {
|
|
self.buf.clear();
|
|
self.buf.extend_from_slice(
|
|
format!("{}\r\n", self.status()).as_bytes()
|
|
);
|
|
for (key, val) in &self.headers {
|
|
self.buf.extend_from_slice(
|
|
format!("{}: {}\r\n", key, val).as_bytes()
|
|
);
|
|
}
|
|
self.buf.extend_from_slice(b"\r\n");
|
|
self.buf.extend_from_slice(&self.body);
|
|
}
|
|
|
|
fn status(&self) -> String {
|
|
let msg = match self.code {
|
|
200 => "OK",
|
|
301 => "Moved Permanently",
|
|
400 => "Bad Request",
|
|
403 => "Forbidden",
|
|
404 => "Not Found",
|
|
500 => "Internal Server Error",
|
|
_ => "Unknown Error",
|
|
};
|
|
format!("HTTP/1.1 {} {}", self.code, msg)
|
|
}
|
|
|
|
fn is_persistent(&self) -> bool {
|
|
if let Some(value) = self.req.headers.get("Connection") {
|
|
if value == "close" {
|
|
return false;
|
|
}
|
|
}
|
|
true
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Response {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
let csi_blue = Style::color("LightBlue");
|
|
let csi_cyan = Style::color("LightCyan");
|
|
let csi_pink = Style::color("Pink");
|
|
let csi_reset = Style::reset();
|
|
write!(
|
|
f,
|
|
"{}{} - -{} [{}] {}\"{} {}\"{} {} {}",
|
|
csi_cyan,
|
|
self.req.addr,
|
|
csi_pink,
|
|
self.time,
|
|
csi_blue,
|
|
self.req.verb,
|
|
self.req.path,
|
|
csi_reset,
|
|
self.code,
|
|
self.size
|
|
)
|
|
}
|
|
}
|
|
|
|
fn get(req: &Request, res: &mut Response) {
|
|
if fs::is_dir(&res.real_path) && !req.path.ends_with('/') {
|
|
res.code = 301;
|
|
res.mime = "text/html".to_string();
|
|
res.headers.insert(
|
|
"Location".to_string(),
|
|
format!("{}/", req.path),
|
|
);
|
|
res.body.extend_from_slice(b"<h1>Moved Permanently</h1>\r\n");
|
|
} else {
|
|
let mut not_found = true;
|
|
for index in INDEX {
|
|
let real_path = format!("{}{}", res.real_path, index);
|
|
if fs::is_dir(&real_path) {
|
|
continue;
|
|
}
|
|
if let Ok(buf) = fs::read_to_bytes(&real_path) {
|
|
res.code = 200;
|
|
res.mime = content_type(&real_path);
|
|
let tmp;
|
|
res.body.extend_from_slice(
|
|
if res.mime.starts_with("text/") {
|
|
tmp = String::from_utf8_lossy(&buf).to_string().
|
|
replace("\n", "\r\n");
|
|
tmp.as_bytes()
|
|
} else {
|
|
&buf
|
|
},
|
|
);
|
|
not_found = false;
|
|
break;
|
|
}
|
|
}
|
|
if not_found {
|
|
if let Ok(mut files) = fs::read_dir(&res.real_path) {
|
|
res.code = 200;
|
|
res.mime = "text/html".to_string();
|
|
res.body.extend_from_slice(
|
|
format!("<h1>Index of {}</h1>\r\n", req.path).as_bytes()
|
|
);
|
|
files.sort_by_key(|f| f.name());
|
|
for file in files {
|
|
let path = format!("{}{}", req.path, file.name());
|
|
let link = format!(
|
|
"<li><a href=\"{}\">{}</a></li>\n",
|
|
path,
|
|
file.name()
|
|
);
|
|
res.body.extend_from_slice(link.as_bytes());
|
|
}
|
|
} else {
|
|
res.code = 404;
|
|
res.mime = "text/html".to_string();
|
|
res.body.extend_from_slice(b"<h1>Not Found</h1>\r\n");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn put(req: &Request, res: &mut Response) {
|
|
if res.real_path.ends_with('/') {
|
|
// Write directory
|
|
let real_path = res.real_path.trim_end_matches('/');
|
|
if fs::exists(real_path) {
|
|
res.code = 403;
|
|
} else if let Some(handle) = fs::create_dir(real_path) {
|
|
syscall::close(handle);
|
|
res.code = 200;
|
|
} else {
|
|
res.code = 500;
|
|
}
|
|
} else {
|
|
// Write file
|
|
if fs::write(&res.real_path, &req.body).is_ok() {
|
|
res.code = 200;
|
|
} else {
|
|
res.code = 500;
|
|
}
|
|
}
|
|
res.mime = "text/plain".to_string();
|
|
}
|
|
|
|
fn delete(_req: &Request, res: &mut Response) {
|
|
if fs::exists(&res.real_path) {
|
|
if fs::delete(&res.real_path).is_ok() {
|
|
res.code = 200;
|
|
} else {
|
|
res.code = 500;
|
|
}
|
|
} else {
|
|
res.code = 404;
|
|
}
|
|
res.mime = "text/plain".to_string();
|
|
}
|
|
|
|
pub fn main(args: &[&str]) -> Result<(), ExitCode> {
|
|
let csi_color = Style::color("Yellow");
|
|
let csi_reset = Style::reset();
|
|
let mut read_only = false;
|
|
let mut port = 80;
|
|
let mut dir = sys::process::dir();
|
|
let mut i = 1;
|
|
let n = args.len();
|
|
while i < n {
|
|
match args[i] {
|
|
"-h" | "--help" => {
|
|
usage();
|
|
return Ok(());
|
|
}
|
|
"-r" | "--read-only" => {
|
|
read_only = true;
|
|
}
|
|
"-p" | "--port" => {
|
|
if i + 1 < n {
|
|
port = args[i + 1].parse().unwrap_or(port);
|
|
i += 1;
|
|
} else {
|
|
error!("Missing port number");
|
|
return Err(ExitCode::UsageError);
|
|
}
|
|
}
|
|
"-d" | "--dir" => {
|
|
if i + 1 < n {
|
|
dir = args[i + 1].to_string();
|
|
i += 1;
|
|
} else {
|
|
error!("Missing directory");
|
|
return Err(ExitCode::UsageError);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
i += 1;
|
|
}
|
|
|
|
// NOTE: This specific format is needed by `join_path`
|
|
let dir = format!("/{}", fs::realpath(&dir).trim_matches('/'));
|
|
|
|
if let Some((ref mut iface, ref mut device)) = *sys::net::NET.lock() {
|
|
let mut sockets = SocketSet::new(vec![]);
|
|
|
|
let mtu = device.capabilities().max_transmission_unit;
|
|
let buf_len = mtu - 14 - 20 - 20; // ETH+TCP+IP headers
|
|
let mut connections = Vec::new();
|
|
for _ in 0..MAX_CONNECTIONS {
|
|
let tcp_rx_buffer = tcp::SocketBuffer::new(vec![0; buf_len]);
|
|
let tcp_tx_buffer = tcp::SocketBuffer::new(vec![0; buf_len]);
|
|
let tcp_socket = tcp::Socket::new(tcp_rx_buffer, tcp_tx_buffer);
|
|
let tcp_handle = sockets.add(tcp_socket);
|
|
|
|
let send_queue: VecDeque<Vec<u8>> = VecDeque::new();
|
|
let keep_alive = true;
|
|
connections.push((tcp_handle, send_queue, keep_alive));
|
|
}
|
|
|
|
println!(
|
|
"{}HTTP Server listening on 0.0.0.0:{}{}",
|
|
csi_color, port, csi_reset
|
|
);
|
|
|
|
loop {
|
|
if console::end_of_text() || console::end_of_transmission() {
|
|
println!();
|
|
return Ok(());
|
|
}
|
|
|
|
let ms = (clock::realtime() * 1000000.0) as i64;
|
|
let time = Instant::from_micros(ms);
|
|
iface.poll(time, device, &mut sockets);
|
|
|
|
for (tcp_handle, send_queue, keep_alive) in &mut connections {
|
|
let socket = sockets.get_mut::<tcp::Socket>(*tcp_handle);
|
|
|
|
if !socket.is_open() {
|
|
socket.listen(port).unwrap();
|
|
}
|
|
let endpoint = match socket.remote_endpoint() {
|
|
Some(endpoint) => endpoint,
|
|
None => continue,
|
|
};
|
|
if socket.may_recv() {
|
|
// The amount of octets queued in the receive buffer may be
|
|
// larger than the contiguous slice returned by `recv` so
|
|
// we need to loop over chunks of it until it is empty.
|
|
let recv_queue = socket.recv_queue();
|
|
let mut receiving = true;
|
|
let mut buf = vec![];
|
|
while receiving {
|
|
let res = socket.recv(|chunk| {
|
|
buf.extend_from_slice(chunk);
|
|
if buf.len() < recv_queue {
|
|
return (chunk.len(), None);
|
|
}
|
|
receiving = false;
|
|
|
|
let addr = endpoint.addr;
|
|
if let Some(req) = Request::from(addr, &buf) {
|
|
let mut res = Response::new(req.clone());
|
|
res.real_path = join_path(&dir, &req.path);
|
|
|
|
match req.verb.as_str() {
|
|
"GET" => {
|
|
get(&req, &mut res)
|
|
}
|
|
"PUT" if !read_only => {
|
|
put(&req, &mut res)
|
|
}
|
|
"DELETE" if !read_only => {
|
|
delete(&req, &mut res)
|
|
}
|
|
_ => {
|
|
let s = b"<h1>Bad Request</h1>\r\n";
|
|
res.body.extend_from_slice(s);
|
|
res.code = 400;
|
|
res.mime = "text/html".to_string();
|
|
}
|
|
}
|
|
res.end();
|
|
println!("{}", res);
|
|
(chunk.len(), Some(res))
|
|
} else {
|
|
(0, None)
|
|
}
|
|
});
|
|
if receiving {
|
|
continue;
|
|
}
|
|
if let Ok(Some(res)) = res {
|
|
*keep_alive = res.is_persistent();
|
|
for chunk in res.buf.chunks(buf_len) {
|
|
send_queue.push_back(chunk.to_vec());
|
|
}
|
|
}
|
|
}
|
|
if socket.can_send() {
|
|
if let Some(chunk) = send_queue.pop_front() {
|
|
let sent = socket.send_slice(&chunk).
|
|
expect("Could not send chunk");
|
|
debug_assert!(sent == chunk.len());
|
|
}
|
|
}
|
|
if send_queue.is_empty() && !*keep_alive {
|
|
socket.close();
|
|
}
|
|
} else if socket.may_send() {
|
|
socket.close();
|
|
send_queue.clear();
|
|
}
|
|
}
|
|
if let Some(wait_duration) = iface.poll_delay(time, &sockets) {
|
|
let t = wait_duration.total_micros() / POLL_DELAY_DIV as u64;
|
|
if t > 0 {
|
|
syscall::sleep((t as f64) / 1000000.0);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
error!("Could not find network interface");
|
|
Err(ExitCode::Failure)
|
|
}
|
|
}
|
|
|
|
fn content_type(path: &str) -> String {
|
|
let ext = path.rsplit_once('.').unwrap_or(("", "")).1;
|
|
match ext {
|
|
"css" => "text/css",
|
|
"csv" => "text/csv",
|
|
"gif" => "text/gif",
|
|
"htm" | "html" => "text/html",
|
|
"jpg" | "jpeg" => "image/jpeg",
|
|
"js" => "text/javascript",
|
|
"json" => "application/json",
|
|
"lsp" | "lisp" => "text/plain",
|
|
"png" => "image/png",
|
|
"sh" => "application/x-sh",
|
|
"txt" => "text/plain",
|
|
_ => "application/octet-stream",
|
|
}.to_string()
|
|
}
|
|
|
|
// Join the requested file path to the root dir of the server
|
|
fn join_path(dir: &str, path: &str) -> String {
|
|
debug_assert!(dir.starts_with('/'));
|
|
debug_assert!(path.starts_with('/'));
|
|
let path = path.trim_matches('/');
|
|
let sep = if dir == "/" || path == "" { "" } else { "/" };
|
|
format!("{}{}{}", dir, sep, path)
|
|
}
|
|
|
|
fn usage() {
|
|
let csi_option = Style::color("LightCyan");
|
|
let csi_title = Style::color("Yellow");
|
|
let csi_reset = Style::reset();
|
|
println!(
|
|
"{}Usage:{} httpd {}<options>{1}",
|
|
csi_title, csi_reset, csi_option
|
|
);
|
|
println!();
|
|
println!("{}Options:{}", csi_title, csi_reset);
|
|
println!(
|
|
" {0}-d{1}, {0}--dir <path>{1} Set directory to {0}<path>{1}",
|
|
csi_option, csi_reset
|
|
);
|
|
println!(
|
|
" {0}-p{1}, {0}--port <number>{1} Listen to port {0}<number>{1}",
|
|
csi_option, csi_reset
|
|
);
|
|
println!(
|
|
" {0}-r{1}, {0}--read-only{1} Set read-only mode",
|
|
csi_option, csi_reset
|
|
);
|
|
}
|
|
|
|
#[test_case]
|
|
fn test_join_path() {
|
|
assert_eq!(join_path("/foo", "/bar/"), "/foo/bar");
|
|
assert_eq!(join_path("/foo", "/bar"), "/foo/bar");
|
|
assert_eq!(join_path("/foo", "/"), "/foo");
|
|
assert_eq!(join_path("/", "/bar/"), "/bar");
|
|
assert_eq!(join_path("/", "/bar"), "/bar");
|
|
assert_eq!(join_path("/", "/"), "/");
|
|
}
|