mirror of https://github.com/adamdruppe/arsd.git
675 lines
15 KiB
D
675 lines
15 KiB
D
/++
|
|
OBSOLETE: Old version of my http implementation. Do not use this, instead use [arsd.http2].
|
|
|
|
I no longer work on this, use http2.d instead.
|
|
+/
|
|
/*deprecated*/ module arsd.http; // adrdox apparently loses the comment above with deprecated, i need to fix that over there.
|
|
|
|
import std.socket;
|
|
|
|
// FIXME: check Transfer-Encoding: gzip always
|
|
|
|
version(with_openssl) {
|
|
pragma(lib, "crypto");
|
|
pragma(lib, "ssl");
|
|
}
|
|
|
|
ubyte[] getBinary(string url, string[string] cookies = null) {
|
|
auto hr = httpRequest("GET", url, null, cookies);
|
|
if(hr.code != 200)
|
|
throw new Exception(format("HTTP answered %d instead of 200 on %s", hr.code, url));
|
|
return hr.content;
|
|
}
|
|
|
|
/**
|
|
Gets a textual document, ignoring headers. Throws on non-text or error.
|
|
*/
|
|
string get(string url, string[string] cookies = null) {
|
|
auto hr = httpRequest("GET", url, null, cookies);
|
|
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[string] cookies = null) {
|
|
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, cookies, ["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[string] cookies;
|
|
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;
|
|
|
|
bool useHttps;
|
|
|
|
this(string uri) {
|
|
original = uri;
|
|
|
|
if(uri[0 .. 8] == "https://")
|
|
useHttps = true;
|
|
else
|
|
if(uri[0..7] != "http://")
|
|
throw new Exception("You must use an absolute, http or https URL.");
|
|
|
|
version(with_openssl) {} else
|
|
if(useHttps)
|
|
throw new Exception("openssl support not compiled in try -version=with_openssl");
|
|
|
|
int start = useHttps ? 8 : 7;
|
|
|
|
auto posSlash = uri[start..$].indexOf("/");
|
|
if(posSlash != -1)
|
|
posSlash += start;
|
|
|
|
if(posSlash == -1)
|
|
posSlash = uri.length;
|
|
|
|
auto posColon = uri[start..$].indexOf(":");
|
|
if(posColon != -1)
|
|
posColon += start;
|
|
|
|
if(useHttps)
|
|
port = 443;
|
|
else
|
|
port = 80;
|
|
|
|
if(posColon != -1 && posColon < posSlash) {
|
|
host = uri[start..posColon];
|
|
port = to!ushort(uri[posColon+1..posSlash]);
|
|
} else
|
|
host = uri[start..posSlash];
|
|
|
|
path = uri[posSlash..$];
|
|
if(path == "")
|
|
path = "/";
|
|
}
|
|
}
|
|
|
|
HttpResponse httpRequest(string method, string uri, const(ubyte)[] content = null, string[string] cookies = null, string[] headers = null) {
|
|
import std.socket;
|
|
|
|
auto u = UriParts(uri);
|
|
// auto f = openNetwork(u.host, u.port);
|
|
auto f = new TcpSocket();
|
|
f.connect(new InternetAddress(u.host, u.port));
|
|
|
|
void delegate(string) write = (string d) {
|
|
f.send(d);
|
|
};
|
|
|
|
char[4096] readBuffer; // rawRead actually blocks until it can fill up the whole buffer... which is broken as far as http goes so one char at a time i guess. slow lol
|
|
char[] delegate() read = () {
|
|
size_t num = f.receive(readBuffer);
|
|
return readBuffer[0..num];
|
|
};
|
|
|
|
version(with_openssl) {
|
|
import deimos.openssl.ssl;
|
|
SSL* ssl;
|
|
SSL_CTX* ctx;
|
|
if(u.useHttps) {
|
|
void sslAssert(bool ret){
|
|
if (!ret){
|
|
throw new Exception("SSL_ERROR");
|
|
}
|
|
}
|
|
SSL_library_init();
|
|
OpenSSL_add_all_algorithms();
|
|
SSL_load_error_strings();
|
|
|
|
ctx = SSL_CTX_new(SSLv3_client_method());
|
|
sslAssert(!(ctx is null));
|
|
|
|
ssl = SSL_new(ctx);
|
|
SSL_set_fd(ssl, f.handle);
|
|
sslAssert(SSL_connect(ssl) != -1);
|
|
|
|
write = (string d) {
|
|
SSL_write(ssl, d.ptr, cast(uint)d.length);
|
|
};
|
|
|
|
read = () {
|
|
auto len = SSL_read(ssl, readBuffer.ptr, readBuffer.length);
|
|
return readBuffer[0 .. len];
|
|
};
|
|
}
|
|
}
|
|
|
|
|
|
HttpResponse response = doHttpRequestOnHelpers(write, read, method, uri, content, cookies, headers, u.useHttps);
|
|
|
|
version(with_openssl) {
|
|
if(u.useHttps) {
|
|
SSL_free(ssl);
|
|
SSL_CTX_free(ctx);
|
|
}
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
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 doHttpRequestOnHelpers(void delegate(string) write, char[] delegate() read, string method, string uri, const(ubyte)[] content = null, string[string] cookies = null, string[] headers = null, bool https = false)
|
|
in {
|
|
assert(method == "POST" || method == "GET");
|
|
}
|
|
do {
|
|
auto u = UriParts(uri);
|
|
|
|
|
|
|
|
|
|
|
|
write(format("%s %s HTTP/1.1\r\n", method, u.path));
|
|
write(format("Host: %s\r\n", u.host));
|
|
write(format("Connection: close\r\n"));
|
|
if(content !is null)
|
|
write(format("Content-Length: %d\r\n", content.length));
|
|
|
|
if(cookies !is null) {
|
|
string cookieHeader = "Cookie: ";
|
|
bool first = true;
|
|
foreach(k, v; cookies) {
|
|
if(first)
|
|
first = false;
|
|
else
|
|
cookieHeader ~= "; ";
|
|
cookieHeader ~= std.uri.encodeComponent(k) ~ "=" ~ std.uri.encodeComponent(v);
|
|
}
|
|
|
|
write(format("%s\r\n", cookieHeader));
|
|
}
|
|
|
|
if(headers !is null)
|
|
foreach(header; headers)
|
|
write(format("%s\r\n", header));
|
|
write("\r\n");
|
|
if(content !is null)
|
|
write(cast(string) content);
|
|
|
|
|
|
string buffer;
|
|
|
|
string readln() {
|
|
auto idx = buffer.indexOf("\r\n");
|
|
if(idx == -1) {
|
|
auto more = read();
|
|
if(more.length == 0) { // end of file or something
|
|
auto ret = buffer;
|
|
buffer = null;
|
|
return ret;
|
|
}
|
|
buffer ~= more;
|
|
return readln();
|
|
}
|
|
auto ret = buffer[0 .. idx + 2]; // + the \r\n
|
|
if(idx + 2 < buffer.length)
|
|
buffer = buffer[idx + 2 .. $];
|
|
else
|
|
buffer = null;
|
|
return ret;
|
|
}
|
|
|
|
HttpResponse hr;
|
|
cont:
|
|
string l = 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;
|
|
|
|
auto line = readln();
|
|
while(line.length) {
|
|
if(line.strip.length == 0)
|
|
break;
|
|
hr.headers ~= line;
|
|
if(line.startsWith("Content-Type: "))
|
|
hr.contentType = line[14..$-1];
|
|
if(line.startsWith("Set-Cookie: ")) {
|
|
auto hdr = line["Set-Cookie: ".length .. $-1];
|
|
auto semi = hdr.indexOf(";");
|
|
if(semi != -1)
|
|
hdr = hdr[0 .. semi];
|
|
|
|
auto equal = hdr.indexOf("=");
|
|
string name, value;
|
|
if(equal == -1) {
|
|
name = hdr;
|
|
// doesn't this mean erase the cookie?
|
|
} else {
|
|
name = hdr[0 .. equal];
|
|
value = hdr[equal + 1 .. $];
|
|
}
|
|
|
|
name = std.uri.decodeComponent(name);
|
|
value = std.uri.decodeComponent(value);
|
|
|
|
hr.cookies[name] = value;
|
|
}
|
|
if(line.startsWith("Transfer-Encoding: chunked"))
|
|
chunked = true;
|
|
line = readln();
|
|
}
|
|
|
|
// there might be leftover stuff in the line buffer
|
|
ubyte[] response = cast(ubyte[]) buffer.dup;
|
|
auto part = read();
|
|
while(part.length) {
|
|
response ~= part;
|
|
part = read();
|
|
}
|
|
|
|
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++) {
|
|
final switch(state) {
|
|
case 0: // reading hex
|
|
char c = response[a];
|
|
if((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
|
|
// 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' + 10;
|
|
|
|
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+= 1; // skipping a 13 10
|
|
start = a + 1;
|
|
state = 0;
|
|
break;
|
|
case 3: // reading footers
|
|
goto done; // FIXME
|
|
}
|
|
}
|
|
} else
|
|
hr.content = response;
|
|
done:
|
|
|
|
return hr;
|
|
}
|
|
|
|
|
|
/*
|
|
void main(string args[]) {
|
|
write(post("http://arsdnet.net/bugs.php", ["test" : "hey", "again" : "what"]));
|
|
}
|
|
*/
|
|
|
|
version(none):
|
|
|
|
struct Url {
|
|
string url;
|
|
}
|
|
|
|
struct BasicAuth {
|
|
string username;
|
|
string password;
|
|
}
|
|
|
|
/*
|
|
When you send something, it creates a request
|
|
and sends it asynchronously. The request object
|
|
|
|
auto request = new HttpRequest();
|
|
// set any properties here
|
|
|
|
// synchronous usage
|
|
auto reply = request.perform();
|
|
|
|
// async usage, type 1:
|
|
request.send();
|
|
request2.send();
|
|
|
|
// wait until the first one is done, with the second one still in-flight
|
|
auto response = request.waitForCompletion();
|
|
|
|
|
|
// async usage, type 2:
|
|
request.onDataReceived = (HttpRequest hr) {
|
|
if(hr.state == HttpRequest.State.complete) {
|
|
// use hr.responseData
|
|
}
|
|
};
|
|
request.send(); // send, using the callback
|
|
|
|
// before terminating, be sure you wait for your requests to finish!
|
|
|
|
request.waitForCompletion();
|
|
|
|
*/
|
|
|
|
class HttpRequest {
|
|
private static {
|
|
// we manage the actual connections. When a request is made on a particular
|
|
// host, we try to reuse connections. We may open more than one connection per
|
|
// host to do parallel requests.
|
|
//
|
|
// The key is the *domain name*. Multiple domains on the same address will have separate connections.
|
|
Socket[][string] socketsPerHost;
|
|
|
|
// only one request can be active on a given socket (at least HTTP < 2.0) so this is that
|
|
HttpRequest[Socket] activeRequestOnSocket;
|
|
HttpRequest[] pending; // and these are the requests that are waiting
|
|
|
|
SocketSet readSet;
|
|
|
|
|
|
void advanceConnections() {
|
|
if(readSet is null)
|
|
readSet = new SocketSet();
|
|
|
|
// are there pending requests? let's try to send them
|
|
|
|
readSet.reset();
|
|
|
|
// active requests need to be read or written to
|
|
foreach(sock, request; activeRequestOnSocket)
|
|
readSet.add(sock);
|
|
|
|
// check the other sockets just for EOF, if they close, take them out of our list,
|
|
// we'll reopen if needed upon request.
|
|
|
|
auto got = Socket.select(readSet, writeSet, null, 10.seconds /* timeout */);
|
|
if(got == 0) /* timeout */
|
|
{}
|
|
else
|
|
if(got == -1) /* interrupted */
|
|
{}
|
|
else /* ready */
|
|
{}
|
|
|
|
// call select(), do what needs to be done
|
|
// no requests are active, send the ones pending connection now
|
|
// we've completed a request, are there any more pending connection? if so, send them now
|
|
|
|
auto readSet = new SocketSet();
|
|
}
|
|
}
|
|
|
|
this() {
|
|
addConnection(this);
|
|
}
|
|
|
|
~this() {
|
|
removeConnection(this);
|
|
}
|
|
|
|
HttpResponse responseData;
|
|
HttpRequestParameters parameters;
|
|
private HttpClient parentClient;
|
|
|
|
size_t bodyBytesSent;
|
|
size_t bodyBytesReceived;
|
|
|
|
State state;
|
|
/// Called when data is received. Check the state to see what data is available.
|
|
void delegate(AsynchronousHttpRequest) onDataReceived;
|
|
|
|
enum State {
|
|
/// The request has not yet been sent
|
|
unsent,
|
|
|
|
/// The send() method has been called, but no data is
|
|
/// sent on the socket yet because the connection is busy.
|
|
pendingAvailableConnection,
|
|
|
|
/// The headers are being sent now
|
|
sendingHeaders,
|
|
|
|
/// The body is being sent now
|
|
sendingBody,
|
|
|
|
/// The request has been sent but we haven't received any response yet
|
|
waitingForResponse,
|
|
|
|
/// We have received some data and are currently receiving headers
|
|
readingHeaders,
|
|
|
|
/// All headers are available but we're still waiting on the body
|
|
readingBody,
|
|
|
|
/// The request is complete.
|
|
complete,
|
|
|
|
/// The request is aborted, either by the abort() method, or as a result of the server disconnecting
|
|
aborted
|
|
}
|
|
|
|
/// Sends now and waits for the request to finish, returning the response.
|
|
HttpResponse perform() {
|
|
send();
|
|
return waitForCompletion();
|
|
}
|
|
|
|
/// Sends the request asynchronously.
|
|
void send() {
|
|
if(state != State.unsent && state != State.aborted)
|
|
return; // already sent
|
|
|
|
responseData = HttpResponse.init;
|
|
bodyBytesSent = 0;
|
|
bodyBytesReceived = 0;
|
|
state = State.pendingAvailableConnection;
|
|
|
|
HttpResponse.advanceConnections();
|
|
}
|
|
|
|
|
|
/// Waits for the request to finish or timeout, whichever comes furst.
|
|
HttpResponse waitForCompletion() {
|
|
while(state != State.aborted && state != State.complete)
|
|
HttpResponse.advanceConnections();
|
|
return responseData;
|
|
}
|
|
|
|
/// Aborts this request.
|
|
/// Due to the nature of the HTTP protocol, aborting one request will result in all subsequent requests made on this same connection to be aborted as well.
|
|
void abort() {
|
|
parentClient.close();
|
|
}
|
|
}
|
|
|
|
struct HttpRequestParameters {
|
|
Duration timeout;
|
|
|
|
// debugging
|
|
bool useHttp11 = true;
|
|
bool acceptGzip = true;
|
|
|
|
// the request itself
|
|
HttpVerb method;
|
|
string host;
|
|
string uri;
|
|
|
|
string userAgent;
|
|
|
|
string[string] cookies;
|
|
|
|
string[] headers; /// do not duplicate host, content-length, content-type, or any others that have a specific property
|
|
|
|
string contentType;
|
|
ubyte[] bodyData;
|
|
}
|
|
|
|
interface IHttpClient {
|
|
|
|
}
|
|
|
|
enum HttpVerb { GET, HEAD, POST, PUT, DELETE, OPTIONS, TRACE, CONNECT }
|
|
|
|
/*
|
|
Usage:
|
|
|
|
auto client = new HttpClient("localhost", 80);
|
|
// relative links work based on the current url
|
|
client.get("foo/bar");
|
|
client.get("baz"); // gets foo/baz
|
|
|
|
auto request = client.get("rofl");
|
|
auto response = request.waitForCompletion();
|
|
*/
|
|
|
|
/// HttpClient keeps cookies, location, and some other state to reuse connections, when possible, like a web browser.
|
|
class HttpClient {
|
|
/* Protocol restrictions, useful to disable when debugging servers */
|
|
bool useHttp11 = true;
|
|
bool useGzip = true;
|
|
|
|
/// Automatically follow a redirection?
|
|
bool followLocation = false;
|
|
|
|
@property Url location() {
|
|
return currentUrl;
|
|
}
|
|
|
|
/// High level function that works similarly to entering a url
|
|
/// into a browser.
|
|
///
|
|
/// Follows locations, updates the current url.
|
|
AsynchronousHttpRequest navigateTo(Url where) {
|
|
currentUrl = where.basedOn(currentUrl);
|
|
assert(0);
|
|
}
|
|
|
|
private Url currentUrl;
|
|
|
|
this() {
|
|
|
|
}
|
|
|
|
this(Url url) {
|
|
open(url);
|
|
}
|
|
|
|
this(string host, ushort port = 80, bool useSsl = false) {
|
|
open(host, port);
|
|
}
|
|
|
|
// FIXME: add proxy
|
|
// FIXME: some kind of caching
|
|
|
|
void open(Url url) {
|
|
|
|
}
|
|
|
|
void open(string host, ushort port = 80, bool useSsl = false) {
|
|
|
|
}
|
|
|
|
void close() {
|
|
socket.close();
|
|
}
|
|
|
|
void setCookie(string name, string value) {
|
|
|
|
}
|
|
|
|
void clearCookies() {
|
|
|
|
}
|
|
|
|
HttpResponse sendSynchronously() {
|
|
auto request = sendAsynchronously();
|
|
return request.waitForCompletion();
|
|
}
|
|
|
|
AsynchronousHttpRequest sendAsynchronously() {
|
|
|
|
}
|
|
|
|
string method;
|
|
string host;
|
|
ushort port;
|
|
string uri;
|
|
|
|
string[] headers;
|
|
ubyte[] requestBody;
|
|
|
|
string userAgent;
|
|
|
|
/* inter-request state */
|
|
string[string] cookies;
|
|
}
|
|
|
|
// FIXME: websocket
|