arsd/obsolete/httpd.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;
}
}
}