mirror of https://github.com/adamdruppe/arsd.git
1502 lines
51 KiB
D
1502 lines
51 KiB
D
module arsd.cgi;
|
|
|
|
// FIXME: would be cool to flush part of a dom document before complete
|
|
// somehow in here and dom.d.
|
|
|
|
|
|
// FIXME: 100 Continue in the nph section? Probably belongs on the
|
|
// httpd class though.
|
|
|
|
public import std.string;
|
|
import std.uri;
|
|
import std.exception;
|
|
import std.base64;
|
|
//import std.algorithm;
|
|
public import std.stdio;
|
|
import std.datetime;
|
|
public import std.conv;
|
|
import std.range;
|
|
|
|
import std.process;
|
|
|
|
import std.zlib;
|
|
|
|
|
|
/+
|
|
/// If you pass -1 to Cgi.this() as maxContentLength, it
|
|
/// lets you use one of these instead of buffering the data
|
|
/// itself.
|
|
|
|
/// The benefit is you can handle data of any size without needing
|
|
/// a buffering solution. The downside is this is one-way and the order
|
|
/// of elements might not be what you want. If you need buffering, you've
|
|
/// gotta do it yourself.
|
|
struct CgiVariableStream {
|
|
bool empty() {
|
|
return true;
|
|
}
|
|
|
|
void popFront() {
|
|
|
|
}
|
|
|
|
/// If you want to do an upload progress bar, these functions
|
|
/// might help.
|
|
int bytesReceived() {
|
|
|
|
}
|
|
|
|
/// ditto
|
|
/// But, note this won't necessarily be known, so it may return zero!
|
|
int bytesExpected() {
|
|
|
|
}
|
|
|
|
|
|
/// The stream returns these Elements.
|
|
struct Element {
|
|
enum Type { String, File }
|
|
|
|
/// Since the last popFront, is this a new element or a
|
|
/// continuation of the last?
|
|
bool isNew;
|
|
|
|
/// Is this the last piece of this element?
|
|
/// Note that sometimes isComplete will only be true with an empty
|
|
/// payload, since it can't be sure until it actually receives the terminator.
|
|
/// This, unless you are buffering parts, you can't depend on it.
|
|
bool isComplete;
|
|
|
|
/// Metainfo from the part header is preserved
|
|
string name;
|
|
string fileName;
|
|
string contentType;
|
|
|
|
ubyte[] content;
|
|
}
|
|
}
|
|
+/
|
|
|
|
|
|
T[] consume(T)(T[] range, int count) {
|
|
if(count > range.length)
|
|
count = range.length;
|
|
return range[count..$];
|
|
}
|
|
|
|
int locationOf(T)(T[] data, string item) {
|
|
const(ubyte[]) d = cast(const(ubyte[])) data;
|
|
const(ubyte[]) i = cast(const(ubyte[])) item;
|
|
|
|
for(int a = 0; a < d.length; a++) {
|
|
if(a + i.length > d.length)
|
|
return -1;
|
|
if(d[a..a+i.length] == i)
|
|
return a;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
/// If you are doing a custom cgi class, mixing this in can take care of
|
|
/// the required constructors for you
|
|
mixin template ForwardCgiConstructors() {
|
|
this(int maxContentLength = 5_000_000,
|
|
string[string] env = null,
|
|
const(ubyte)[] delegate() readdata = null,
|
|
void delegate(const(ubyte)[]) _rawDataOutput = null
|
|
) { super(maxContentLength, env, readdata, _rawDataOutput); }
|
|
|
|
this(string[] headers, immutable(ubyte)[] data, string address, void delegate(const(ubyte)[]) _rawDataOutput = null, int pathInfoStarts = 0) {
|
|
super(headers, data, address, _rawDataOutput, pathInfoStarts);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
version(Windows) {
|
|
// FIXME: ugly hack to solve stdin exception problems on Windows:
|
|
// reading stdin results in StdioException (Bad file descriptor)
|
|
// this is probably due to http://d.puremagic.com/issues/show_bug.cgi?id=3425
|
|
private struct stdin {
|
|
struct ByChunk { // Replicates std.stdio.ByChunk
|
|
private:
|
|
ubyte[] chunk_;
|
|
public:
|
|
this(size_t size)
|
|
in {
|
|
assert(size, "size must be larger than 0");
|
|
}
|
|
body {
|
|
chunk_ = new ubyte[](size);
|
|
popFront();
|
|
}
|
|
|
|
@property bool empty() const {
|
|
return !std.stdio.stdin.isOpen || std.stdio.stdin.eof; // Ugly, but seems to do the job
|
|
}
|
|
@property nothrow ubyte[] front() { return chunk_; }
|
|
void popFront() {
|
|
enforce(!empty, "Cannot call popFront on empty range");
|
|
chunk_ = stdin.rawRead(chunk_);
|
|
}
|
|
}
|
|
|
|
import std.c.windows.windows;
|
|
static:
|
|
|
|
static this() {
|
|
// Set stdin to binary mode
|
|
setmode(std.stdio.stdin.fileno(), 0x8000);
|
|
}
|
|
|
|
T[] rawRead(T)(T[] buf) {
|
|
uint bytesRead;
|
|
auto result = ReadFile(GetStdHandle(STD_INPUT_HANDLE), buf.ptr, buf.length * T.sizeof, &bytesRead, null);
|
|
|
|
if (!result) {
|
|
auto err = GetLastError();
|
|
if (err == 38/*ERROR_HANDLE_EOF*/ || err == 109/*ERROR_BROKEN_PIPE*/) // 'good' errors meaning end of input
|
|
return buf[0..0];
|
|
// Some other error, throw it
|
|
|
|
char* buffer;
|
|
scope(exit) LocalFree(buffer);
|
|
|
|
// FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100
|
|
// FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000
|
|
FormatMessageA(0x1100, null, err, 0, cast(char*)&buffer, 256, null);
|
|
throw new Exception(to!string(buffer));
|
|
}
|
|
enforce(!(bytesRead % T.sizeof), "I/O error");
|
|
return buf[0..bytesRead / T.sizeof];
|
|
}
|
|
|
|
auto byChunk(size_t sz) { return ByChunk(sz); }
|
|
}
|
|
}
|
|
|
|
string makeDataUrl(string mimeType, in ubyte[] data) {
|
|
auto data64 = Base64.encode(data);
|
|
return "data:" ~ mimeType ~ ";base64," ~ assumeUnique(data64);
|
|
}
|
|
|
|
|
|
/// The main interface with the web request
|
|
class Cgi {
|
|
public:
|
|
enum RequestMethod { GET, HEAD, POST, PUT, DELETE, // GET and POST are the ones that really work
|
|
// these are defined in the standard, but idk if they are useful for anything
|
|
OPTIONS, TRACE, CONNECT,
|
|
// this is an extension for when the method is not specified and you want to assume
|
|
CommandLine }
|
|
|
|
/** Initializes it using the CGI interface */
|
|
this(int maxContentLength = 5_000_000,
|
|
// use this to override the environment variable listing
|
|
in string[string] env = null,
|
|
// and this should return a chunk of data. return empty when done
|
|
const(ubyte)[] delegate() readdata = null,
|
|
// finally, use this to do custom output if needed
|
|
void delegate(const(ubyte)[]) _rawDataOutput = null
|
|
)
|
|
{
|
|
rawDataOutput = _rawDataOutput;
|
|
auto getenv = delegate string(string var) {
|
|
if(env is null)
|
|
return .getenv(var);
|
|
auto e = var in env;
|
|
if(e is null)
|
|
return null;
|
|
return *e;
|
|
};
|
|
|
|
// fetching all the request headers
|
|
string[string] requestHeadersHere;
|
|
foreach(k, v; env is null ? cast(const) environment.toAA() : env) {
|
|
if(k.startsWith("HTTP_")) {
|
|
requestHeadersHere[replace(k["HTTP_".length .. $].toLower(), "_", "-")] = v;
|
|
}
|
|
}
|
|
|
|
this.requestHeaders = assumeUnique(requestHeadersHere);
|
|
|
|
requestUri = getenv("REQUEST_URI");
|
|
cookie = getenv("HTTP_COOKIE");
|
|
referrer = getenv("HTTP_REFERER");
|
|
userAgent = getenv("HTTP_USER_AGENT");
|
|
queryString = getenv("QUERY_STRING");
|
|
remoteAddress = getenv("REMOTE_ADDR");
|
|
host = getenv("HTTP_HOST");
|
|
pathInfo = getenv("PATH_INFO");
|
|
scriptName = getenv("SCRIPT_NAME");
|
|
|
|
bool iis = false;
|
|
|
|
// Because IIS doesn't pass requestUri, we simulate it here if it's empty.
|
|
if(requestUri.length == 0) {
|
|
// IIS sometimes includes the script name as part of the path info - we don't want that
|
|
if(pathInfo.length >= scriptName.length && (pathInfo[0 .. scriptName.length] == scriptName))
|
|
pathInfo = pathInfo[scriptName.length .. $];
|
|
|
|
requestUri = scriptName ~ pathInfo ~ (queryString.length ? ("?" ~ queryString) : "");
|
|
|
|
iis = true; // FIXME HACK - used in byChunk below - see bugzilla 6339
|
|
|
|
// FIXME: this works for apache and iis... but what about others?
|
|
}
|
|
|
|
// NOTE: on shitpache, you need to specifically forward this
|
|
authorization = getenv("HTTP_AUTHORIZATION");
|
|
// this is a hack because Apache is a shitload of fuck and
|
|
// refuses to send the real header to us. Compatible
|
|
// programs should send both the standard and X- versions
|
|
|
|
// NOTE: if you have access to .htaccess or httpd.conf, you can make this
|
|
// unnecessary with mod_rewrite, so it is commented
|
|
|
|
//if(authorization.length == 0) // if the std is there, use it
|
|
// authorization = getenv("HTTP_X_AUTHORIZATION");
|
|
|
|
if(getenv("SERVER_PORT").length)
|
|
port = to!int(getenv("SERVER_PORT"));
|
|
else
|
|
port = 0; // this was probably called from the command line
|
|
|
|
auto ae = getenv("HTTP_ACCEPT_ENCODING");
|
|
if(ae.length && ae.indexOf("gzip") != -1)
|
|
acceptsGzip = true;
|
|
|
|
accept = getenv("HTTP_ACCEPT");
|
|
lastEventId = getenv("HTTP_LAST_EVENT_ID");
|
|
|
|
auto ka = getenv("HTTP_CONNECTION");
|
|
if(ka.length && ka.toLower().indexOf("keep-alive") != -1)
|
|
keepAliveRequested = true;
|
|
|
|
auto rm = getenv("REQUEST_METHOD");
|
|
if(rm.length)
|
|
requestMethod = to!RequestMethod(getenv("REQUEST_METHOD"));
|
|
else
|
|
requestMethod = RequestMethod.CommandLine;
|
|
|
|
https = getenv("HTTPS") == "on";
|
|
|
|
// FIXME: DOCUMENT_ROOT?
|
|
|
|
immutable(ubyte)[] data;
|
|
string contentType;
|
|
|
|
// FIXME: what about PUT?
|
|
if(requestMethod == RequestMethod.POST) {
|
|
contentType = getenv("CONTENT_TYPE");
|
|
|
|
// FIXME: is this ever not going to be set? I guess it depends
|
|
// on if the server de-chunks and buffers... seems like it has potential
|
|
// to be slow if they did that. The spec says it is always there though.
|
|
// And it has worked reliably for me all year in the live environment,
|
|
// but some servers might be different.
|
|
int contentLength = to!int(getenv("CONTENT_LENGTH"));
|
|
immutable originalContentLength = contentLength;
|
|
if(contentLength) {
|
|
if(maxContentLength > 0 && contentLength > maxContentLength) {
|
|
setResponseStatus("413 Request entity too large");
|
|
write("You tried to upload a file that is too large.");
|
|
close();
|
|
throw new Exception("POST too large");
|
|
}
|
|
|
|
if(readdata is null)
|
|
foreach(ubyte[] chunk; stdin.byChunk(iis ? contentLength : 4096)) { // FIXME: maybe it should pass the range directly to the parser
|
|
if(chunk.length > contentLength) {
|
|
data ~= chunk[0..contentLength];
|
|
contentLength = 0;
|
|
break;
|
|
} else {
|
|
data ~= chunk;
|
|
contentLength -= chunk.length;
|
|
}
|
|
if(contentLength == 0)
|
|
break;
|
|
|
|
onRequestBodyDataReceived(data.length, originalContentLength);
|
|
}
|
|
else {
|
|
// we have a custom data source..
|
|
auto chunk = readdata();
|
|
while(chunk.length) {
|
|
// FIXME: DRY
|
|
if(chunk.length > contentLength) {
|
|
data ~= chunk[0..contentLength];
|
|
contentLength = 0;
|
|
break;
|
|
} else {
|
|
data ~= chunk;
|
|
contentLength -= chunk.length;
|
|
}
|
|
if(contentLength == 0)
|
|
break;
|
|
|
|
chunk = readdata();
|
|
onRequestBodyDataReceived(data.length, originalContentLength);
|
|
}
|
|
}
|
|
onRequestBodyDataReceived(data.length, originalContentLength);
|
|
}
|
|
|
|
version(preserveData)
|
|
originalPostData = data;
|
|
}
|
|
|
|
mixin(createVariableHashes());
|
|
// fixme: remote_user script name
|
|
}
|
|
|
|
/// you can override this function to somehow react
|
|
/// to an upload in progress.
|
|
///
|
|
/// Take note that most of the CGI object is not yet
|
|
/// initialized! Stuff from HTTP headers, in raw form, is usable.
|
|
/// Stuff processed from them (such as get[]!) is not.
|
|
///
|
|
/// In the current implementation, you can't even look at partial
|
|
/// post. My idea here was so you can output a progress bar or
|
|
/// something to a cooperative client.
|
|
///
|
|
/// The default is to do nothing. Subclass cgi and use the
|
|
/// CustomCgiMain mixin to do something here.
|
|
void onRequestBodyDataReceived(size_t receivedSoFar, size_t totalExpected) {
|
|
// This space intentionally left blank.
|
|
}
|
|
|
|
/** Initializes it from some almost* raw HTTP request data
|
|
headers[0] should be the "GET / HTTP/1.1" line
|
|
|
|
* Note the data should /not/ be chunked at this point.
|
|
|
|
headers: each http header, excluding the \r\n at the end, but including the request line at headers[0]
|
|
data: the request data (usually POST variables)
|
|
address: the remote IP
|
|
_rawDataOutput: delegate to accept response data. If not null, this is called for all data output, which
|
|
will include HTTP headers and the status line. The data may also be chunked; it is already suitable for
|
|
being sent directly down the wire.
|
|
|
|
If null, the data is sent to stdout.
|
|
|
|
|
|
|
|
FIXME: data should be able to be streaming, for large files
|
|
*/
|
|
this(string[] headers, immutable(ubyte)[] data, string address, void delegate(const(ubyte)[]) _rawDataOutput = null, int pathInfoStarts = 0) {
|
|
auto parts = headers[0].split(" ");
|
|
|
|
https = false;
|
|
port = 80; // FIXME
|
|
|
|
rawDataOutput = _rawDataOutput;
|
|
nph = true;
|
|
|
|
requestMethod = to!RequestMethod(parts[0]);
|
|
|
|
requestUri = parts[1];
|
|
|
|
scriptName = requestUri[0 .. pathInfoStarts];
|
|
|
|
auto question = requestUri.indexOf("?");
|
|
if(question == -1) {
|
|
queryString = "";
|
|
pathInfo = requestUri[pathInfoStarts..$];
|
|
} else {
|
|
queryString = requestUri[question+1..$];
|
|
pathInfo = requestUri[pathInfoStarts..question];
|
|
}
|
|
|
|
remoteAddress = address;
|
|
|
|
if(headers[0].indexOf("HTTP/1.0")) {
|
|
http10 = true;
|
|
autoBuffer = true;
|
|
}
|
|
|
|
string contentType = "";
|
|
|
|
string[string] requestHeadersHere;
|
|
|
|
foreach(header; headers[1..$]) {
|
|
auto colon = header.indexOf(":");
|
|
if(colon == -1)
|
|
throw new Exception("HTTP headers should have a colon!");
|
|
string name = header[0..colon].toLower;
|
|
string value = header[colon+2..$]; // skip the colon and the space
|
|
|
|
requestHeadersHere[name] = value;
|
|
|
|
switch(name) {
|
|
case "accept":
|
|
accept = value;
|
|
break;
|
|
case "last-event-id":
|
|
lastEventId = value;
|
|
break;
|
|
case "authorization":
|
|
authorization = value;
|
|
break;
|
|
case "content-type":
|
|
contentType = value;
|
|
break;
|
|
case "connection":
|
|
if(value.toLower().indexOf("keep-alive") != -1)
|
|
keepAliveRequested = true;
|
|
break;
|
|
case "host":
|
|
host = value;
|
|
break;
|
|
case "accept-encoding":
|
|
if(value.indexOf("gzip") != -1)
|
|
acceptsGzip = true;
|
|
break;
|
|
case "user-agent":
|
|
userAgent = value;
|
|
break;
|
|
case "referer":
|
|
referrer = value;
|
|
break;
|
|
case "cookie":
|
|
cookie ~= value;
|
|
break;
|
|
default:
|
|
// ignore it
|
|
}
|
|
}
|
|
|
|
requestHeaders = assumeUnique(requestHeadersHere);
|
|
|
|
// Need to set up get, post, and cookies
|
|
mixin(createVariableHashes());
|
|
}
|
|
|
|
// This gets mixed in because it is shared but set inside invariant constructors
|
|
pure private static string createVariableHashes() {
|
|
return q{
|
|
if(queryString.length == 0)
|
|
get = null;//get.init;
|
|
else {
|
|
auto _get = decodeVariables(queryString);
|
|
getArray = assumeUnique(_get);
|
|
|
|
string[string] ga;
|
|
|
|
// Some sites are shit and don't let you handle multiple parameters.
|
|
// If so, compile this in and encode it as a single parameter
|
|
version(with_cgi_packed) {
|
|
auto idx = pathInfo.indexOf("PACKED");
|
|
if(idx != -1) {
|
|
auto pi = pathInfo[idx + "PACKED".length .. $];
|
|
|
|
auto _unpacked = decodeVariables(
|
|
cast(string) base64UrlDecode(pi));
|
|
|
|
foreach(k, v; _unpacked)
|
|
ga[k] = v[$-1];
|
|
|
|
pathInfo = pathInfo[0 .. idx];
|
|
}
|
|
|
|
if("arsd_packed_data" in getArray) {
|
|
auto _unpacked = decodeVariables(
|
|
cast(string) base64UrlDecode(getArray["arsd_packed_data"][0]));
|
|
|
|
foreach(k, v; _unpacked)
|
|
ga[k] = v[$-1];
|
|
}
|
|
}
|
|
|
|
foreach(k, v; getArray)
|
|
ga[k] = v[$-1];
|
|
|
|
get = assumeUnique(ga);
|
|
}
|
|
|
|
if(cookie.length == 0)
|
|
cookies = null;//cookies.init;
|
|
else {
|
|
auto _cookies = decodeVariables(cookie, "; ");
|
|
cookiesArray = assumeUnique(_cookies);
|
|
|
|
string[string] ca;
|
|
foreach(k, v; cookiesArray)
|
|
ca[k] = v[$-1];
|
|
|
|
cookies = assumeUnique(ca);
|
|
}
|
|
|
|
if(data.length == 0)
|
|
post = null;//post.init;
|
|
else {
|
|
auto terminator = contentType.indexOf(";");
|
|
if(terminator == -1)
|
|
terminator = contentType.length;
|
|
switch(contentType[0..terminator]) {
|
|
default: assert(0);
|
|
case "multipart/form-data":
|
|
string[][string] _post;
|
|
|
|
UploadedFile[string] _files;
|
|
|
|
auto b = contentType[terminator..$].indexOf("boundary=") + terminator;
|
|
assert(b >= 0, "no boundary");
|
|
immutable boundary = contentType[b+9..$];
|
|
|
|
sizediff_t pos = 0;
|
|
|
|
// all boundaries except the first should have a \r\n before them
|
|
while(pos < data.length) {
|
|
assert(data[pos] == '-', "no leading dash");
|
|
pos++;
|
|
assert(data[pos] == '-', "no second leading dash");
|
|
pos++;
|
|
//writefln("**expected** %s\n** got** %s", boundary, cast(string) data[pos..pos+boundary.length]);
|
|
assert(data[pos..pos+boundary.length] == cast(const(ubyte[])) boundary, "not lined up on boundary");
|
|
pos += boundary.length;
|
|
if(data[pos] == '\r' && data[pos+1] == '\n') {
|
|
pos += 2;
|
|
} else {
|
|
assert(data[pos] == '-', "improper ending #1");
|
|
assert(data[pos+1] == '-', "improper ending #2");
|
|
if(pos+2 != data.length) {
|
|
pos += 2;
|
|
assert(data[pos] == '\r', "not new line part 1");
|
|
assert(data[pos + 1] == '\n', "not new line part 2");
|
|
assert(pos + 2 == data.length, "wtf, wrong length");
|
|
}
|
|
break;
|
|
}
|
|
|
|
auto nextloc = locationOf(data[pos..$], boundary) + pos - 2; // the -2 is a HACK
|
|
assert(nextloc > 0, "empty piece");
|
|
assert(nextloc != -1, "no next boundary");
|
|
immutable thisOne = data[pos..nextloc-2]; // the 2 skips the leading \r\n of the next boundary
|
|
|
|
// thisOne has the headers and the data
|
|
int headerEndLocation = locationOf(thisOne, "\r\n\r\n");
|
|
assert(headerEndLocation >= 0, "no header");
|
|
auto thisOnesHeaders = thisOne[0..headerEndLocation];
|
|
auto thisOnesData = thisOne[headerEndLocation+4..$];
|
|
|
|
string[] pieceHeaders = split(cast(string) thisOnesHeaders, "\r\n");
|
|
|
|
UploadedFile piece;
|
|
bool isFile = false;
|
|
|
|
foreach(h; pieceHeaders) {
|
|
auto p = h.indexOf(":");
|
|
assert(p != -1, "no colon in header");
|
|
string hn = h[0..p];
|
|
string hv = h[p+2..$];
|
|
|
|
switch(hn.toLower) {
|
|
default: assert(0);
|
|
case "content-disposition":
|
|
auto info = hv.split("; ");
|
|
foreach(i; info[1..$]) { // skipping the form-data
|
|
auto o = i.split("="); // FIXME
|
|
string pn = o[0];
|
|
string pv = o[1][1..$-1];
|
|
|
|
if(pn == "name") {
|
|
piece.name = pv;
|
|
} else if (pn == "filename") {
|
|
piece.filename = pv;
|
|
isFile = true;
|
|
}
|
|
}
|
|
break;
|
|
case "content-type":
|
|
piece.contentType = hv;
|
|
break;
|
|
}
|
|
}
|
|
|
|
piece.content = thisOnesData;
|
|
|
|
//writefln("Piece: [%s] (%s) %d\n***%s****", piece.name, piece.filename, piece.content.length, cast(string) piece.content);
|
|
|
|
if(isFile)
|
|
_files[piece.name] = piece;
|
|
else
|
|
_post[piece.name] ~= cast(string) piece.content;
|
|
|
|
pos = nextloc;
|
|
}
|
|
|
|
postArray = assumeUnique(_post);
|
|
files = assumeUnique(_files);
|
|
break;
|
|
case "application/x-www-form-urlencoded":
|
|
auto _post = decodeVariables(cast(string) data);
|
|
postArray = assumeUnique(_post);
|
|
break;
|
|
}
|
|
string[string] pa;
|
|
foreach(k, v; postArray)
|
|
pa[k] = v[$-1];
|
|
|
|
post = assumeUnique(pa);
|
|
}
|
|
};
|
|
}
|
|
|
|
/// This represents a file the user uploaded via a POST request.
|
|
struct UploadedFile {
|
|
string name; /// The name of the form element.
|
|
string filename; /// The filename the user set.
|
|
string contentType; /// The MIME type the user's browser reported. (Not reliable.)
|
|
|
|
/**
|
|
For small files, cgi.d will buffer the uploaded file in memory, and make it
|
|
directly accessible to you through the content member. I find this very convenient
|
|
and somewhat efficient, since it can avoid hitting the disk entirely. (I
|
|
often want to inspect and modify the file anyway!)
|
|
|
|
I find the file is very large, it is undesirable to eat that much memory just
|
|
for a file buffer. In those cases, if you pass a large enough value for maxContentLength
|
|
to the constructor so they are accepted, cgi.d will write the content to a temporary
|
|
file that you can re-read later.
|
|
|
|
You can override this behavior by subclassing Cgi and overriding the protected
|
|
handlePostChunk method. Note that the object is not initialized when you
|
|
write that method - the http headers are available, but the cgi.post method
|
|
is not. You may parse the file as it streams in using this method.
|
|
|
|
|
|
Anyway, if the file is small enough to be in memory, contentInMemory will be
|
|
set to true, and the content is available in the content member.
|
|
|
|
If not, contentInMemory will be set to false, and the content saved in a file,
|
|
whose name will be available in the contentFilename member.
|
|
|
|
|
|
Tip: if you know you are always dealing with small files, and want the convenience
|
|
of ignoring this member, construct Cgi with a small maxContentLength. Then, if
|
|
a large file comes in, it simply throws an exception (and HTTP error response)
|
|
instead of trying to handle it.
|
|
|
|
The default value of maxContentLength in the constructor is for small files.
|
|
*/
|
|
bool contentInMemory;
|
|
immutable(ubyte)[] content; /// The actual content of the file, if contentInMemory == true
|
|
string contentFilename; /// the file where we dumped the content, if contentInMemory == false
|
|
}
|
|
|
|
/// Very simple method to require a basic auth username and password.
|
|
/// If the http request doesn't include the required credentials, it throws a
|
|
/// HTTP 401 error, and an exception.
|
|
///
|
|
/// Note: basic auth does not provide great security, especially over unencrypted HTTP;
|
|
/// the user's credentials are sent in plain text on every request.
|
|
///
|
|
/// If you are using Apache, the HTTP_AUTHORIZATION variable may not be sent to the
|
|
/// application. Either use Apache's built in methods for basic authentication, or add
|
|
/// something along these lines to your server configuration:
|
|
///
|
|
/// RewriteEngine On
|
|
/// RewriteCond %{HTTP:Authorization} ^(.*)
|
|
/// RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1]
|
|
///
|
|
/// To ensure the necessary data is available to cgi.d.
|
|
void requireBasicAuth(string user, string pass, string message = null) {
|
|
if(authorization != "Basic " ~ Base64.encode(cast(immutable(ubyte)[]) (user ~ ":" ~ pass))) {
|
|
setResponseStatus("401 Authorization Required");
|
|
header ("WWW-Authenticate: Basic realm=\""~message~"\"");
|
|
close();
|
|
throw new Exception("Not authorized; got " ~ authorization);
|
|
}
|
|
}
|
|
|
|
/// Very simple caching controls - setCache(false) means it will never be cached. Good for rapidly updated or sensitive sites.
|
|
/// setCache(true) means it will always be cached for as long as possible. Best for static content.
|
|
/// Use setResponseExpires and updateResponseExpires for more control
|
|
void setCache(bool allowCaching) {
|
|
noCache = !allowCaching;
|
|
}
|
|
|
|
/// Set to true and use cgi.write(data, true); to send a gzipped response to browsers
|
|
/// who can accept it
|
|
bool gzipResponse;
|
|
|
|
immutable bool acceptsGzip;
|
|
immutable bool keepAliveRequested;
|
|
|
|
/// This gets a full url for the current request, including port, protocol, host, path, and query
|
|
string getCurrentCompleteUri() const {
|
|
return format("http%s://%s%s%s",
|
|
https ? "s" : "",
|
|
host,
|
|
port == 80 ? "" : ":" ~ to!string(port),
|
|
requestUri);
|
|
}
|
|
|
|
/// Sets the HTTP status of the response. For example, "404 File Not Found" or "500 Internal Server Error".
|
|
/// It assumes "200 OK", and automatically changes to "302 Found" if you call setResponseLocation().
|
|
/// Note setResponseStatus() must be called *before* you write() any data to the output.
|
|
void setResponseStatus(string status) {
|
|
assert(!outputtedResponseData);
|
|
responseStatus = status;
|
|
}
|
|
private string responseStatus = null;
|
|
|
|
/// Sets the location header, which the browser will redirect the user to automatically.
|
|
/// Note setResponseLocation() must be called *before* you write() any data to the output.
|
|
/// The optional important argument is used if it's a default suggestion rather than something to insist upon.
|
|
void setResponseLocation(string uri, bool important = true) {
|
|
if(!important && isCurrentResponseLocationImportant)
|
|
return; // important redirects always override unimportant ones
|
|
|
|
assert(!outputtedResponseData);
|
|
responseStatus = "302 Found";
|
|
responseLocation = uri.strip;
|
|
isCurrentResponseLocationImportant = important;
|
|
}
|
|
protected string responseLocation = null;
|
|
private bool isCurrentResponseLocationImportant = false;
|
|
|
|
/// Sets the Expires: http header. See also: updateResponseExpires, setPublicCaching
|
|
/// The parameter is in unix_timestamp * 1000. Try setResponseExpires(getUTCtime() + SOME AMOUNT) for normal use.
|
|
/// Note: the when parameter is different than setCookie's expire parameter.
|
|
void setResponseExpires(long when, bool isPublic = false) {
|
|
responseExpires = when;
|
|
setCache(true); // need to enable caching so the date has meaning
|
|
|
|
responseIsPublic = isPublic;
|
|
}
|
|
private long responseExpires = long.min;
|
|
private bool responseIsPublic = false;
|
|
|
|
/// This is like setResponseExpires, but it can be called multiple times. The setting most in the past is the one kept.
|
|
/// If you have multiple functions, they all might call updateResponseExpires about their own return value. The program
|
|
/// output as a whole is as cacheable as the least cachable part in the chain.
|
|
|
|
/// setCache(false) always overrides this - it is, by definition, the strictest anti-cache statement available. If your site outputs sensitive user data, you should probably call setCache(false) when you do, to ensure no other functions will cache the content, as it may be a privacy risk.
|
|
/// Conversely, setting here overrides setCache(true), since any expiration date is in the past of infinity.
|
|
void updateResponseExpires(long when, bool isPublic) {
|
|
if(responseExpires == long.min)
|
|
setResponseExpires(when, isPublic);
|
|
else if(when < responseExpires)
|
|
setResponseExpires(when, responseIsPublic && isPublic); // if any part of it is private, it all is
|
|
}
|
|
|
|
/*
|
|
/// Set to true if you want the result to be cached publically - that is, is the content shared?
|
|
/// Should generally be false if the user is logged in. It assumes private cache only.
|
|
/// setCache(true) also turns on public caching, and setCache(false) sets to private.
|
|
void setPublicCaching(bool allowPublicCaches) {
|
|
publicCaching = allowPublicCaches;
|
|
}
|
|
private bool publicCaching = false;
|
|
*/
|
|
|
|
/// Sets an HTTP cookie, automatically encoding the data to the correct string.
|
|
/// expiresIn is how many milliseconds in the future the cookie will expire.
|
|
/// TIP: to make a cookie accessible from subdomains, set the domain to .yourdomain.com.
|
|
/// Note setCookie() must be called *before* you write() any data to the output.
|
|
void setCookie(string name, string data, long expiresIn = 0, string path = null, string domain = null, bool httpOnly = false, bool secure = false) {
|
|
assert(!outputtedResponseData);
|
|
string cookie = name ~ "=";
|
|
cookie ~= data;
|
|
if(path !is null)
|
|
cookie ~= "; path=" ~ path;
|
|
// FIXME: should I just be using max-age here? (also in cache below)
|
|
if(expiresIn != 0)
|
|
cookie ~= "; expires=" ~ printDate(cast(DateTime) Clock.currTime + dur!"msecs"(expiresIn));
|
|
if(domain !is null)
|
|
cookie ~= "; domain=" ~ domain;
|
|
if(secure == true)
|
|
cookie ~= "; Secure";
|
|
if(httpOnly == true )
|
|
cookie ~= "; HttpOnly";
|
|
|
|
responseCookies ~= cookie;
|
|
}
|
|
private string[] responseCookies;
|
|
|
|
/// Clears a previously set cookie with the given name, path, and domain.
|
|
void clearCookie(string name, string path = null, string domain = null) {
|
|
assert(!outputtedResponseData);
|
|
setCookie(name, "", 1, path, domain);
|
|
}
|
|
|
|
/// Sets the content type of the response, for example "text/html" (the default) for HTML, or "image/png" for a PNG image
|
|
void setResponseContentType(string ct) {
|
|
assert(!outputtedResponseData);
|
|
responseContentType = ct;
|
|
}
|
|
private string responseContentType = null;
|
|
|
|
/// Adds a custom header. It should be the name: value, but without any line terminator.
|
|
/// For example: header("X-My-Header: Some value");
|
|
/// Note you should use the specialized functions in this object if possible to avoid
|
|
/// duplicates in the output.
|
|
void header(string h) {
|
|
customHeaders ~= h;
|
|
}
|
|
|
|
private string[] customHeaders;
|
|
|
|
void flushHeaders(const(void)[] t, bool isAll = false) {
|
|
string[] hd;
|
|
// Flush the headers
|
|
if(responseStatus !is null) {
|
|
if(nph) {
|
|
if(http10)
|
|
hd ~= "HTTP/1.0 " ~ responseStatus;
|
|
else
|
|
hd ~= "HTTP/1.1 " ~ responseStatus;
|
|
} else
|
|
hd ~= "Status: " ~ responseStatus;
|
|
} else if (nph) {
|
|
if(http10)
|
|
hd ~= "HTTP/1.0 200 OK";
|
|
else
|
|
hd ~= "HTTP/1.1 200 OK";
|
|
}
|
|
if(nph) { // we're responsible for setting the date too according to http 1.1
|
|
hd ~= "Date: " ~ printDate(cast(DateTime) Clock.currTime);
|
|
}
|
|
|
|
// FIXME: what if the user wants to set his own content-length?
|
|
// The custom header function can do it, so maybe that's best.
|
|
// Or we could reuse the isAll param.
|
|
if(responseLocation !is null) {
|
|
hd ~= "Location: " ~ responseLocation;
|
|
}
|
|
if(!noCache && responseExpires != long.min) { // an explicit expiration date is set
|
|
auto expires = SysTime(unixTimeToStdTime(cast(int)(responseExpires / 1000)), UTC());
|
|
hd ~= "Expires: " ~ printDate(
|
|
cast(DateTime) expires);
|
|
// FIXME: assuming everything is private unless you use nocache - generally right for dynamic pages, but not necessarily
|
|
hd ~= "Cache-Control: "~(responseIsPublic ? "public" : "private")~", no-cache=\"set-cookie, set-cookie2\"";
|
|
}
|
|
if(responseCookies !is null && responseCookies.length > 0) {
|
|
foreach(c; responseCookies)
|
|
hd ~= "Set-Cookie: " ~ c;
|
|
}
|
|
if(noCache) { // we specifically do not want caching (this is actually the default)
|
|
hd ~= "Cache-Control: private, no-cache=\"set-cookie\"";
|
|
hd ~= "Expires: 0";
|
|
hd ~= "Pragma: no-cache";
|
|
} else {
|
|
if(responseExpires == long.min) { // caching was enabled, but without a date set - that means assume cache forever
|
|
hd ~= "Cache-Control: public";
|
|
hd ~= "Expires: Tue, 31 Dec 2030 14:00:00 GMT"; // FIXME: should not be more than one year in the future
|
|
}
|
|
}
|
|
if(responseContentType !is null) {
|
|
hd ~= "Content-Type: " ~ responseContentType;
|
|
} else
|
|
hd ~= "Content-Type: text/html; charset=utf-8";
|
|
|
|
if(gzipResponse && acceptsGzip && isAll) { // FIXME: isAll really shouldn't be necessary
|
|
hd ~= "Content-Encoding: gzip";
|
|
}
|
|
|
|
if(customHeaders !is null)
|
|
hd ~= customHeaders;
|
|
|
|
if(!isAll) {
|
|
if(nph && !http10) {
|
|
hd ~= "Transfer-Encoding: chunked";
|
|
responseChunked = true;
|
|
}
|
|
} else {
|
|
hd ~= "Content-Length: " ~ to!string(t.length);
|
|
if(nph && keepAliveRequested) {
|
|
hd ~= "Connection: Keep-Alive";
|
|
}
|
|
}
|
|
|
|
// FIXME: what about duplicated headers?
|
|
|
|
foreach(h; hd) {
|
|
if(rawDataOutput !is null)
|
|
rawDataOutput(cast(const(ubyte)[]) (h ~ "\r\n"));
|
|
else
|
|
writeln(h);
|
|
}
|
|
if(rawDataOutput !is null)
|
|
rawDataOutput(cast(const(ubyte)[]) ("\r\n"));
|
|
else
|
|
writeln("");
|
|
|
|
outputtedResponseData = true;
|
|
}
|
|
|
|
/// Writes the data to the output, flushing headers if they have not yet been sent.
|
|
void write(const(void)[] t, bool isAll = false) {
|
|
assert(!closed, "Output has already been closed");
|
|
|
|
if(gzipResponse && acceptsGzip && isAll) { // FIXME: isAll really shouldn't be necessary
|
|
// actually gzip the data here
|
|
|
|
auto c = new Compress(HeaderFormat.gzip); // want gzip
|
|
|
|
auto data = c.compress(t);
|
|
data ~= c.flush();
|
|
|
|
// std.file.write("/tmp/last-item", data);
|
|
|
|
t = data;
|
|
}
|
|
|
|
if(!outputtedResponseData && (!autoBuffer || isAll)) {
|
|
flushHeaders(t, isAll);
|
|
}
|
|
|
|
if(requestMethod != RequestMethod.HEAD && t.length > 0) {
|
|
if (autoBuffer) {
|
|
outputBuffer ~= cast(ubyte[]) t;
|
|
}
|
|
if(!autoBuffer || isAll) {
|
|
if(rawDataOutput !is null)
|
|
if(nph && responseChunked)
|
|
rawDataOutput(makeChunk(cast(const(ubyte)[]) t));
|
|
else
|
|
rawDataOutput(cast(const(ubyte)[]) t);
|
|
else
|
|
stdout.rawWrite(t);
|
|
}
|
|
}
|
|
}
|
|
|
|
void flush() {
|
|
if(rawDataOutput is null)
|
|
stdout.flush();
|
|
// FIXME: also flush to other sources
|
|
}
|
|
|
|
version(autoBuffer)
|
|
bool autoBuffer = true;
|
|
else
|
|
bool autoBuffer = false;
|
|
ubyte[] outputBuffer;
|
|
|
|
/// Flushes the buffers to the network, signifying that you are done.
|
|
/// You should always call this explicitly when you are done outputting data.
|
|
void close() {
|
|
if(closed)
|
|
return; // don't double close
|
|
|
|
if(!outputtedResponseData)
|
|
write("");
|
|
|
|
// writing auto buffered data
|
|
if(requestMethod != RequestMethod.HEAD && autoBuffer) {
|
|
if(!nph)
|
|
stdout.rawWrite(outputBuffer);
|
|
else
|
|
write(outputBuffer, true); // tell it this is everything
|
|
}
|
|
|
|
// closing the last chunk...
|
|
if(nph && rawDataOutput !is null && responseChunked)
|
|
rawDataOutput(cast(const(ubyte)[]) "0\r\n\r\n");
|
|
|
|
closed = true;
|
|
}
|
|
|
|
// Closes without doing anything, shouldn't be used often
|
|
void rawClose() {
|
|
closed = true;
|
|
}
|
|
|
|
/// Gets a request variable as a specific type, or the default value of it isn't there
|
|
/// or isn't convertable to the request type. Checks both GET and POST variables.
|
|
T request(T = string)(in string name, in T def = T.init) const nothrow {
|
|
try {
|
|
return
|
|
(name in post) ? to!T(post[name]) :
|
|
(name in get) ? to!T(get[name]) :
|
|
def;
|
|
} catch(Exception e) { return def; }
|
|
}
|
|
|
|
bool isClosed() const {
|
|
return closed;
|
|
}
|
|
|
|
/* Hooks for redirecting input and output */
|
|
private void delegate(const(ubyte)[]) rawDataOutput = null;
|
|
|
|
/* This info is used when handling a more raw HTTP protocol */
|
|
private bool nph;
|
|
private bool http10;
|
|
private bool closed;
|
|
private bool responseChunked = false;
|
|
|
|
version(preserveData) // note: this can eat lots of memory; don't use unless you're sure you need it.
|
|
immutable(ubyte)[] originalPostData;
|
|
|
|
/* Internal state flags */
|
|
private bool outputtedResponseData;
|
|
private bool noCache = true;
|
|
|
|
/** What follows is data gotten from the HTTP request. It is all fully immutable,
|
|
partially because it logically is (your code doesn't change what the user requested...)
|
|
and partially because I hate how bad programs in PHP change those superglobals to do
|
|
all kinds of hard to follow ugliness. I don't want that to ever happen in D.
|
|
|
|
For some of these, you'll want to refer to the http or cgi specs for more details.
|
|
*/
|
|
immutable(string[string]) requestHeaders; /// All the raw headers in the request as name/value pairs. The name is stored as all lower case, but otherwise the same as it is in HTTP; words separated by dashes. For example, "cookie" or "accept-encoding". Many HTTP headers have specialized variables below for more convenience and static name checking; you should generally try to use them.
|
|
|
|
immutable(char[]) host; /// The hostname in the request. If one program serves multiple domains, you can use this to differentiate between them.
|
|
immutable(char[]) userAgent; /// The browser's user-agent string. Can be used to identify the browser.
|
|
immutable(char[]) pathInfo; /// This is any stuff sent after your program's name on the url, but before the query string. For example, suppose your program is named "app". If the user goes to site.com/app, pathInfo is empty. But, he can also go to site.com/app/some/sub/path; treating your program like a virtual folder. In this case, pathInfo == "/some/sub/path".
|
|
immutable(char[]) scriptName; /// The full base path of your program, as seen by the user. If your program is located at site.com/programs/apps, scriptName == "/programs/apps".
|
|
immutable(char[]) authorization; /// The full authorization string from the header, undigested. Useful for implementing auth schemes such as OAuth 1.0. Note that some web servers do not forward this to the app without taking extra steps. See requireBasicAuth's comment for more info.
|
|
immutable(char[]) accept; /// The HTTP accept header is the user agent telling what content types it is willing to accept. This is often */*; they accept everything, so it's not terribly useful. (The similar sounding Accept-Encoding header is handled automatically for chunking and gzipping. Simply set gzipResponse = true and cgi.d handles the details, zipping if the user's browser is willing to accept it.
|
|
immutable(char[]) lastEventId; /// The HTML 5 draft includes an EventSource() object that connects to the server, and remains open to take a stream of events. My arsd.rtud module can help with the server side part of that. The Last-Event-Id http header is defined in the draft to help handle loss of connection. When the browser reconnects to you, it sets this header to the last event id it saw, so you can catch it up. This member has the contents of that header.
|
|
|
|
immutable(RequestMethod) requestMethod; /// The HTTP request verb: GET, POST, etc. It is represented as an enum in cgi.d (which, like many enums, you can convert back to string with std.conv.to()). A HTTP GET is supposed to, according to the spec, not have side effects; a user can GET something over and over again and always have the same result. On all requests, the get[] and getArray[] members may be filled in. The post[] and postArray[] members are only filled in on POST methods.
|
|
immutable(char[]) queryString; /// The unparsed content of the request query string - the stuff after the ? in your URL. See get[] and getArray[] for a parse view of it. Sometimes, the unparsed string is useful though if you want a custom format of data up there (probably not a good idea, unless it is really simple, like "?username" perhaps.)
|
|
immutable(char[]) cookie; /// The unparsed content of the Cookie: header in the request. See also the cookies[string] member for a parsed view of the data.
|
|
/** The Referer header from the request. (It is misspelled in the HTTP spec, and thus the actual request and cgi specs too, but I spelled the word correctly here because that's sane. The spec's misspelling is an implementation detail.) It contains the site url that referred the user to your program; the site that linked to you, or if you're serving images, the site that has you as an image. Also, if you're in an iframe, the referrer is the site that is framing you.
|
|
|
|
Important note: if the user copy/pastes your url, this is blank, and, just like with all other user data, their browsers can also lie to you. Don't rely on it for real security.
|
|
*/
|
|
immutable(char[]) referrer;
|
|
immutable(char[]) requestUri; /// The full url if the current request, excluding the protocol and host. requestUri == scriptName ~ pathInfo ~ (queryString.length ? "?" ~ queryString : "");
|
|
|
|
immutable(char[]) remoteAddress; /// The IP address of the user, as we see it. (Might not match the IP of the user's computer due to things like proxies and NAT.)
|
|
|
|
immutable bool https; /// Was the request encrypted via https?
|
|
immutable int port; /// On what TCP port number did the server receive the request?
|
|
|
|
/** Here come the parsed request variables - the things that come close to PHP's _GET, _POST, etc. superglobals in content. */
|
|
|
|
immutable(string[string]) get; /// The data from your query string in the url, only showing the last string of each name. If you want to handle multiple values with the same name, use getArray. This only works right if the query string is x-www-form-urlencoded; the default you see on the web with name=value pairs separated by the & character.
|
|
immutable(string[string]) post; /// The data from the request's body, on POST requests. It parses application/x-www-form-urlencoded data (used by most web requests, including typical forms), and multipart/form-data requests (used by file uploads on web forms) into the same container, so you can always access them the same way. It makes no attempt to parse other content types. If you want to accept an XML Post body (for a web api perhaps), you'll need to handle the raw data yourself.
|
|
immutable(string[string]) cookies; /// Separates out the cookie header into individual name/value pairs (which is how you set them!)
|
|
immutable(UploadedFile[string]) files; /// Represents user uploaded files. When making a file upload form, be sure to follow the standard: set method="POST" and enctype="multipart/form-data" in your html <form> tag attributes. The key into this array is the name attribute on your input tag, just like with other post variables. See the comments on the UploadedFile struct for more information about the data inside, including important notes on max size and content location.
|
|
|
|
/// Use these if you expect multiple items submitted with the same name. btw, assert(get[name] is getArray[name][$-1); should pass. Same for post and cookies.
|
|
/// the order of the arrays is the order the data arrives
|
|
immutable(string[][string]) getArray; /// like get, but an array of values per name
|
|
immutable(string[][string]) postArray; /// ditto for post
|
|
immutable(string[][string]) cookiesArray; /// ditto for cookies
|
|
|
|
// FIXME: what about multiple files with the same name?
|
|
private:
|
|
//RequestMethod _requestMethod;
|
|
}
|
|
|
|
|
|
// should this be a separate module? Probably, but that's a hassle.
|
|
|
|
/// Represents a url that can be broken down or built up through properties
|
|
// FIXME: finish this
|
|
struct Url {
|
|
string uri;
|
|
alias uri this;
|
|
|
|
this(string uri) {
|
|
this.uri = uri;
|
|
}
|
|
|
|
string toString() {
|
|
return uri;
|
|
}
|
|
|
|
/// Returns a new absolute Url 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.)
|
|
Url basedOn(in Url baseUrl) const {
|
|
Url n = this;
|
|
|
|
// if anything is given in the existing url, we don't use the base anymore.
|
|
if(n.protocol is null) {
|
|
n.protocol = baseUrl.protocol;
|
|
if(n.server is null) {
|
|
n.server = baseUrl.server;
|
|
if(n.port == 0) {
|
|
n.port = baseUrl.port;
|
|
if(n.path.length > 0 && n.path[0] != '/') {
|
|
n.path = baseUrl.path[0 .. baseUrl.path.lastIndexOf("/") + 1] ~ n.path;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
n.uri = n.toString();
|
|
|
|
return n;
|
|
}
|
|
|
|
// This can sometimes be a big pain in the butt for me, so lots of copy/paste here to cover
|
|
// the possibilities.
|
|
unittest {
|
|
auto url = Url("cool.html"); // checking relative links
|
|
assert(url.basedOn(Url("http://test.com/what/test.html")) == "http://test.com/what/cool.html");
|
|
assert(url.basedOn(Url("https://test.com/what/test.html")) == "https://test.com/what/cool.html");
|
|
assert(url.basedOn(Url("http://test.com/what/")) == "http://test.com/what/cool.html");
|
|
assert(url.basedOn(Url("http://test.com/")) == "http://test.com/cool.html");
|
|
assert(url.basedOn(Url("http://test.com")) == "http://test.com/cool.html");
|
|
assert(url.basedOn(Url("http://test.com/what/test.html?a=b")) == "http://test.com/what/cool.html");
|
|
assert(url.basedOn(Url("http://test.com/what/test.html?a=b&c=d")) == "http://test.com/what/cool.html");
|
|
assert(url.basedOn(Url("http://test.com/what/test.html?a=b&c=d#what")) == "http://test.com/what/cool.html");
|
|
assert(url.basedOn(Url("http://test.com")) == "http://test.com/cool.html");
|
|
|
|
url = Url("/something/cool.html"); // same server, different path
|
|
assert(url.basedOn(Url("http://test.com/what/test.html")) == "http://test.com/something/cool.html");
|
|
assert(url.basedOn(Url("https://test.com/what/test.html")) == "https://test.com/something/cool.html");
|
|
assert(url.basedOn(Url("http://test.com/what/")) == "http://test.com/something/cool.html");
|
|
assert(url.basedOn(Url("http://test.com/")) == "http://test.com/something/cool.html");
|
|
assert(url.basedOn(Url("http://test.com")) == "http://test.com/something/cool.html");
|
|
assert(url.basedOn(Url("http://test.com/what/test.html?a=b")) == "http://test.com/something/cool.html");
|
|
assert(url.basedOn(Url("http://test.com/what/test.html?a=b&c=d")) == "http://test.com/something/cool.html");
|
|
assert(url.basedOn(Url("http://test.com/what/test.html?a=b&c=d#what")) == "http://test.com/something/cool.html");
|
|
assert(url.basedOn(Url("http://test.com")) == "http://test.com/something/cool.html");
|
|
|
|
url = Url("?query=answer"); // same path. server, protocol, and port, just different query string and fragment
|
|
assert(url.basedOn(Url("http://test.com/what/test.html")) == "http://test.com/what/test.html?query=answer");
|
|
assert(url.basedOn(Url("https://test.com/what/test.html")) == "https://test.com/what/test.html?query=answer");
|
|
assert(url.basedOn(Url("http://test.com/what/")) == "http://test.com/what/?query=answer");
|
|
assert(url.basedOn(Url("http://test.com/")) == "http://test.com/?query=answer");
|
|
assert(url.basedOn(Url("http://test.com")) == "http://test.com?query=answer");
|
|
assert(url.basedOn(Url("http://test.com/what/test.html?a=b")) == "http://test.com/what/test.html?query=answer");
|
|
assert(url.basedOn(Url("http://test.com/what/test.html?a=b&c=d")) == "http://test.com/what/test.html?query=answer");
|
|
assert(url.basedOn(Url("http://test.com/what/test.html?a=b&c=d#what")) == "http://test.com/what/test.html?query=answer");
|
|
assert(url.basedOn(Url("http://test.com")) == "http://test.com?query=answer");
|
|
|
|
url = Url("#anchor"); // everything should remain the same except the anchor
|
|
|
|
url = Url("//example.com"); // same protocol, but different server. the path here should be blank.
|
|
|
|
url = Url("//example.com/example.html"); // same protocol, but different server and path
|
|
|
|
url = Url("http://example.com/test.html"); // completely absolute link should never be modified
|
|
|
|
url = Url("http://example.com"); // completely absolute link should never be modified, even if it has no path
|
|
|
|
// FIXME: add something for port too
|
|
|
|
|
|
}
|
|
|
|
string protocol;
|
|
string server;
|
|
int port;
|
|
string path;
|
|
string query;
|
|
string fragment;
|
|
}
|
|
|
|
|
|
/*
|
|
for session, see web.d
|
|
*/
|
|
|
|
/// breaks down a url encoded string
|
|
string[][string] decodeVariables(string data, string separator = "&") {
|
|
auto vars = data.split(separator);
|
|
string[][string] _get;
|
|
foreach(var; vars) {
|
|
auto equal = var.indexOf("=");
|
|
if(equal == -1) {
|
|
_get[decodeComponent(var)] ~= "";
|
|
} else {
|
|
//_get[decodeComponent(var[0..equal])] ~= decodeComponent(var[equal + 1 .. $].replace("+", " "));
|
|
// stupid + -> space conversion.
|
|
_get[decodeComponent(var[0..equal]).replace("+", " ")] ~= decodeComponent(var[equal + 1 .. $].replace("+", " "));
|
|
}
|
|
}
|
|
return _get;
|
|
}
|
|
|
|
/// breaks down a url encoded string, but only returns the last value of any array
|
|
string[string] decodeVariablesSingle(string data) {
|
|
string[string] va;
|
|
auto varArray = decodeVariables(data);
|
|
foreach(k, v; varArray)
|
|
va[k] = v[$-1];
|
|
|
|
return va;
|
|
}
|
|
|
|
/// url encodes the whole string
|
|
string encodeVariables(in string[string] data) {
|
|
string ret;
|
|
|
|
bool outputted = false;
|
|
foreach(k, v; data) {
|
|
if(outputted)
|
|
ret ~= "&";
|
|
else
|
|
outputted = true;
|
|
|
|
ret ~= std.uri.encodeComponent(k) ~ "=" ~ std.uri.encodeComponent(v);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/// url encodes a whole string
|
|
string encodeVariables(in string[][string] data) {
|
|
string ret;
|
|
|
|
bool outputted = false;
|
|
foreach(k, arr; data) {
|
|
foreach(v; arr) {
|
|
if(outputted)
|
|
ret ~= "&";
|
|
else
|
|
outputted = true;
|
|
ret ~= std.uri.encodeComponent(k) ~ "=" ~ std.uri.encodeComponent(v);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
// http helper functions
|
|
|
|
const(ubyte)[] makeChunk(const(ubyte)[] data) {
|
|
const(ubyte)[] ret;
|
|
|
|
ret = cast(const(ubyte)[]) toHex(cast(int) data.length);
|
|
ret ~= cast(const(ubyte)[]) "\r\n";
|
|
ret ~= data;
|
|
ret ~= cast(const(ubyte)[]) "\r\n";
|
|
|
|
return ret;
|
|
}
|
|
|
|
string toHex(int num) {
|
|
string ret;
|
|
while(num) {
|
|
int v = num % 16;
|
|
num /= 16;
|
|
char d = cast(char) ((v < 10) ? v + '0' : (v-10) + 'a');
|
|
ret ~= d;
|
|
}
|
|
|
|
return to!string(array(ret.retro));
|
|
}
|
|
|
|
// the generic mixins
|
|
|
|
/// Use this instead of writing your own main
|
|
mixin template GenericMain(alias fun, T...) {
|
|
mixin CustomCgiMain!(Cgi, fun, T);
|
|
}
|
|
|
|
/// If you want to use a subclass of Cgi with generic main, use this mixin.
|
|
mixin template CustomCgiMain(CustomCgi, alias fun, T...) if(is(CustomCgi : Cgi)) {
|
|
// kinda hacky - the T... is passed to Cgi's constructor in standard cgi mode, and ignored elsewhere
|
|
version(embedded_httpd)
|
|
import arsd.httpd;
|
|
|
|
void main() {
|
|
version(embedded_httpd) {
|
|
serveHttp(&fun, 8080);//5005);
|
|
return;
|
|
}
|
|
|
|
version(fastcgi) {
|
|
FCGX_Stream* input, output, error;
|
|
FCGX_ParamArray env;
|
|
|
|
const(ubyte)[] getFcgiChunk() {
|
|
const(ubyte)[] ret;
|
|
while(FCGX_HasSeenEOF(input) != -1)
|
|
ret ~= cast(ubyte) FCGX_GetChar(input);
|
|
return ret;
|
|
}
|
|
|
|
void writeFcgi(const(ubyte)[] data) {
|
|
FCGX_PutStr(data.ptr, data.length, output);
|
|
}
|
|
while(FCGX_Accept(&input, &output, &error, &env) >= 0) {
|
|
string[string] fcgienv;
|
|
|
|
for(auto e = env; e !is null && *e !is null; e++) {
|
|
string cur = to!string(*e);
|
|
auto idx = cur.indexOf("=");
|
|
string name, value;
|
|
if(idx == -1)
|
|
name = cur;
|
|
else {
|
|
name = cur[0 .. idx];
|
|
value = cur[idx + 1 .. $];
|
|
}
|
|
|
|
fcgienv[name] = value;
|
|
}
|
|
|
|
string getFcgiEnvVar(string what) {
|
|
if(what in fcgienv)
|
|
return fcgienv[what];
|
|
return "";
|
|
}
|
|
|
|
auto cgi = new CustomCgi(5_000_000, fcgienv, &getFcgiChunk, &writeFcgi);
|
|
try {
|
|
fun(cgi);
|
|
cgi.close();
|
|
} catch(Throwable t) {
|
|
if(1) { // !cgi.isClosed)
|
|
auto msg = t.toString;
|
|
FCGX_PutStr(cast(ubyte*) msg.ptr, msg.length, error);
|
|
msg = "Status: 500 Internal Server Error\n";
|
|
msg ~= "Content-Type: text/plain\n\n";
|
|
debug msg ~= t.toString;
|
|
else msg ~= "An unexpected error has occurred.";
|
|
|
|
FCGX_PutStr(cast(ubyte*) msg.ptr, msg.length, output);
|
|
}
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
auto cgi = new CustomCgi(T);
|
|
|
|
try {
|
|
fun(cgi);
|
|
cgi.close();
|
|
} catch (Throwable c) {
|
|
// if the thing is closed, the app probably wrote an error message already, don't do it again.
|
|
//if(cgi.isClosed)
|
|
//goto doNothing;
|
|
|
|
// FIXME: this sucks
|
|
string message = "An unexpected error has occurred.";
|
|
|
|
debug message = c.toString();
|
|
|
|
writefln("Status: 500 Internal Server Error\nContent-Type: text/html\n\n%s", "<html><head><title>Internal Server Error</title></head><body><br><br><br><br><code><pre>"~(std.array.replace(std.array.replace(message, "<", "<"), ">", ">"))~"</pre></code></body></html>");
|
|
|
|
string str = c.toString();
|
|
auto idx = str.indexOf("\n");
|
|
if(idx != -1)
|
|
str = str[0..idx];
|
|
|
|
doNothing:
|
|
|
|
stderr.writeln(str);
|
|
}
|
|
}
|
|
}
|
|
|
|
string printDate(DateTime date) {
|
|
return format(
|
|
"%.3s, %02d %.3s %d %02d:%02d:%02d GMT", // could be UTC too
|
|
to!string(date.dayOfWeek).capitalize,
|
|
date.day,
|
|
to!string(date.month).capitalize,
|
|
date.year,
|
|
date.hour,
|
|
date.minute,
|
|
date.second);
|
|
}
|
|
|
|
|
|
version(with_cgi_packed) {
|
|
// This is temporary until Phobos supports base64
|
|
immutable(ubyte)[] base64UrlDecode(string e) {
|
|
string encoded = e.idup;
|
|
while (encoded.length % 4) {
|
|
encoded ~= "="; // add padding
|
|
}
|
|
|
|
// convert base64 URL to standard base 64
|
|
encoded = encoded.replace("-", "+");
|
|
encoded = encoded.replace("_", "/");
|
|
|
|
return cast(immutable(ubyte)[]) Base64.decode(encoded);
|
|
}
|
|
// should be set as arsd_packed_data
|
|
string packedDataEncode(in string[string] variables) {
|
|
string result;
|
|
|
|
bool outputted = false;
|
|
foreach(k, v; variables) {
|
|
if(outputted)
|
|
result ~= "&";
|
|
else
|
|
outputted = true;
|
|
|
|
result ~= std.uri.encodeComponent(k) ~ "=" ~ std.uri.encodeComponent(v);
|
|
}
|
|
|
|
result = cast(string) Base64.encode(cast(ubyte[]) result);
|
|
|
|
// url variant
|
|
result.replace("=", "");
|
|
result.replace("+", "-");
|
|
result.replace("/", "_");
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
|
|
// Referencing this gigantic typeid seems to remind the compiler
|
|
// to actually put the symbol in the object file. I guess the immutable
|
|
// assoc array array isn't actually included in druntime
|
|
void hackAroundLinkerError() {
|
|
writeln(typeid(const(immutable(char)[][])[immutable(char)[]]));
|
|
writeln(typeid(immutable(char)[][][immutable(char)[]]));
|
|
writeln(typeid(Cgi.UploadedFile[immutable(char)[]]));
|
|
writeln(typeid(immutable(Cgi.UploadedFile)[immutable(char)[]]));
|
|
writeln(typeid(immutable(char[])[immutable(char)[]]));
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
version(fastcgi) {
|
|
pragma(lib, "fcgi");
|
|
extern(C) {
|
|
|
|
struct FCGX_Stream {
|
|
ubyte* rdNext;
|
|
ubyte* wrNext;
|
|
ubyte* stop;
|
|
ubyte* stopUnget;
|
|
int isReader;
|
|
int isClosed;
|
|
int wasFCloseCalled;
|
|
int FCGI_errno;
|
|
void* function(FCGX_Stream* stream) fillBuffProc;
|
|
void* function(FCGX_Stream* stream, int doClose) emptyBuffProc;
|
|
void* data;
|
|
}
|
|
|
|
alias char** FCGX_ParamArray;
|
|
|
|
int FCGX_Accept(FCGX_Stream** stdin, FCGX_Stream** stdout, FCGX_Stream** stderr, FCGX_ParamArray* envp);
|
|
int FCGX_GetChar(FCGX_Stream* stream);
|
|
int FCGX_PutStr(const ubyte* str, int n, FCGX_Stream* stream);
|
|
int FCGX_HasSeenEOF(FCGX_Stream* stream);
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
Copyright: Adam D. Ruppe, 2008 - 2011
|
|
License: <a href="http://www.boost.org/LICENSE_1_0.txt">Boost License 1.0</a>.
|
|
Authors: Adam D. Ruppe
|
|
|
|
Copyright Adam D. Ruppe 2008 - 2011.
|
|
Distributed under the Boost Software License, Version 1.0.
|
|
(See accompanying file LICENSE_1_0.txt or copy at
|
|
http://www.boost.org/LICENSE_1_0.txt)
|
|
*/
|