~blainsmith/static-httpd

79e4f70574697fc5f86564930a0c53a3d42b3261 — Blain Smith 2 months ago 567c9f3
handle errors and sort dir listing like `ls`

Signed-off-by: Blain Smith <rebelgeek@blainsmith.com>
1 files changed, 116 insertions(+), 67 deletions(-)

M cmd/static-httpd/main.ha
M cmd/static-httpd/main.ha => cmd/static-httpd/main.ha +116 -67
@@ 1,3 1,4 @@
use ascii;
use bufio;
use encoding::utf8;
use errors;


@@ 12,9 13,16 @@ use net::ip;
use net::tcp;
use os;
use path;
use sort;
use sort::cmp;
use strconv;
use strings;

type invalid = !void;

type error = (...errors::error | ...io::error | ...path::error |
...fs::error | invalid);

let mimetypes: [](str, str) = [];

@init fn init() void = {


@@ 64,6 72,11 @@ let mimetypes: [](str, str) = [];
	free(mimetypes);
};

fn cmpstrs(a: const *opaque, b: const *opaque) int = {
	const a = *(a: *str), b = *(b: *str);
	return ascii::strcasecmp(a, b);
};

export fn main() void = {
	let addr: ip::addr = ip::ANY_V6;
	let port = 8080u16;


@@ 97,43 110,62 @@ export fn main() void = {

	const cwd = os::getcwd();

	const sock = tcp::listen(addr, port)!;
	const sock = match (tcp::listen(addr, port)) {
	case let s: net::socket =>
		yield s;
	case let err: net::error =>
		log::fatalf(net::strerror(err));
	};
	defer net::close(sock)!;

	log::printfln("static-httpd running on [{}]:{}", ip::string(addr), port);

	for (true) {
		const conn = tcp::accept(sock)!;
		handle_conn(conn, cwd)!;
		net::close(conn)!;
		match (tcp::accept(sock)) {
		case let conn: net::socket =>
			match (handle_conn(conn, cwd)) {
			case let err: io::error =>
				log::println(io::strerror(err));
			case let err: fs::error =>
				log::println(fs::strerror(err));
			case let err: path::error =>
				log::println(path::strerror(err));
			case let err: invalid =>
				log::println("invalid request");
			case =>
				yield;
			};

			match (net::close(conn)) {
			case let err: net::error =>
				log::println(net::strerror(err));
			case =>
				yield;
			};
		case let err: net::error =>
			log::println(net::strerror(err));
		};
	};
};

fn handle_conn(conn: net::socket, cwd: str) (void | errors::error) = {
fn handle_conn(conn: net::socket, cwd: str) (size | error) = {
	let buf: [os::BUFSZ]u8 = [0...];

	const file = bufio::init(conn, [], buf);
	bufio::setflush(&file, []);
	defer bufio::flush(&file)!;

	const scan = bufio::newscanner(conn, 512);
	defer bufio::finish(&scan);

	const (method, path) = match (read_statusline(&scan)) {
	case let r: (str, str) =>
		yield (r.0, r.1);
	case void =>
		httperror(file, 500);
		return;
	};
	let (method, path) = read_statusline(&scan)?;

	const pathbuf = match (path::init(cwd, path)) {
	case let b: path::buffer =>
		yield b;
	case let err: path::error =>
		httperror(file, 500);
		log::printfln("path error: {}", path::strerror(err));
		return;
		log::printfln("method={} path={} status=500", method, path);
		httperror(file, 500)?;
		return err;
	};
	const fullpath = path::string(&pathbuf);



@@ 141,46 173,61 @@ fn handle_conn(conn: net::socket, cwd: str) (void | errors::error) = {
	case let s: fs::filestat =>
		yield s;
	case let err: fs::error =>
		httperror(file, 404);
		return;
		log::printfln("method={} path={} status=400", method, path);
		httperror(file, 404)?;
		return 0z;
	};

	if (fs::isdir(stat.mode)) {
		if (!strings::hassuffix(path, "/")) {
			fmt::fprint(&file, "HTTP/1.1 302 Found\r\n")!;
			fmt::fprint(&file, "Content-Type: text/html\r\n")!;
			fmt::fprint(&file, "Content-Length: 0\r\n")!;
			fmt::fprintf(&file, "Location: {}\r\n", strings::concat(path, "/"))!;
			return;
			path = strings::concat(path, "/");

			fmt::fprint(&file, "HTTP/1.1 302 Found\r\n")?;
			fmt::fprint(&file, "Content-Type: text/html\r\n")?;
			fmt::fprint(&file, "Content-Length: 0\r\n")?;
			fmt::fprintf(&file, "Location: {}\r\n", path)?;
			bufio::flush(&file)?;

			log::printfln("method={} path={} status=302", method, path);

			return 0;
		};

		const indexpath = strings::concat(fullpath, "/index.html");
		if (!os::exists(indexpath)) {
			const dirents = match (os::readdir(fullpath)) {
			let dirents = match (os::readdir(fullpath)) {
			case let de: []fs::dirent =>
				yield de;
			case let err: fs::error =>
				httperror(file, 500);
				log::printfln("readdir error: {}", fs::strerror(err));
				return;
				log::printfln("method={} path={} status=500", method, path);
				httperror(file, 500)?;
				return err;
			};

			const listing = memio::dynamic();
			let sz = fmt::fprint(&listing, "<html lang=""en""><head><title>static-httpd</title></head><body>")!;
			sz += fmt::fprintf(&listing, "<h1>Directory listing for {}</h1><hr /><table><thead><tr><th>Name</th><th>Size</th><th>Modified</th></tr></thead><tbody>", fullpath)!;
			let names: []str = [];
			for (let idx = 0z; idx < len(dirents); idx += 1) {
				const fname = dirents[idx].name;
				sz += fmt::fprintf(&listing, "<tr><td><a href=""./{}"">{}</a></td><td>{}</td><td>{}</td></tr>", fname, fname, "", "")!;
				append(names, dirents[idx].name);
			};
			sort::sort(names, size(str), &cmpstrs);

			const listing = memio::dynamic();
			let sz = fmt::fprint(&listing, "<html lang=""en""><head><title>static-httpd</title></head><body>")?;
			sz += fmt::fprintf(&listing, "<h1>Directory listing for {}</h1><hr /><table><thead><tr><th>Name</th><th>Size</th><th>Modified</th></tr></thead><tbody>", fullpath)?;
			for (let idx = 0z; idx < len(names); idx += 1) {
				sz += fmt::fprintf(&listing, "<tr><td><a href=""./{}"">{}</a></td><td>{}</td><td>{}</td></tr>", names[idx], names[idx], "", "")?;
			};
			sz += fmt::fprint(&listing, "</tbody></table></body></html>")!;
			sz += fmt::fprint(&listing, "</tbody></table></body></html>")?;
			fs::dirents_free(dirents);


			fmt::fprint(&file, "HTTP/1.1 200 OK\r\n")!;
			fmt::fprint(&file, "Content-Type: text/html\r\n")!;
			fmt::fprintf(&file, "Content-Length: {}\r\n\r\n", sz)!;
			io::write(&file, memio::buffer(&listing))!;
			return;
			fmt::fprint(&file, "HTTP/1.1 200 OK\r\n")?;
			fmt::fprint(&file, "Content-Type: text/html\r\n")?;
			fmt::fprintf(&file, "Content-Length: {}\r\n\r\n", sz)?;
			
			const sz = io::write(&file, memio::buffer(&listing))?;
			bufio::flush(&file)?;

			return sz;
		};

		fullpath = indexpath;


@@ 190,8 237,9 @@ fn handle_conn(conn: net::socket, cwd: str) (void | errors::error) = {
	case let f: io::file =>
		yield f;
	case let err: fs::error =>
		httperror(file, 404);
		return;
		log::printfln("method={} path={} status=400", method, path);
		httperror(file, 404)?;
		return 0z;
	};
	defer io::close(f)!;



@@ 199,33 247,36 @@ fn handle_conn(conn: net::socket, cwd: str) (void | errors::error) = {
	case let s: fs::filestat =>
		yield s;
	case let err: fs::error =>
		httperror(file, 500);
		log::printfln("stat error: {}", fs::strerror(err));
		return;
		log::printfln("method={} path={} status=500", method, path);
		httperror(file, 500)?;
		return 0z;
	};

	const (_, ext) = strings::rcut(fullpath, ".");
	const ct = typebyext(ext);

	fmt::fprint(&file, "HTTP/1.1 200 OK\r\n")!;
	fmt::fprintf(&file, "Content-Type: {}\r\n", ct)!;
	fmt::fprintf(&file, "Content-Length: {}\r\n\r\n", stat.sz)!;
	io::copy(&file, f)!;
	fmt::fprint(&file, "HTTP/1.1 200 OK\r\n")?;
	fmt::fprintf(&file, "Content-Type: {}\r\n", ct)?;
	fmt::fprintf(&file, "Content-Length: {}\r\n\r\n", stat.sz)?;
	
	const sz = io::copy(&file, f)?;
	bufio::flush(&file)?;

	log::printfln("method={} path={} status=200", method, path);

	return sz;
};

fn read_statusline(scan: *bufio::scanner) (void | (str, str)) = {
fn read_statusline(scan: *bufio::scanner) ((str, str) | io::error | invalid) = {
	const status = match (bufio::scan_string(scan, "\r\n")) {
	case let line: const str =>
		yield line;
	case let err: io::error =>
		log::printfln("error scanning: {}", io::strerror(err));
		return;
		return err;
	case io::EOF =>
		log::println("EOF");
		return;
		return invalid;
	case utf8::invalid =>
		log::println("invalid");
		return;
		return invalid;
	};

	const tok = strings::tokenize(status, " ");


@@ 234,16 285,14 @@ fn read_statusline(scan: *bufio::scanner) (void | (str, str)) = {
	case let m: str =>
		yield m;
	case void =>
		log::println("method not found");
		return;
		return invalid;
	};

	const path = match (strings::next_token(&tok)) {
	case let p: str =>
		yield p;
	case let r: str =>
		yield r;
	case void =>
		log::println("path not found");
		return;
		return invalid;
	};

	return (method, path);


@@ 261,15 310,15 @@ fn typebyext(ext: str) str = {
	return strings::dup(mt);
};

fn httperror(s: bufio::stream, code: int) void = {
	fmt::fprintf(&s, "HTTP/1.1 {} ", code)!;
	
fn httperror(s: bufio::stream, code: int) (void | io::error) = {
	switch (code) {
	case 404 =>
		fmt::fprint(&s, "Not found\r\n")!;
	case 500 =>
		fmt::fprint(&s, "Internal server error\r\n")!;
		fmt::fprintf(&s, "HTTP/1.1 {} ", code)?;
		fmt::fprint(&s, "Not found\r\n")?;
	case =>
		fmt::fprint(&s, "Teacup\r\n")!;
		fmt::fprint(&s, "HTTP/1.1 500 ")?;
		fmt::fprint(&s, "Internal server error\r\n")?;
	};

	return bufio::flush(&s);
};