mirror of https://github.com/adamdruppe/arsd.git
312 lines
6.9 KiB
D
312 lines
6.9 KiB
D
// This thing sucks. It's primarily to prove that cgi.d *can* work
|
|
// with an embedded http server without much difficulty, so you aren't
|
|
// tied to CGI if you don't want to be, even if you have a huge codebase
|
|
// built on cgi.d already.
|
|
|
|
// But this particular module is no where near ready for serious use.
|
|
// (it does do reasonably well under controlled conditions though)
|
|
|
|
module arsd.httpd;
|
|
|
|
public import arsd.cgi;
|
|
|
|
import arsd.netman;
|
|
|
|
import std.range;
|
|
|
|
/*
|
|
|
|
import arsd.curl;
|
|
void handler(Cgi cgi) {
|
|
cgi.write("hello world!");
|
|
cgi.close();
|
|
}
|
|
|
|
void main() {
|
|
serveHttp(&handler, 5000);
|
|
}
|
|
|
|
*/
|
|
|
|
void serveHttp(CustomCgi)(void function(Cgi) requestHandler, ushort port) if(is(CustomCgi: Cgi)) {
|
|
auto netman = new NetMan!(CustomCgi)(requestHandler);
|
|
netman.listen(port);
|
|
for(;;)
|
|
try
|
|
netman.proceed();
|
|
catch(ConnectionException e)
|
|
e.c.disconnectNow();
|
|
catch(Exception e)
|
|
writefln("Exception: %s", e.toString());
|
|
}
|
|
|
|
class NetMan(CustomCgi) : NetworkManager /*if(is(CustomCgi : Cgi))*/ {
|
|
void function(Cgi) requestHandler;
|
|
|
|
this(void function(Cgi) requestHandler) {
|
|
this.requestHandler = requestHandler;
|
|
}
|
|
|
|
override Connection allocConnection(int port) {
|
|
return new HttpdConnection!CustomCgi(requestHandler);
|
|
}
|
|
}
|
|
|
|
class HttpdConnection(CustomCgi) : Connection /* if(is(CustomCgi : Cgi)) */ {
|
|
// The way this rolls is to get the whole thing in memory, then pass it off to Cgi to do the rest
|
|
|
|
this(void function(Cgi) requestHandler) {
|
|
handler = requestHandler;
|
|
}
|
|
|
|
void function(Cgi) handler;
|
|
|
|
int state;
|
|
string[] headers;
|
|
immutable(ubyte)[] data;
|
|
int contentLength;
|
|
bool chunked;
|
|
int chunkSize = 0;
|
|
string separator = "\r\n";
|
|
bool closeConnection;
|
|
|
|
void log(in ubyte[] a) {
|
|
data ~= a;
|
|
}
|
|
|
|
void finishRequest() {
|
|
state = 0;
|
|
separator = "\r\n";
|
|
|
|
// writeln("FINISHED");
|
|
|
|
scope(exit) {
|
|
if(closeConnection)
|
|
disconnect();
|
|
closeConnection = false;
|
|
}
|
|
|
|
Cgi cgi;
|
|
|
|
try {
|
|
cgi = new CustomCgi(headers, data, peerAddress(),
|
|
cast(void delegate(const(ubyte)[])) &this.write, 0, &this.flush);
|
|
} catch(Throwable t) {
|
|
write("HTTP/1.1 400 Bad Request\r\n");
|
|
write("Content-Type: text/plain\r\n");
|
|
string s = t.toString();
|
|
write("Content-Length: "~to!string(s.length)~"\r\n");
|
|
write("Connection: close\r\n");
|
|
write("\r\n");
|
|
write(s);
|
|
|
|
closeConnection = true;
|
|
|
|
return;
|
|
}
|
|
|
|
scope(exit) {
|
|
cgi.close();
|
|
cgi.dispose();
|
|
}
|
|
|
|
try {
|
|
handler(cgi);
|
|
} catch(Throwable e) {
|
|
cgi.setResponseStatus("500 Internal Server Error");
|
|
cgi.write(e.toString());
|
|
}
|
|
}
|
|
|
|
override void onDataReceived(){
|
|
auto a = read();
|
|
|
|
// writeln("data received ", state, "\n", cast(string) a);
|
|
|
|
more:
|
|
switch(state) {
|
|
default: assert(0);
|
|
case 0: // reading the headers
|
|
// while it's supposed to be \r\n, we want
|
|
// to be permissive here to avoid hanging if
|
|
// the wrong thing comes.
|
|
try_again:
|
|
int l = locationOf(a, separator ~ separator);
|
|
if(l == -1) {
|
|
if(separator.length > 1) {
|
|
separator = "\n";
|
|
goto try_again;
|
|
} else {
|
|
separator = "\r\n";
|
|
}
|
|
|
|
return; // not enough data
|
|
}
|
|
changeReadPosition(l+separator.length * 2); // we're now at the beginning of the data
|
|
|
|
data.length = 0;
|
|
contentLength = 0;
|
|
|
|
string hdrs = cast(string) a[0..l].idup;
|
|
a = read(); // advance ourselves
|
|
headers = hdrs.split(separator);
|
|
|
|
chunked = false;
|
|
|
|
if(headers.length == 0) {
|
|
disconnect();
|
|
return;
|
|
}
|
|
|
|
if(headers[0].indexOf("HTTP/1.0") != -1)
|
|
closeConnection = true; // always one request per connection with 1.0
|
|
|
|
foreach(ref h; headers[1..$]) {
|
|
int colon = h.indexOf(":");
|
|
if(colon == -1)
|
|
throw new Exception("Http headers need colons");
|
|
string name = h[0..colon].tolower;
|
|
string value = h[colon+2..$]; // FIXME?
|
|
|
|
switch(name) {
|
|
case "transfer-encoding":
|
|
if(value == "chunked")
|
|
chunked = true;
|
|
break;
|
|
case "content-length":
|
|
contentLength = to!int(value);
|
|
break;
|
|
case "connection":
|
|
if(value == "close")
|
|
closeConnection = true;
|
|
break;
|
|
default:
|
|
// leave it unmolested for passthrough
|
|
}
|
|
}
|
|
|
|
// forward the header and advance our state
|
|
state = 1;
|
|
// break; // fall through to read some more data if we have any
|
|
case 1: // reading Content-Length type data
|
|
// We need to read up the data we have, and write it out as a chunk.
|
|
if(!chunked) {
|
|
if(a.length <= contentLength) {
|
|
log(a);
|
|
contentLength -= a.length;
|
|
resetRead();
|
|
// we just finished it off, terminate the chunks
|
|
if(contentLength == 0) {
|
|
finishRequest();
|
|
}
|
|
} else {
|
|
// we actually have *more* here than we need....
|
|
log(a[0..contentLength]);
|
|
contentLength = 0;
|
|
finishRequest();
|
|
|
|
changeReadPosition(contentLength);
|
|
a = read();
|
|
// we're done
|
|
goto more; // see if we can make use of the rest of the data
|
|
}
|
|
} else {
|
|
// decode it, modify it, then reencode it
|
|
// If here, we are at the beginning of a chunk.
|
|
int loc = locationOf(a, "\r\n");
|
|
if(loc == -1) {
|
|
return; // don't have the length
|
|
}
|
|
|
|
string hex;
|
|
hex = "";
|
|
for(int i = 0; i < loc; i++) {
|
|
char c = a[i];
|
|
if(c >= 'A' && c <= 'Z')
|
|
c += 0x20;
|
|
if((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z')) {
|
|
hex ~= c;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
assert(hex.length);
|
|
|
|
int power = 1;
|
|
int size = 0;
|
|
foreach(cc1; retro(hex)) {
|
|
dchar cc = cc1;
|
|
if(cc >= 'a' && cc <= 'z')
|
|
cc -= 0x20;
|
|
int val = 0;
|
|
if(cc >= '0' && cc <= '9')
|
|
val = cc - '0';
|
|
else
|
|
val = cc - 'A' + 10;
|
|
|
|
size += power * val;
|
|
power *= 16;
|
|
}
|
|
|
|
chunkSize = size;
|
|
assert(size >= 0);
|
|
|
|
if(loc + 2 > a.length) {
|
|
return; // need more data
|
|
}
|
|
changeReadPosition(loc+2); // skips the chunk header
|
|
a = read();
|
|
|
|
if(chunkSize == 0) { // we're done with the response
|
|
state = 3;
|
|
goto more;
|
|
} else {
|
|
state = 2;
|
|
goto more;
|
|
}
|
|
|
|
resetRead();
|
|
}
|
|
break;
|
|
case 2: // reading a chunk
|
|
// if we got here, will change must be true....
|
|
if(a.length < chunkSize + 2) {
|
|
return; // we want to handle the whole chunk at once
|
|
}
|
|
|
|
log(a[0..chunkSize]);
|
|
|
|
state = 1;
|
|
|
|
if(a.length > chunkSize + 2) {
|
|
assert(a[chunkSize] == 13);
|
|
assert(a[chunkSize+1] == 10);
|
|
changeReadPosition(chunkSize + 2); // skip the \r\n
|
|
a = read();
|
|
chunkSize = 0;
|
|
state = 1;
|
|
goto more;
|
|
} else {
|
|
chunkSize = 0;
|
|
resetRead();
|
|
}
|
|
break;
|
|
case 3: // reading footers
|
|
// if we got here, will change must be true....
|
|
int loc = locationOf(a, "\r\n");
|
|
if(loc == -1) {
|
|
return; // not done yet
|
|
} else {
|
|
assert(loc == 0);
|
|
changeReadPosition(loc+2); // FIXME: should handle footers properly
|
|
finishRequest();
|
|
a = read();
|
|
|
|
goto more;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|