import arsd.cginew; /* TODO Headers from request handler (namely, cookie and location, but probably caching ones too) Fix the date header to be GMT Add referrer Add correcting caching responses (Cache-control, expires, not modified, etc. Add www.* rewriting 301 Moved Permanently 301 The new permanent URI SHOULD be given by the Location field in the response. Unless the request method was HEAD, the entity of the response SHOULD contain a short hypertext note with a hyperlink to the new URI(s). Add better buffering Add HEAD support Add the 100 Continue response 303: See Other Prettify the error messages Make sure I'm sending the correct error message */ import arsd.netman; static import std.date; import std.string; import std.conv; static import std.uri; import std.stdio; class HttpConnection : Connection { public: static Connection spawn() { return new HttpConnection(&doHttpRequest); } this( int function(string method, string uri, string host, string userAgent, string cookie, string remote_addr, string contentType, const(byte)[] content, out string type, out byte[] response, out string otherHeaders) adoRequest ) { doRequest = adoRequest; super(); } int function(string method, string uri, string host, string userAgent, string cookie, string remote_addr, string contentType, const(byte)[] content, out string type, out byte[] response, out string otherHeaders) doRequest; // returns -1 if cannot parse, otherwise ending index of the request // if return != -1, you should send the response and maybe close int tryParse(const(byte)[] data, out byte[] response, out bool closeConnection) { int place = 0; bool first = true; bool using1; string method; string uri; string host; string userAgent; string cookie; bool keepAlive = false; string contentType; int contentLength; try { // I feel like an assembly programmer... goto realStuff; ///////// void preconditionFailed(){ response = cast(byte[]) "HTTP/1.1 412 Precondition Failed\n\n"; } void notModified() { auto date = std.date.toString(std.date.getUTCtime()); // FIXME response = cast(byte[]) format("HTTP/1.1 304 Not Modified\nDate: %s\n\n", date); } // maybe FIXME for the above too with keep alive void ok(string type, byte[] contents, string otherHeaders){ response = makeResponse("200 OK", type, contents, using1, keepAlive, otherHeaders); } void forbidden(){ response = makeResponse("403 Forbidden", "text/html", "hello

hello

", using1, keepAlive); } void notFound(){ response = makeResponse("404 Not Found", "text/html", "hello

hello

", using1, keepAlive); } // The following abort the connection altogether badRequest: { response = makeResponse("400 Bad Request", "text/html", "hello

hello

", using1, false); closeConnection = true; return data.length; } notImplemented: { response = makeResponse("501 Not Implemented", "text/html", "hello

hello

", using1, false); closeConnection = true; return data.length; } internalServerError: { response = makeResponse("500 Internal Server Error", "text/html", "hello

hello

", using1, false); closeConnection = true; return data.length; } ///////// realStuff: // FIXME: support chunked encoding // FIXME: do absolute URL http://www.jmarshall.com/easy/http/#headerlines // FIXME: should return -1 if the headers are incomplete bool wantBreak; while(!wantBreak && place < data.length) { int start = place; int end = place; while(data[end] != 10) end++; string header; // FIXME: doesn't do wrapped headers if(data[end-1] == 13) header = cast(string) data[start..end-1]; else header = cast(string) data[start..end]; place = end + 1; if(place >= data.length) return -1; // we haven't received all the headers yet, so wait on more if(data[place] == 10 || data[place] == 13) { if(data[place] == 13) place += 2; else place++; wantBreak = true; } // we get to parse the header if(first) { string[] request = split(header); method = request[0]; uri = std.uri.decode(request[1]); // FIXME: leaves behind some special chars which we might not want. Maybe. string protocol = request[2]; switch(protocol){ case "HTTP/1.0": using1 = true; break; case "HTTP/1.1": using1 = false; break; default: goto notImplemented; } switch(method) { case "GET": case "HEAD": case "POST": // do nothing; method is already stored break; default: goto notImplemented; } first = false; } else { string name; string contents; int f = indexOf(header, ":"); if(f == -1) goto badRequest; name = tolower(header[0..f]); contents = header[f+2..$]; switch(name) { case "user-agent": userAgent = contents; break; case "host": host = contents; break; case "connection": switch(contents) { case "keep-alive": keepAlive = true; break; case "close": keepAlive = false; break; default: keepAlive = true; // goto badRequest; } break; case "referer": // FIXME break; case "cookie": if(cookie.length) cookie ~= ";"~contents; else cookie = contents; break; case "if-modified-since": case "if-unmodified-since": goto notImplemented; // FIXME break; //case "keep-alive": //case "accept": // for POST case "content-type": contentType = contents; // application/x-www-form-urlencoded break; case "content-length": // FIXME: might want to catch exception contentLength = to!(int)(contents); break; default: // just ignoring unknown ones } } } closeConnection = ! keepAlive; // Now, we actually read in the rest of the data, if there is any. // If we don't have all the data, wait on more. if(place + contentLength > data.length) return -1; const(byte)[] content = data[place..place+contentLength]; place += contentLength; // And then answer the request. //ok("text/html", cast(byte[]) "hello

hello

"); // Send this off to another function to fulfill... int number; string type, otherHeaders; try number = doRequest(method, uri, host, userAgent, cookie, "", contentType, content, type, response, otherHeaders); // FIXME: should have remote_addr in there and referrer catch (Throwable e) { response = makeResponse("500 Internal Server Error", "text/html", "Exception

"~e.toString()~"

", using1, false); closeConnection = true; return data.length; } switch(number) { case 200: // OK ok(type, response, otherHeaders); break; case 403: // Forbidden forbidden(); break; case 404: // Not Found notFound(); break; //case 303: // See Other // FIXME: implement this! //break; // FIXME these need adding: /* 412 Precondition Failed 302 Not Modified */ } return place; } catch (Exception e) { response = makeResponse("500 Internal Server Error", "text/html", "hello

exception: "~e.toString~"

", using1, false); closeConnection = true; return data.length; } } override void onDataReceived(){ auto a = read(); //writefln("%s", cast(string) a); // stuff to send: Last-Modified, Server /* if-modified-since can return HTTP/1.1 304 Not Modified Date: Fri, 31 Dec 1999 23:59:59 GMT [blank line here] the opposite can do: HTTP/1.1 412 Precondition Failed [blank line here] */ // we have uri, host, method, and cookie int goodPlace = 0; int place; byte[] response; bool closeConnection; more: place = tryParse(a, response, closeConnection); if(place == -1) { changeReadPosition(goodPlace); return; // do nothing; we're waiting on more data } else goodPlace = place; //writefln("\033[32m%s\033[30m", cast(string) response); write(response); if(closeConnection) { disconnect(); return; } if(place < a.length) goto more; resetRead(); } byte[] makeResponse(string code, string type, string content, bool keepalive, bool ishttp1){ return makeResponse(code, type, cast(byte[]) content, keepalive, ishttp1); } byte[] makeResponse(string code, string type, byte[] content, bool keepalive, bool ishttp1, string otherHeaders = ""){ byte[] response; if(ishttp1) { auto date = std.date.toString(std.date.getUTCtime()); // FIXME response = cast(byte[]) (format("HTTP/1.1 %s\nDate: %s\nConnection: %s\nServer: ARSD\nContent-type: %s\n%sContent-length: %d\n\n", code, date, keepalive ? "keep-alive" : "close", type, otherHeaders, content.length)) ~ content; } else { response = cast(byte[]) (format("HTTP/1.0 %s\nContent-type: %s\n%sContent-length: %d\n\n", code, type, otherHeaders, content.length)) ~ content; } return response; } } //============= /+ class CGIHandler { void handleRequest(string method, string uri, string userAgent, string cookie, string remote_addr, string contentType, const(byte)[] content, out string type, out byte[] response) { void acceptResponse(string type, const(byte[]) data){ contentType = type; response = cast(byte[]) data; } prepareFromEmbeddedServer (method, uri, userAgent, cookie, remote_addr, contentType, content, &acceptResponse); lolmain(); } } +/ /* File path to file file type CGI program path to program Embedded program function name to call */ interface Resource { void call(); string type(); const(byte[]) response(); bool hack(); } class EmbeddedProgram : Resource { this(void function() func) { f = func; } string type() { return ctype; } const(byte[]) response() { return data; } bool hack() { return true; } void call() { f(); } void function() f; string name; string ctype; byte[] data; } class CgiProgram : Resource { this(string filename) { name = filename; } string type() { return ctype; } const(byte[]) response() { return data; } bool hack() { throw new Exception("not implemented"); } void call() { // FIXME } string name; string ctype; byte[] data; } static import std.file; static import std.path; class File : Resource { this(string filename) { name = filename; } bool hack() { return false; } void call() { data = cast(invariant(byte)[]) std.file.read(name); string ext = std.path.getExt(name); switch(ext) { case "png": ctype = "image/png"; break; case "js": ctype = "application/javascript"; break; case "jpg": ctype = "image/jpeg"; break; case "txt": ctype = "text/plain"; break; case "html": ctype = "text/html"; break; case "css": ctype = "text/css"; break; default: ctype = "application/octet-stream"; } } string type() { return ctype; } const(byte[]) response() { return data; } string name; string ctype; invariant(byte)[] data; } /* avatars/ graphs/ equations/ images/ files/ */ /* Commands: stats top referrers top users reset stats reset referrers reset users terminate add resource file/cgi remove resource list resources pause 503 Service Unavailable (Retry-After: val) resume Stats: Referrers Hits from said referrer Total bytes received and sent Total requests Total connections Total number of unique users Number of POSTs (ALL database writes must be done on posts) Number of active POSTs uptime Number of current connections Number of failures Emails sent */ Resource[string] resources; void registerFileResource(string name, string filename, bool allowHotLinking = true) { resources["/files/" ~ name] = new File(filename); } void registerCgiResource(string name, string filename) { /*FIXME*/ } void registerEmbeddedResource(string name, void function() func) { resources[name] = new EmbeddedProgram(func); } // FIXME: should break down name, pathinfo, and query string here // FIXME: doesn't handle hosts Resource findResource(string host, string uri, out int code, out string name, out string pathinfo, out string query) { // 200 if not null, 403, or 404 if null name = uri; pathinfo = ""; query = ""; int idx = indexOf(uri, "?"); if(idx != -1) { name = uri[0..idx]; // strip query string query = uri[idx+1..$]; } if(!startsWith(name, "/files/")) { int idx2 = indexOf(name[1..$], "/"); // strip pathinfo if(idx2 != -1) { idx2 += 1; pathinfo = name[idx2..$]; name = name[0..idx2]; } } Resource* r = name in resources; if(r is null) { code = 404; return null; } code = 200; return *r; /+ if(uri != "/"){ if(!std.file.exists(uri[1..$])) { code = 404; return null; } else { code = 200; return new File(uri[1..$]); } } else code = 200; return null; +/ } /* Implemented return codes: 200 OK 303 See Other (not yet) 403 Forbidden 404 Not Found You should throw an exception on failure. */ // returns the http code, so 200 is success, etc. int doHttpRequest(string method, string uri, string host, string userAgent, string cookie, string remote_addr, string contentType, const(byte)[] content, out string type, out byte[] response, out string otherHeaders) { int code; string name, pathinfo, query; Resource r = findResource(host, uri, code, name, pathinfo, query); if(code == 200) { if(r.hack()) { // FIXME: hackish void acceptResponse(string ctype, const(byte[]) data, string oh){ if(ctype == "") throw new Exception("Program error: no data outputted"); type = ctype; response = cast(byte[]) data; otherHeaders = oh; } prepareFromEmbeddedServer (method, uri, name, pathinfo, query, userAgent, cookie, remote_addr, contentType, content, &acceptResponse); r.call(); } else { r.call(); // FIXME: this won't actually work with cgi programs either. type = r.type(); response = cast(byte[]) r.response(); } } /+ acceptResponse("text/html", cast(const(byte[]))"Under construction

Welcome! I just got the server set up on December 24, 2008. Still have a lot to do before the website will be here. Check back in a week or two.

"); return 200; +/ // lolmain(); return code; }