initial commit
This commit is contained in:
commit
a1d36338a1
5 changed files with 274 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*.o
|
||||||
|
httpd
|
20
Makefile
Normal file
20
Makefile
Normal 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
231
httpd.d
Normal 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
10
static/fail.html
Normal 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
11
static/index.html
Normal 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>
|
Loading…
Add table
Add a link
Reference in a new issue