module arsd.http; import std.stdio; /** Gets a textual document, ignoring headers. Throws on non-text or error. */ string get(string url) { auto hr = httpRequest("GET", url); if(hr.code != 200) throw new Exception(format("HTTP answered %d instead of 200 on %s", hr.code, url)); if(hr.contentType.indexOf("text/") == -1) throw new Exception(hr.contentType ~ " is bad content for conversion to string"); return cast(string) hr.content; } static import std.uri; string post(string url, string[string] args) { string content; foreach(name, arg; args) { if(content.length) content ~= "&"; content ~= std.uri.encode(name) ~ "=" ~ std.uri.encode(arg); } auto hr = httpRequest("POST", url, cast(ubyte[]) content, ["Content-Type: application/x-www-form-urlencoded"]); if(hr.code != 200) throw new Exception(format("HTTP answered %d instead of 200", hr.code)); if(hr.contentType.indexOf("text/") == -1) throw new Exception(hr.contentType ~ " is bad content for conversion to string"); return cast(string) hr.content; } struct HttpResponse { int code; string contentType; string[] headers; ubyte[] content; } import std.string; static import std.algorithm; import std.conv; struct UriParts { string original; string method; string host; ushort port; string path; this(string uri) { original = uri; if(uri[0..7] != "http://") throw new Exception("You must use an absolute, unencrypted URL."); int posSlash = uri[7..$].indexOf("/"); if(posSlash != -1) posSlash += 7; if(posSlash == -1) posSlash = uri.length; int posColon = uri[7..$].indexOf(":"); if(posColon != -1) posColon += 7; port = 80; if(posColon != -1 && posColon < posSlash) { host = uri[7..posColon]; port = to!ushort(uri[posColon+1..posSlash]); } else host = uri[7..posSlash]; path = uri[posSlash..$]; if(path == "") path = "/"; } } HttpResponse httpRequest(string method, string uri, const(ubyte)[] content = null, string headers[] = null) { auto u = UriParts(uri); auto f = openNetwork(u.host, u.port); return doHttpRequestOnFile(f, method, uri, content, headers); } /** Executes a generic http request, returning the full result. The correct formatting of the parameters are the caller's responsibility. Content-Length is added automatically, but YOU must give Content-Type! */ HttpResponse doHttpRequestOnFile(File f, string method, string uri, const(ubyte)[] content = null, string headers[] = null) in { assert(method == "POST" || method == "GET"); } body { auto u = UriParts(uri); f.writefln("%s %s HTTP/1.1", method, u.path); f.writefln("Host: %s", u.host); f.writefln("Connection: close"); if(content !is null) f.writefln("Content-Length: %d", content.length); if(headers !is null) foreach(header; headers) f.writefln("%s", header); f.writefln(""); if(content !is null) f.rawWrite(content); HttpResponse hr; cont: string l = f.readln(); if(l[0..9] != "HTTP/1.1 ") throw new Exception("Not talking to a http server"); hr.code = to!int(l[9..12]); // HTTP/1.1 ### OK if(hr.code == 100) { // continue do { l = readln(); } while(l.length > 1); goto cont; } bool chunked = false; foreach(line; f.byLine) { if(line.length <= 1) break; hr.headers ~= line.idup; if(line.startsWith("Content-Type: ")) hr.contentType = line[14..$-1].idup; if(line.startsWith("Transfer-Encoding: chunked")) chunked = true; } ubyte[] response; foreach(ubyte[] chunk; f.byChunk(4096)) { response ~= chunk; } if(chunked) { // read the hex length, stopping at a \r\n, ignoring everything between the new line but after the first non-valid hex character // read binary data of that length. it is our content // repeat until a zero sized chunk // then read footers as headers. int state = 0; int size; int start = 0; for(int a = 0; a < response.length; a++) { switch(state) { case 0: // reading hex char c = response[a]; if((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z')) { // just keep reading } else { int power = 1; size = 0; for(int b = a-1; b >= start; b--) { char cc = response[b]; if(cc >= 'a' && cc <= 'z') cc -= 0x20; int val = 0; if(cc >= '0' && cc <= '9') val = cc - '0'; else val = cc - 'A'; size += power * val; power *= 16; } state++; continue; } break; case 1: // reading until end of line char c = response[a]; if(c == '\n') { if(size == 0) state = 3; else state = 2; } break; case 2: // reading data hr.content ~= response[a..a+size]; a += size; a+= 2; // skipping a 13 10 start = a; state = 0; break; case 3: // reading footers goto done; // FIXME break; } } } else hr.content = response; done: return hr; } /* void main(string args[]) { write(post("http://arsdnet.net/bugs.php", ["test" : "hey", "again" : "what"])); } */