From a1d36338a1e8537640cac20e2c4b65c43afb60f1 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 7 Aug 2024 09:38:16 +1200 Subject: [PATCH] initial commit --- .gitignore | 2 + Makefile | 20 ++++ httpd.d | 231 ++++++++++++++++++++++++++++++++++++++++++++++ static/fail.html | 10 ++ static/index.html | 11 +++ 5 files changed, 274 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 httpd.d create mode 100644 static/fail.html create mode 100644 static/index.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a452358 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.o +httpd \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a96df30 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +DC = ldc2 +CFLAGS = -Jstatic -Oz + +OBJS = httpd.o + +all: httpd + +httpd: ${OBJS} + ${DC} -of=httpd ${OBJS} + +.SUFFIXES: .d .o +.d.o: + ${DC} ${CFLAGS} -c $< + +clean: + rm -f httpd ${OBJS} + +$(shell ${DC} -o- -makedeps ${OBJS}) + +.PHONY: all clean diff --git a/httpd.d b/httpd.d new file mode 100644 index 0000000..69d19e6 --- /dev/null +++ b/httpd.d @@ -0,0 +1,231 @@ +module httpd; + +import std.algorithm; +import std.array; +import std.conv; +import std.exception; +import std.format; +import std.socket; +import std.stdio; +import std.string; + +enum HTTPMethod +{ + GET, + HEAD, + POST, + PUT, + DELETE, + CONNECT, + OPTIONS, + TRACE, + invalid +} + +struct HTTPRequest +{ + HTTPMethod method; + string resource; + string[string] parameters; + string[string] headers; + string httpVersion = "HTTP/1.1"; + + alias path = resource; +} + +struct HTTPResponse +{ + string status; + string[string] headers; + string responseBody; + string httpVersion = "HTTP/1.1"; +} + +class HttpdException : Exception +{ + mixin basicExceptionCtors; +} + +int +main(string[] args) +{ + char[1024] buffer; + Socket[] clients; + SocketSet readSet; + Socket listener; + + listener = new Socket(AddressFamily.INET, SocketType.STREAM); + listener.bind(new InternetAddress("127.0.0.1", 80)); + listener.listen(32); + + readSet = new SocketSet(); + + while (true) { + readSet.reset(); + readSet.add(listener); + foreach (Socket client; clients) { + readSet.add(client); + } + + if (Socket.select(readSet, null, null)) { + foreach (ulong i, Socket client; clients) { + if (readSet.isSet(client)) { + client.send(tryRequest(buffer[0 .. client.receive(buffer)])); + clients.remove(i); + } + } + if (readSet.isSet(listener)) { + clients ~= listener.accept(); + } + } + } + return 0; +} + +string +tryRequest(char[] requestBody) +{ + HTTPRequest rq; + + try { + rq = parseHTTPRequest(requestBody); + if (rq.method != HTTPMethod.GET) + return failurePage("405 Method Not Allowed"); + return rq.resource == "/" + || rq.resource == "/index.html" + ? respondWithPage(import("index.html")) + : failurePage("404 Not Found"); + } catch (HttpdException e) + return failurePage(e.msg); +} + +string +failurePage(string status) +{ + return respondWithPage( + import("fail.html") + .replace("{status}", status), + status); +} + +string +respondWithPage(string pageBody, string status = "200 OK", + string contentType = "text/html") +{ + return makeHTTPResponse( + HTTPResponse( + status, + ["Content-Type": contentType], + pageBody)); +} + +string +makeHTTPResponse(in HTTPResponse resp) +{ + string[string] defaultHeaders, headers; + char[] result; + + defaultHeaders = [ + "Content-Length": resp.responseBody.length.to!string(), + "Server": "Jeremy's httpd" + ]; + + result = (resp.httpVersion ~ ' ' ~ resp.status).dup(); + + headers = mergeHeaders(defaultHeaders, resp.headers); + foreach (string header; headers.byKey()) { + result ~= "\r\n" ~ header ~ ": " ~ headers[header]; + } + + finishResponse: + return result.idup() ~ "\r\n\r\n" ~ resp.responseBody; +} + +string[string] +mergeHeaders(const(string[string]) a1, const(string[string]) a2) +{ + string[string] result = cast(string[string])a1.dup(); + + foreach (string key; a2.byKey()) { + result[key] = a2[key]; + } + + return result; +} + +HTTPRequest +parseHTTPRequest(in char[] request) +{ + HTTPRequest result; + + result = parseHTTPRequestHeading(request + .findSplitBefore("\r\n")[0] + .idup()); + return result; +} + +HTTPRequest +parseHTTPRequestHeading(string heading) +{ + HTTPRequest result; + string methodString, path, params; + + heading = heading.dup(); + + auto tup = heading.findSplit(" "); + methodString = tup[0]; + heading = tup[2]; + + switch (methodString) { + case "GET": + result.method = HTTPMethod.GET; + break; + case "HEAD": + result.method = HTTPMethod.HEAD; + break; + case "POST": + result.method = HTTPMethod.POST; + break; + case "PUT": + result.method = HTTPMethod.PUT; + break; + case "DELETE": + result.method = HTTPMethod.DELETE; + break; + case "CONNECT": + result.method = HTTPMethod.CONNECT; + break; + case "OPTIONS": + result.method = HTTPMethod.OPTIONS; + break; + case "TRACE": + result.method = HTTPMethod.TRACE; + break; + default: + throw new HttpdException("400 Bad Request"); + } + + tup = heading.findSplit(" "); + path = tup[0]; + params = path.findSplitAfter("?")[1]; + heading = tup[2]; + + params = params.length == path.length ? "" : params; + + if (path.length == 0 || path[0] != '/') + throw new HttpdException("400 Bad Request"); + + result.path = path; + + if (params.length == 0) + return result; + + foreach (string param; params.split("&")) { + auto pair = param.findSplit("="); + result.parameters[pair[0]] = pair[2]; + } + + result.httpVersion = heading.strip(); + + return result; +} diff --git a/static/fail.html b/static/fail.html new file mode 100644 index 0000000..cad9ad3 --- /dev/null +++ b/static/fail.html @@ -0,0 +1,10 @@ + + + + {status} + + +

{status}

+
Jeremy's httpd + + diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..1b73dc8 --- /dev/null +++ b/static/index.html @@ -0,0 +1,11 @@ + + + + Index + + +

Welcome to httpd

+ You've reached the default page for Jeremy's httpd. +
Jeremy's httpd + +