extern crate alloc;
use alloc::vec::Vec;
use crate::io;
use crate::sys;
use super::get_arg;
#[cfg(target_os = "linux")]
pub fn httpd(argc: i32, argv: *const *const u8) -> i32 {
let mut foreground = false;
let mut port: u16 = 80;
let mut doc_root: &[u8] = b".";
let mut verbose = false;
let mut i = 1;
while i < argc as usize {
let arg = match unsafe { get_arg(argv, i as i32) } {
Some(a) => a,
None => break,
};
if arg == b"-f" {
foreground = true;
} else if arg == b"-p" {
i += 1;
if let Some(p) = unsafe { get_arg(argv, i as i32) } {
port = sys::parse_u64(p).unwrap_or(80) as u16;
}
} else if arg == b"-h" {
i += 1;
if let Some(h) = unsafe { get_arg(argv, i as i32) } {
doc_root = h;
}
} else if arg == b"-v" {
verbose = true;
} else if arg == b"--help" {
print_usage();
return 0;
}
i += 1;
}
if io::chdir(doc_root) < 0 {
io::write_str(2, b"httpd: cannot chdir to ");
io::write_all(2, doc_root);
io::write_str(2, b"\n");
return 1;
}
let listen_fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_STREAM, 0) };
if listen_fd < 0 {
io::write_str(2, b"httpd: socket failed\n");
return 1;
}
let opt: i32 = 1;
unsafe {
libc::setsockopt(listen_fd, libc::SOL_SOCKET, libc::SO_REUSEADDR,
&opt as *const _ as *const libc::c_void,
core::mem::size_of::<i32>() as u32);
}
let mut addr: libc::sockaddr_in = unsafe { core::mem::zeroed() };
addr.sin_family = libc::AF_INET as u16;
addr.sin_port = port.to_be();
addr.sin_addr.s_addr = 0;
if unsafe { libc::bind(listen_fd, &addr as *const _ as *const libc::sockaddr,
core::mem::size_of::<libc::sockaddr_in>() as u32) } < 0 {
io::write_str(2, b"httpd: bind failed (port ");
let mut buf = [0u8; 16];
io::write_all(2, sys::format_u64(port as u64, &mut buf));
io::write_str(2, b")\n");
io::close(listen_fd);
return 1;
}
if unsafe { libc::listen(listen_fd, 10) } < 0 {
io::write_str(2, b"httpd: listen failed\n");
io::close(listen_fd);
return 1;
}
if !foreground {
let pid = unsafe { libc::fork() };
if pid < 0 {
io::write_str(2, b"httpd: fork failed\n");
io::close(listen_fd);
return 1;
}
if pid > 0 {
return 0;
}
unsafe { libc::setsid() };
io::close(0);
io::close(1);
io::close(2);
let null_fd = io::open(b"/dev/null", libc::O_RDWR, 0);
if null_fd >= 0 {
io::dup2(null_fd, 0);
io::dup2(null_fd, 1);
io::dup2(null_fd, 2);
if null_fd > 2 {
io::close(null_fd);
}
}
} else if verbose {
io::write_str(1, b"httpd: listening on port ");
let mut buf = [0u8; 16];
io::write_all(1, sys::format_u64(port as u64, &mut buf));
io::write_str(1, b"\n");
}
loop {
let mut client_addr: libc::sockaddr_in = unsafe { core::mem::zeroed() };
let mut addr_len = core::mem::size_of::<libc::sockaddr_in>() as u32;
let client_fd = unsafe {
libc::accept(listen_fd, &mut client_addr as *mut _ as *mut libc::sockaddr, &mut addr_len)
};
if client_fd < 0 {
continue;
}
let pid = unsafe { libc::fork() };
if pid == 0 {
io::close(listen_fd);
handle_request(client_fd, verbose);
io::close(client_fd);
unsafe { libc::_exit(0) };
} else {
io::close(client_fd);
unsafe {
libc::waitpid(-1, core::ptr::null_mut(), libc::WNOHANG);
}
}
}
}
#[cfg(target_os = "linux")]
fn handle_request(fd: i32, _verbose: bool) {
let mut buf = [0u8; 4096];
let n = io::read(fd, &mut buf);
if n <= 0 {
return;
}
let request = &buf[..n as usize];
let mut pos = 0;
while pos < request.len() && request[pos] != b' ' {
pos += 1;
}
let method = &request[..pos];
while pos < request.len() && request[pos] == b' ' {
pos += 1;
}
let path_start = pos;
while pos < request.len() && request[pos] != b' ' && request[pos] != b'?' {
pos += 1;
}
let path = &request[path_start..pos];
if method != b"GET" {
send_error(fd, 405, b"Method Not Allowed");
return;
}
let decoded_path = decode_url(path);
if has_dotdot(&decoded_path) {
send_error(fd, 403, b"Forbidden");
return;
}
let file_path = if decoded_path.len() <= 1 {
b"index.html".to_vec()
} else {
decoded_path[1..].to_vec()
};
let final_path = if is_directory(&file_path) {
let mut p = file_path.clone();
if !p.ends_with(b"/") {
p.push(b'/');
}
p.extend_from_slice(b"index.html");
p
} else {
file_path
};
let file_fd = io::open(&final_path, libc::O_RDONLY, 0);
if file_fd < 0 {
send_error(fd, 404, b"Not Found");
return;
}
let mut stat_buf = io::stat_zeroed();
if io::fstat(file_fd, &mut stat_buf) < 0 {
io::close(file_fd);
send_error(fd, 500, b"Internal Server Error");
return;
}
let content_length = stat_buf.st_size as u64;
let content_type = get_mime_type(&final_path);
io::write_all(fd, b"HTTP/1.0 200 OK\r\n");
io::write_all(fd, b"Server: armybox httpd\r\n");
io::write_all(fd, b"Content-Type: ");
io::write_all(fd, content_type);
io::write_all(fd, b"\r\n");
io::write_all(fd, b"Content-Length: ");
let mut len_buf = [0u8; 20];
io::write_all(fd, sys::format_u64(content_length, &mut len_buf));
io::write_all(fd, b"\r\n");
io::write_all(fd, b"Connection: close\r\n");
io::write_all(fd, b"\r\n");
let mut send_buf = [0u8; 8192];
loop {
let n = io::read(file_fd, &mut send_buf);
if n <= 0 {
break;
}
io::write_all(fd, &send_buf[..n as usize]);
}
io::close(file_fd);
}
#[cfg(target_os = "linux")]
fn send_error(fd: i32, code: u32, message: &[u8]) {
let mut buf = [0u8; 16];
io::write_all(fd, b"HTTP/1.0 ");
io::write_all(fd, sys::format_u64(code as u64, &mut buf));
io::write_all(fd, b" ");
io::write_all(fd, message);
io::write_all(fd, b"\r\n");
io::write_all(fd, b"Content-Type: text/html\r\n");
io::write_all(fd, b"Connection: close\r\n");
io::write_all(fd, b"\r\n");
io::write_all(fd, b"<html><body><h1>");
io::write_all(fd, sys::format_u64(code as u64, &mut buf));
io::write_all(fd, b" ");
io::write_all(fd, message);
io::write_all(fd, b"</h1></body></html>\n");
}
fn decode_url(url: &[u8]) -> Vec<u8> {
let mut result = Vec::with_capacity(url.len());
let mut i = 0;
while i < url.len() {
if url[i] == b'%' && i + 2 < url.len() {
let h1 = hex_digit(url[i + 1]);
let h2 = hex_digit(url[i + 2]);
if let (Some(d1), Some(d2)) = (h1, h2) {
result.push((d1 << 4) | d2);
i += 3;
continue;
}
}
result.push(url[i]);
i += 1;
}
result
}
fn hex_digit(c: u8) -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'a'..=b'f' => Some(c - b'a' + 10),
b'A'..=b'F' => Some(c - b'A' + 10),
_ => None,
}
}
fn has_dotdot(path: &[u8]) -> bool {
let mut i = 0;
while i < path.len() {
if path[i] == b'.' && i + 1 < path.len() && path[i + 1] == b'.' {
let before_ok = i == 0 || path[i - 1] == b'/';
let after_ok = i + 2 >= path.len() || path[i + 2] == b'/';
if before_ok && after_ok {
return true;
}
}
i += 1;
}
false
}
fn is_directory(path: &[u8]) -> bool {
let mut stat_buf = io::stat_zeroed();
if io::stat(path, &mut stat_buf) < 0 {
return false;
}
(stat_buf.st_mode & libc::S_IFMT) == libc::S_IFDIR
}
fn get_mime_type(path: &[u8]) -> &'static [u8] {
let mut ext_start = path.len();
for i in (0..path.len()).rev() {
if path[i] == b'.' {
ext_start = i + 1;
break;
}
if path[i] == b'/' {
break;
}
}
if ext_start >= path.len() {
return b"application/octet-stream";
}
let ext = &path[ext_start..];
if ext == b"html" || ext == b"htm" {
b"text/html"
} else if ext == b"css" {
b"text/css"
} else if ext == b"js" {
b"application/javascript"
} else if ext == b"json" {
b"application/json"
} else if ext == b"txt" {
b"text/plain"
} else if ext == b"xml" {
b"application/xml"
} else if ext == b"png" {
b"image/png"
} else if ext == b"jpg" || ext == b"jpeg" {
b"image/jpeg"
} else if ext == b"gif" {
b"image/gif"
} else if ext == b"svg" {
b"image/svg+xml"
} else if ext == b"ico" {
b"image/x-icon"
} else if ext == b"pdf" {
b"application/pdf"
} else if ext == b"zip" {
b"application/zip"
} else if ext == b"tar" {
b"application/x-tar"
} else if ext == b"gz" {
b"application/gzip"
} else if ext == b"mp3" {
b"audio/mpeg"
} else if ext == b"mp4" {
b"video/mp4"
} else if ext == b"webm" {
b"video/webm"
} else if ext == b"woff" {
b"font/woff"
} else if ext == b"woff2" {
b"font/woff2"
} else {
b"application/octet-stream"
}
}
fn print_usage() {
io::write_str(1, b"Usage: httpd [-f] [-p PORT] [-h HOME]\n\n");
io::write_str(1, b"Simple HTTP server.\n\n");
io::write_str(1, b"Options:\n");
io::write_str(1, b" -f Stay in foreground\n");
io::write_str(1, b" -p PORT Listen on PORT (default: 80)\n");
io::write_str(1, b" -h HOME Document root (default: .)\n");
io::write_str(1, b" -v Verbose mode\n");
}
#[cfg(not(target_os = "linux"))]
pub fn httpd(_argc: i32, _argv: *const *const u8) -> i32 {
io::write_str(2, b"httpd: only available on Linux\n");
1
}
#[cfg(test)]
mod tests {
extern crate std;
use std::process::Command;
use std::path::PathBuf;
fn get_armybox_path() -> PathBuf {
if let Ok(path) = std::env::var("ARMYBOX_PATH") {
return PathBuf::from(path);
}
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| std::env::current_dir().unwrap());
let release = manifest_dir.join("target/release/armybox");
if release.exists() { return release; }
manifest_dir.join("target/debug/armybox")
}
#[test]
fn test_httpd_help() {
let armybox = get_armybox_path();
if !armybox.exists() { return; }
let output = Command::new(&armybox)
.args(["httpd", "--help"])
.output()
.unwrap();
assert_eq!(output.status.code(), Some(0));
let stdout = std::string::String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Usage"));
}
#[test]
fn test_mime_types() {
use super::get_mime_type;
assert_eq!(get_mime_type(b"index.html"), b"text/html");
assert_eq!(get_mime_type(b"style.css"), b"text/css");
assert_eq!(get_mime_type(b"app.js"), b"application/javascript");
assert_eq!(get_mime_type(b"image.png"), b"image/png");
assert_eq!(get_mime_type(b"data.json"), b"application/json");
}
#[test]
fn test_has_dotdot() {
use super::has_dotdot;
assert!(has_dotdot(b".."));
assert!(has_dotdot(b"/../"));
assert!(has_dotdot(b"/foo/../bar"));
assert!(!has_dotdot(b"/foo/bar"));
assert!(!has_dotdot(b"/foo..bar"));
}
#[test]
fn test_decode_url() {
use super::decode_url;
assert_eq!(decode_url(b"hello%20world"), b"hello world");
assert_eq!(decode_url(b"test%2Fpath"), b"test/path");
assert_eq!(decode_url(b"normal"), b"normal");
}
}