better url stuff (borrowed from cgi.d)

This commit is contained in:
Adam D. Ruppe 2014-12-22 13:52:35 -05:00
parent ff68e1cf00
commit 6c3507ca3b
1 changed files with 171 additions and 60 deletions

231
http2.d
View File

@ -1,6 +1,8 @@
// Copyright 2013, Adam D. Ruppe. // Copyright 2013, Adam D. Ruppe.
module arsd.http2; module arsd.http2;
debug import std.stdio;
import std.socket; import std.socket;
import core.time; import core.time;
@ -95,56 +97,128 @@ struct HttpResponse {
import std.string; import std.string;
static import std.algorithm; static import std.algorithm;
import std.conv; import std.conv;
import std.range;
struct UriParts {
string original;
string method;
string host;
ushort port;
string path;
bool useHttps;
// Copy pasta from cgi.d, then stripped down
struct Uri {
alias toString this; // blargh idk a url really is a string, but should it be implicit?
// scheme//userinfo@host:port/path?query#fragment
string scheme; /// e.g. "http" in "http://example.com/"
string userinfo; /// the username (and possibly a password) in the uri
string host; /// the domain name
int port; /// port number, if given. Will be zero if a port was not explicitly given
string path; /// e.g. "/folder/file.html" in "http://example.com/folder/file.html"
string query; /// the stuff after the ? in a uri
string fragment; /// the stuff after the # in a uri.
/// Breaks down a uri string to its components
this(string uri) { this(string uri) {
original = uri; reparse(uri);
}
if(uri[0 .. 8] == "https://") private void reparse(string uri) {
useHttps = true; import std.regex;
else // from RFC 3986
if(uri[0..7] != "http://")
throw new Exception("You must use an absolute, http or https URL.");
version(with_openssl) {} else // the ctRegex triples the compile time and makes ugly errors for no real benefit
if(useHttps) // it was a nice experiment but just not worth it.
throw new Exception("openssl support not compiled in try -version=with_openssl"); // enum ctr = ctRegex!r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?";
auto ctr = regex(r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?");
int start = useHttps ? 8 : 7; auto m = match(uri, ctr);
if(m) {
scheme = m.captures[2];
auto authority = m.captures[4];
auto posSlash = uri[start..$].indexOf("/"); auto idx = authority.indexOf("@");
if(posSlash != -1) if(idx != -1) {
posSlash += start; userinfo = authority[0 .. idx];
authority = authority[idx + 1 .. $];
}
if(posSlash == -1) idx = authority.indexOf(":");
posSlash = uri.length; if(idx == -1) {
port = 0; // 0 means not specified; we should use the default for the scheme
host = authority;
} else {
host = authority[0 .. idx];
port = to!int(authority[idx + 1 .. $]);
}
auto posColon = uri[start..$].indexOf(":"); path = m.captures[5];
if(posColon != -1) query = m.captures[7];
posColon += start; fragment = m.captures[9];
}
// uriInvalidated = false;
}
if(useHttps) private string rebuildUri() const {
port = 443; string ret;
else if(scheme.length)
port = 80; ret ~= scheme ~ ":";
if(userinfo.length || host.length)
ret ~= "//";
if(userinfo.length)
ret ~= userinfo ~ "@";
if(host.length)
ret ~= host;
if(port)
ret ~= ":" ~ to!string(port);
if(posColon != -1 && posColon < posSlash) { ret ~= path;
host = uri[start..posColon];
port = to!ushort(uri[posColon+1..posSlash]);
} else
host = uri[start..posSlash];
path = uri[posSlash..$]; if(query.length)
if(path == "") ret ~= "?" ~ query;
path = "/";
if(fragment.length)
ret ~= "#" ~ fragment;
// uri = ret;
// uriInvalidated = false;
return ret;
}
/// Converts the broken down parts back into a complete string
string toString() const {
// if(uriInvalidated)
return rebuildUri();
}
/// Returns a new absolute Uri given a base. It treats this one as
/// relative where possible, but absolute if not. (If protocol, domain, or
/// other info is not set, the new one inherits it from the base.)
///
/// Browsers use a function like this to figure out links in html.
Uri basedOn(in Uri baseUrl) const {
Uri n = this; // copies
// n.uriInvalidated = true; // make sure we regenerate...
// userinfo is not inherited... is this wrong?
// if anything is given in the existing url, we don't use the base anymore.
if(n.scheme.empty) {
n.scheme = baseUrl.scheme;
if(n.host.empty) {
n.host = baseUrl.host;
if(n.port == 0) {
n.port = baseUrl.port;
if(n.path.length > 0 && n.path[0] != '/') {
auto b = baseUrl.path[0 .. baseUrl.path.lastIndexOf("/") + 1];
if(b.length == 0)
b = "/";
n.path = b ~ n.path;
} else if(n.path.length == 0) {
n.path = baseUrl.path;
}
}
}
}
return n;
} }
} }
@ -154,12 +228,6 @@ void main(string args[]) {
} }
*/ */
struct Url {
string url;
Url basedOn(Url url) { return url; }
}
struct BasicAuth { struct BasicAuth {
string username; string username;
string password; string password;
@ -203,23 +271,58 @@ class HttpRequest {
// host, we try to reuse connections. We may open more than one connection per // host, we try to reuse connections. We may open more than one connection per
// host to do parallel requests. // host to do parallel requests.
// //
// The key is the *domain name*. Multiple domains on the same address will have separate connections. // The key is the *domain name* and the port. Multiple domains on the same address will have separate connections.
Socket[][string] socketsPerHost; Socket[][string] socketsPerHost;
Socket getOpenSocketOnHost(string host, ushort port) { void loseSocket(string host, ushort port, Socket s) {
// FIXME: port import std.string;
auto key = format("%s:%s", host, port);
if(auto list = key in socketsPerHost) {
for(int a = 0; a < (*list).length; a++) {
if((*list)[a] is s) {
for(int b = a; b < (*list).length - 1; b++)
(*list)[b] = (*list)[b+1];
(*list).length = (*list).length - 1;
break;
}
}
}
}
Socket getOpenSocketOnHost(string host, ushort port) {
Socket openNewConnection() { Socket openNewConnection() {
auto socket = new Socket(AddressFamily.INET, SocketType.STREAM); auto socket = new Socket(AddressFamily.INET, SocketType.STREAM);
socket.connect(new InternetAddress(host, port)); socket.connect(new InternetAddress(host, port));
debug writeln("opening to ", host, ":", port);
return socket; return socket;
} }
if(auto hostListing = host in socketsPerHost) { import std.string;
auto key = format("%s:%s", host, port);
if(auto hostListing = key in socketsPerHost) {
// try to find an available socket that is already open // try to find an available socket that is already open
foreach(socket; *hostListing) { foreach(socket; *hostListing) {
if(socket !in activeRequestOnSocket) if(socket !in activeRequestOnSocket) {
// let's see if it has closed since we last tried
// e.g. a server timeout or something. If so, we need
// to lose this one and immediately open a new one.
SocketSet readSet = new SocketSet();
readSet.reset();
readSet.add(socket);
auto got = Socket.select(readSet, null, null, 5.msecs /* timeout */);
if(got > 0) {
// we can read something off this... but there aren't
// any active requests. Assume it is EOF and open a new one
socket.close();
loseSocket(host, port, socket);
goto openNew;
}
return socket; return socket;
}
} }
// if not too many already open, go ahead and do a new one // if not too many already open, go ahead and do a new one
@ -231,8 +334,10 @@ class HttpRequest {
return null; // too many, you'll have to wait return null; // too many, you'll have to wait
} }
openNew:
auto socket = openNewConnection(); auto socket = openNewConnection();
socketsPerHost[host] ~= socket; socketsPerHost[key] ~= socket;
return socket; return socket;
} }
@ -265,7 +370,7 @@ class HttpRequest {
continue; continue;
} }
auto socket = getOpenSocketOnHost(pc.requestParameters.host, 80); auto socket = getOpenSocketOnHost(pc.requestParameters.host, pc.requestParameters.port);
if(socket !is null) { if(socket !is null) {
activeRequestOnSocket[socket] = pc; activeRequestOnSocket[socket] = pc;
@ -308,9 +413,10 @@ class HttpRequest {
throw new Exception("receive error"); throw new Exception("receive error");
} else if(got == 0) { } else if(got == 0) {
// remote side disconnected // remote side disconnected
debug writeln("remote disconnect");
request.state = State.aborted; request.state = State.aborted;
inactive[inactiveCount++] = sock; inactive[inactiveCount++] = sock;
// FIXME remove the socket from the list loseSocket(request.requestParameters.host, request.requestParameters.port, sock);
} else { } else {
// data available // data available
request.handleIncomingData(buffer[0 .. got]); request.handleIncomingData(buffer[0 .. got]);
@ -336,8 +442,10 @@ class HttpRequest {
} }
} }
foreach(s; inactive[0 .. inactiveCount]) foreach(s; inactive[0 .. inactiveCount]) {
debug writeln("removing socket from active list");
activeRequestOnSocket.remove(s); activeRequestOnSocket.remove(s);
}
} }
// we've completed a request, are there any more pending connection? if so, send them now // we've completed a request, are there any more pending connection? if so, send them now
@ -549,10 +657,13 @@ class HttpRequest {
this() { this() {
} }
this(Url where) { this(Uri where) {
auto parts = UriParts(where.url); auto parts = where;
requestParameters.method = HttpVerb.GET; requestParameters.method = HttpVerb.GET;
requestParameters.host = parts.host; requestParameters.host = parts.host;
requestParameters.port = cast(ushort) parts.port;
if(parts.port == 0)
requestParameters.port = 80; // FIXME: SSL
requestParameters.uri = parts.path; requestParameters.uri = parts.path;
} }
@ -655,6 +766,7 @@ struct HttpRequestParameters {
// the request itself // the request itself
HttpVerb method; HttpVerb method;
string host; string host;
ushort port;
string uri; string uri;
string userAgent; string userAgent;
@ -694,7 +806,7 @@ class HttpClient {
/// Automatically follow a redirection? /// Automatically follow a redirection?
bool followLocation = false; bool followLocation = false;
@property Url location() { @property Uri location() {
return currentUrl; return currentUrl;
} }
@ -702,16 +814,14 @@ class HttpClient {
/// into a browser. /// into a browser.
/// ///
/// Follows locations, updates the current url. /// Follows locations, updates the current url.
HttpRequest navigateTo(Url where) { HttpRequest navigateTo(Uri where) {
currentUrl = where.basedOn(currentUrl); currentUrl = where.basedOn(currentUrl);
currentUrl = where; currentDomain = where.host;
auto parts = UriParts(where.url);
currentDomain = parts.host;
auto request = new HttpRequest(currentUrl); auto request = new HttpRequest(currentUrl);
return request; return request;
} }
private Url currentUrl; private Uri currentUrl;
private string currentDomain; private string currentDomain;
this(ICache cache = null) { this(ICache cache = null) {
@ -771,6 +881,7 @@ struct HttpCookie {
// FIXME: websocket // FIXME: websocket
version(testing)
void main() { void main() {
import std.stdio; import std.stdio;
auto client = new HttpClient(); auto client = new HttpClient();