Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 16 additions & 27 deletions danode/acme.d
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,16 @@ version(SSL) {
JSONValue order = newOrder(dir, pkey, kid, domain, orderURL);
log(Level.Verbose, "ACME: order: %s, orderURL: %s", order.toString(), orderURL);
string[] tokens;
scope(exit) synchronized(getAcmeMutex()) { foreach (t; tokens) acmeChallenges.remove(t); }

foreach (authURL; order["authorizations"].array) {
JSONValue challenge = getHTTP01Challenge(authURL.str, dir, pkey, kid);
if (challenge.type == JSONType.null_) return false;
tokens ~= prepareChallenge(challenge, pkey);
triggerChallenge(challenge, dir, pkey, kid);
}

if (!pollAllAuthorizations(order, dir, pkey, kid)) {
foreach (t; tokens) synchronized(getAcmeMutex()) { acmeChallenges.remove(t); }
return false;
}
foreach (t; tokens) synchronized(getAcmeMutex()) { acmeChallenges.remove(t); }
if (!pollAllAuthorizations(order, dir, pkey, kid)) { return(false); }

JSONValue finalized = finalizeOrder(order, dir, pkey, kid, csrPath);
log(Level.Verbose, "ACME: order status: %s, finalized: %s", finalized["status"].str, finalized.toString());
Expand Down Expand Up @@ -204,21 +202,23 @@ version(SSL) {
return JSONValue.init;
}

// Compute SHA256 thumbprint of the public JWK
string jwkThumbprint(EVP_PKEY* pkey) {
// JWK thumbprint requires canonical JSON: sorted keys, no whitespace
// Extract RSA public key parameters as byte arrays
void extractRSAParams(EVP_PKEY* pkey, out ubyte[] nbuf, out ubyte[] ebuf) {
BIGNUM* bn_n = BN_new();
BIGNUM* bn_e = BN_new();
scope(exit) { BN_free(bn_n); BN_free(bn_e); }
EVP_PKEY_get_bn_param(pkey, "n", &bn_n);
EVP_PKEY_get_bn_param(pkey, "e", &bn_e);
int nlen = BN_num_bytes(bn_n);
int elen = BN_num_bytes(bn_e);
ubyte[] nbuf = new ubyte[](nlen);
ubyte[] ebuf = new ubyte[](elen);
nbuf = new ubyte[](BN_num_bytes(bn_n));
ebuf = new ubyte[](BN_num_bytes(bn_e));
BN_bn2bin(bn_n, nbuf.ptr);
BN_bn2bin(bn_e, ebuf.ptr);
BN_free(bn_n);
BN_free(bn_e);
}

// Compute SHA256 thumbprint of the public JWK
string jwkThumbprint(EVP_PKEY* pkey) {
ubyte[] nbuf, ebuf;
pkey.extractRSAParams(nbuf, ebuf);

// RFC 7638 canonical form - keys must be sorted alphabetically
string canonical = `{"e":"` ~ b64url(ebuf) ~ `","kty":"RSA","n":"` ~ b64url(nbuf) ~ `"}`;
Expand Down Expand Up @@ -253,19 +253,8 @@ version(SSL) {

// Extract public key as JWK JSON (for newAccount header)
string jwkPublic(EVP_PKEY* pkey) {
BIGNUM* bn_n = BN_new();
BIGNUM* bn_e = BN_new();
EVP_PKEY_get_bn_param(pkey, "n", &bn_n);
EVP_PKEY_get_bn_param(pkey, "e", &bn_e);

int nlen = BN_num_bytes(bn_n);
int elen = BN_num_bytes(bn_e);
ubyte[] nbuf = new ubyte[](nlen);
ubyte[] ebuf = new ubyte[](elen);
BN_bn2bin(bn_n, nbuf.ptr);
BN_bn2bin(bn_e, ebuf.ptr);
BN_free(bn_n);
BN_free(bn_e);
ubyte[] nbuf, ebuf;
pkey.extractRSAParams(nbuf, ebuf);

JSONValue jwk = ["kty": JSONValue("RSA"), "n": JSONValue(b64url(nbuf)), "e": JSONValue(b64url(ebuf))];
return toJSON(jwk);
Expand Down
3 changes: 1 addition & 2 deletions danode/client.d
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ class Client : Thread, ClientInterface {
stop(); continue;
}
log(Level.Trace, "Connection %s:%s (%s msecs) %s", ip, port, starttime, to!string(driver.inbuffer.data));
Thread.sleep(dur!"msecs"(2));
}
this.log(request, response);
} catch(Exception e) { log(Level.Verbose, "Unknown Client Exception: %s", e); stop();
Expand Down Expand Up @@ -119,7 +118,7 @@ void log(in ClientInterface cl, in Request rq, in Response rs) {
int code = cast(int)(rs.payload ? rs.statuscode.code : 0);
long ms = rq.starttime == SysTime.init ? -1 : Msecs(rq.starttime);
tag(Level.Always, format("%d", code),
"%s %s:%s %s%s [%d] %s %skb", htmltime(), cl.ip, cl.port, rq.shorthost, uri.replace("%", "%%"), cl.requests, ms, bytes/1024);
"%s %s:%s %s%s [%d] %.1fkb in %s ms ", htmltime(), cl.ip, cl.port, rq.shorthost, uri.replace("%", "%%"), cl.requests, bytes/1024f, ms);
}

// serve a 408 connection timed out page
Expand Down
21 changes: 12 additions & 9 deletions danode/files.d
Original file line number Diff line number Diff line change
Expand Up @@ -73,34 +73,37 @@ class FilePayload : Payload {
/* Does the file require to be updated before sending ? */
final bool needsupdate() {
if (!isStaticFile()) return false; // CGI files are never buffered, since they are executed
if (fileSize() > 0 && fileSize() < buffermaxsize) { //
ptrdiff_t sz = fileSize();
if (sz > 0 && sz < buffermaxsize) { //
if (!buffered) { log(Level.Trace, "File: '%s' needs buffering", path); return true; }
if (mtime > btime) { log(Level.Trace, "File: '%s' stale record", path); return true; }
}else{
log(Level.Verbose, "File: '%s' exceeds buffer (%dkb > %dkb)", path, fileSize() / 1024, buffermaxsize / 1024);
log(Level.Verbose, "File: '%s' exceeds buffer (%.1fkb > %.1fkb)", path, sz / 1024f, buffermaxsize / 1024f);
}
return false;
}

/* Reads the file into the internal buffer, and compress the buffer to the enc buffer
Updates the buffer time and status.
*/
final void buffer() { synchronized {
if(buf is null) buf = new char[](fileSize());
buf.length = fileSize();
final bool buffer() { synchronized {
if (!needsupdate()) return(false); // re-check under lock
ptrdiff_t sz = fileSize();
if(buf is null) buf = new char[](sz);
buf.length = sz;
try {
auto f = File(path, "rb");
f.rawRead(buf);
f.close();
} catch (Exception e) { error("Exception during buffering '%s': %s", path, e.msg); return; }
} catch (Exception e) { error("Exception during buffering '%s': %s", path, e.msg); return(false); }
try {
auto c = new Compress(6, HeaderFormat.gzip);
encbuf = cast(char[])(c.compress(buf));
encbuf ~= cast(char[])(c.flush());
} catch (Exception e) { error("Exception during compressing '%s': %s", path, e.msg); }
} catch (Exception e) { error("Exception during compressing '%s': %s", path, e.msg); return(false); }
btime = Clock.currTime();
log(Level.Trace, "File: '%s' buffered %d|%d bytes", path, fileSize(), encbuf.length);
buffered = true;
log(Level.Trace, "File: '%s' buffered %.1fkb|%.1fkb", path, sz / 1024f, encbuf.length / 1024f);
return(buffered = true);
} }

/* Whole file content served via the bytes function */
Expand Down
13 changes: 4 additions & 9 deletions danode/filesystem.d
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,7 @@ class FileSystem {
if (!domain.files.has(shortname)) {
domain.files[shortname] = new FilePayload(f.name, maxsize);
domain.entries++;
if (domain.files[shortname].needsupdate()) {
domain.files[shortname].buffer();
domain.buffered++;
}
if(domain.files[shortname].buffer()) { domain.buffered++; }
}
}
}
Expand Down Expand Up @@ -97,11 +94,9 @@ class FileSystem {

/* Rebuffer all file domains from disk,
By reusing domain keys so, we don't buffer new domains. This is ok since we would need to load SSL */
final void rebuffer() {
foreach(ref d; domains.byKey){ foreach(ref f; domains[d].files.byKey){
domains[d].files[f].buffer();
} }
}
final void rebuffer() { synchronized {
foreach(ref d; domains.byValue) { foreach(ref f; d.files.byValue) { f.buffer(); } }
} }
}

/* Basic unit-tests should be extended */
Expand Down
4 changes: 2 additions & 2 deletions danode/functions.d
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,10 @@ string browseDir(in string root, in string localpath) {
}

// Reset the socketset and add a server socket to the set
int sISelect(SocketSet set, Socket socket, int timeout = 10) {
int sISelect(SocketSet set, Socket socket, bool write = false, int timeout = 25) {
set.reset();
set.add(socket);
return Socket.select(set, null, null, dur!"msecs"(timeout));
return(write ? Socket.select(null, set, null, dur!"msecs"(timeout)) : Socket.select(set, null, null, dur!"msecs"(timeout)));
}

unittest {
Expand Down
6 changes: 3 additions & 3 deletions danode/http.d
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
module danode.http;

import danode.imports;

import danode.functions : sISelect;
import danode.interfaces : DriverInterface;
import danode.response : Response;
import danode.log : log, tag, error, Level;
Expand All @@ -28,9 +30,7 @@ class HTTP : DriverInterface {
override void send(ref Response response, Socket socket, ptrdiff_t maxsize = 4096) {
if (!socketReady()) return;
// Wait until socket is writable before sending
SocketSet writeSet = new SocketSet();
writeSet.add(socket);
if (Socket.select(null, writeSet, null, dur!"msecs"(0)) <= 0) return;
if (socketSet.sISelect(socket, true, 0) <= 0) return;
ptrdiff_t send = socket.send(response.bytes(maxsize));
if (send > 0) {
log(Level.Trace, "Send result=%d index=%d length=%d", send, response.index, response.length);
Expand Down
2 changes: 2 additions & 0 deletions danode/https.d
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ version(SSL) {
import danode.imports;
import danode.includes;

import danode.functions : sISelect;
import danode.response : Response;
import danode.log : tag, log, error, Level;
import danode.interfaces : DriverInterface;
Expand Down Expand Up @@ -87,6 +88,7 @@ version(SSL) {
// Send upto maxsize bytes from the response to the client
override void send(ref Response response, Socket socket, ptrdiff_t maxsize = 4096){
if (!socketReady()) return;
if (socketSet.sISelect(socket, true, 0) <= 0) return;
// SSL requires retrying with exact same buffer on WANT_WRITE
if (pending.length == 0) pending = response.bytes(maxsize).dup;
if (pending.length == 0) return;
Expand Down
12 changes: 10 additions & 2 deletions danode/interfaces.d
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
module danode.interfaces;

import danode.imports;
import danode.functions : Msecs, bodystart, endofheader, fullheader;
import danode.functions : Msecs, sISelect, bodystart, endofheader, fullheader;
import danode.response : Response;
import danode.statuscode : StatusCode;
import danode.log : log, error, Level;
Expand All @@ -27,14 +27,21 @@ abstract class DriverInterface {
public:
Appender!(char[]) inbuffer; /// Input appender buffer
Socket socket; /// Client socket for reading and writing
SocketSet socketSet;
long requests = 0; /// Number of requests we handled
long[long] senddata; /// Size of data send per request
SysTime systime; /// Time in ms since this process came alive
SysTime modtime; /// Time in ms since this process was last modified
Address address; /// Private address field
bool blocking = false; /// Blocking communication ?

this(Socket socket, bool blocking = false) { this.socket = socket; this.blocking = blocking; systime = Clock.currTime(); touch(); }
this(Socket socket, bool blocking = false) {
this.socket = socket;
this.socketSet = new SocketSet();
this.blocking = blocking;
systime = Clock.currTime();
touch();
}
bool socketReady() const { if (socket !is null) { return socket.isAlive(); } return false; }; /// Is the connection alive ?
void touch() { modtime = Clock.currTime(); }
void closeSocket() {
Expand All @@ -46,6 +53,7 @@ abstract class DriverInterface {
// Receive upto maxsize of bytes from the client into the input buffer
ptrdiff_t receive(Socket socket, ptrdiff_t maxsize = 4096) {
if (!socketReady()) return(-1);
if (socketSet.sISelect(socket, false, 25) <= 0) return(0);
ptrdiff_t received;
char[] tmpbuffer = new char[](maxsize);
if ((received = receiveData(tmpbuffer)) > 0) { inbuffer.put(tmpbuffer[0 .. received]); touch(); }
Expand Down
10 changes: 3 additions & 7 deletions danode/log.d
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,9 @@ private void logTo(A...)(ref File fp, string tag, const string fmt, auto ref A a
synchronized(logM) { fp.writeln(format("[%s] %s", tag, format(fmt, args))); fp.flush(); }
}

void log(A...)(Level lvl, const string fmt, auto ref A args) {
if(atomicLoad(cv) >= lvl) stdout.logTo("LOG", fmt, args);
}
void tag(A...)(Level lvl, const string tag, const string fmt, auto ref A args) {
if(atomicLoad(cv) >= lvl) stdout.logTo(tag, fmt, args);
}

void log(A...)(Level lvl, const string fmt, auto ref A args) { tag(lvl, "LOG", fmt, args); }
void tag(A...)(Level lvl, const string tag, const string fmt, auto ref A args) { if(atomicLoad(cv) >= lvl) stdout.logTo(tag, fmt, args); }
void error(A...)(const string fmt, auto ref A args) { stderr.logTo("ERR", fmt, args); }
void abort(in string s, int exitcode = -1) { error(s); exit(exitcode); }
void expect(A...)(bool cond, string msg, auto ref A args) { if (!cond) abort(format(msg, args)); }

15 changes: 9 additions & 6 deletions danode/post.d
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,19 @@ struct PostItem {
final bool parsePost(ref Request request, ref Response response, in FileSystem filesystem) {
if (response.havepost || request.method != RequestMethod.POST) { return(response.havepost = true); }

long expectedlength = to!long(from(request.headers, "Content-Length", "0"));
long expectedlength;
try {
expectedlength = to!long(request.headers.from("Content-Length", "0"));
} catch (Exception e) {
return(response.setPayload(StatusCode.BadRequest, "400 - Bad Request\n", "text/plain"));
}
string content = request.body;
if (expectedlength == 0) {
log(Level.Trace, "Post: [T] Content-Length not specified (or 0), length: %s", content.length);
return(response.havepost = true); // When we don't receive any post data it is meaningless to scan for any content
} else if (expectedlength > MAX_REQUEST_SIZE) {
log(Level.Verbose, "Post: [W] Upload too large: %d bytes from %s", expectedlength, request.ip);
response.setPayload(StatusCode.PayloadTooLarge, "413 - Payload Too Large\n", "text/plain");
return(response.havepost = true);
return(response.setPayload(StatusCode.PayloadTooLarge, "413 - Payload Too Large\n", "text/plain"));
}
log(Level.Trace, "Post: [T] Received %s of %s", content.length, expectedlength);
if(content.length < expectedlength) return(false);
Expand All @@ -58,7 +62,7 @@ final bool parsePost(ref Request request, ref Response response, in FileSystem f
log(Level.Verbose, "XFORM: [T] # of items: %s", request.postinfo.length);
} else if (contenttype.indexOf(MPHEADER) >= 0) {
auto parts = split(contenttype, "boundary=");
if (parts.length < 2) return response.havepost = true;
if (parts.length < 2) return(response.havepost = true);
string mpid = parts[1];
log(Level.Verbose, "MPART: [I] header: %s, parsing %d bytes", mpid, expectedlength);
request.parseMultipart(filesystem, content, mpid);
Expand All @@ -70,8 +74,7 @@ final bool parsePost(ref Request request, ref Response response, in FileSystem f
error("parsePost: Unsupported POST content type: %s [%s] -> %s", contenttype, expectedlength, content);
request.parseXform(content);
}
response.havepost = true;
return(response.havepost);
return(response.havepost = true);
}

// Parse X-form content in the body of the request
Expand Down
5 changes: 4 additions & 1 deletion danode/process.d
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,10 @@ class Process : Thread {
unittest {
tag(Level.Always, "FILE", "%s", __FILE__);

auto p = new Process(["rdmd", "www/localhost/sse.d"], "/dev/null", null, false);
string nulldev = "/dev/null";
version(Windows) nulldev = "NUL";

auto p = new Process(["rdmd", "www/localhost/sse.d"], nulldev, null, false);
p.start();
while(!p.finished){ Thread.sleep(msecs(5)); }
assert(p.status() == 0, "process must exit 0");
Expand Down
17 changes: 12 additions & 5 deletions danode/request.d
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,11 @@ struct Request {
string r = headers.from("Range");
if (r.length == 0 || !r.startsWith("bytes=")) return [-1, -1];
string[] parts = r[6 .. $].split("-");
long start = parts[0].length > 0 ? to!long(parts[0]) : 0;
long end = (parts.length > 1 && parts[1].length > 0) ? to!long(parts[1]) : -1;
return [start, end];
try {
long start = parts[0].length > 0 ? to!long(parts[0]) : 0;
long end = (parts.length > 1 && parts[1].length > 0) ? to!long(parts[1]) : -1;
return [start, end];
} catch (Exception e) { return [-1, -1]; }
}

final @property @nogc bool hasRange() const nothrow { return headers.from("Range").startsWith("bytes="); }
Expand Down Expand Up @@ -165,7 +167,7 @@ struct Request {
}

final string[string] environ(string localpath) const {
string[string] env = environment.toAA();
string[string] env;
env["REQUEST_METHOD"] = to!string(method);
env["QUERY_STRING"] = query.length > 1 ? query[1 .. $] : "";
env["REQUEST_URI"] = decodeComponent(uripath);
Expand All @@ -177,7 +179,8 @@ struct Request {
env["HTTP_HOST"] = host;
env["HTTPS"] = isSecure ? "on" : "";
env["REDIRECT_STATUS"] = "200";
foreach (k, v; headers) env["HTTP_" ~ k.toUpper().replace("-", "_")] = v;
env["PATH"] = environment.get("PATH", "");
foreach (k, v; headers) { env["HTTP_" ~ k.toUpper().replace("-", "_")] = v; }
return env;
}

Expand Down Expand Up @@ -262,4 +265,8 @@ unittest {
r10.headers["Accept-Encoding"] = "gzip, deflate";
assert(r10.acceptsEncoding("deflate"), "deflate must be accepted");
assert(!r10.acceptsEncoding("br"), "br must not be accepted");

Request r11;
r11.headers["Range"] = "bytes=abc-def";
assert(r11.range() == [-1, -1], "malformed range must return [-1, -1]");
}
6 changes: 4 additions & 2 deletions danode/response.d
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ bool buildScriptHeader(ref Appender!(char[]) hdr, ref string connection, CGI scr
hdr.put(format("%s %d %s\r\n", protocol, script.statuscode, script.statuscode.reason));
}
foreach (line; scriptheader.split("\n")) {
if (line.length > 0 && icmp(strip(line).split(":")[0], "connection") != 0){ hdr.put(line ~ "\n"); }
auto stripped = strip(line);
auto parts = stripped.split(":");
if (stripped.length > 0 && parts.length > 0 && icmp(parts[0], "connection") != 0) { hdr.put(line ~ "\n"); }
}
hdr.put(format("Connection: %s\r\n\r\n", connection));
return true;
Expand All @@ -136,7 +138,7 @@ Response create(in Request request, Address address, in StatusCode statuscode =

bool setPayload(ref Response response, StatusCode code, string msg = "", in string mimetype = UNSUPPORTED_FILE) {
response.payload = new Message(code, msg, mimetype);
return(response.ready = true);
return(response.ready = response.havepost = true);
}

// send a redirect permanently response
Expand Down
Loading