initial commit

This commit is contained in:
Jeremy Baxter 2024-08-07 09:38:16 +12:00
commit a1d36338a1
5 changed files with 274 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*.o
httpd

20
Makefile Normal file
View file

@ -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

231
httpd.d Normal file
View file

@ -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;
}

10
static/fail.html Normal file
View file

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>{status}</title>
</head>
<body>
<h2>{status}</h2>
<hr><em>Jeremy's httpd</em>
</body>
</html>

11
static/index.html Normal file
View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<title>Index</title>
</head>
<body>
<h2>Welcome to httpd</h2>
You've reached the default page for Jeremy's httpd.
<hr><em>Jeremy's httpd</em>
</body>
</html>