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