Cory McWilliams
b5b6ed8ba5
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@3900 ed5197a5-7fde-0310-b194-c3ffbd925b24
638 lines
17 KiB
JavaScript
638 lines
17 KiB
JavaScript
"use strict";
|
|
|
|
var gHandlers = [];
|
|
var gSocketHandlers = [];
|
|
var gBadRequests = {};
|
|
|
|
const kRequestTimeout = 15000;
|
|
const kStallTimeout = 60000;
|
|
|
|
function logError(error) {
|
|
print("ERROR " + error);
|
|
if (error.stackTrace) {
|
|
print(error.stackTrace);
|
|
}
|
|
}
|
|
|
|
function addHandler(handler) {
|
|
var added = false;
|
|
for (var i in gHandlers) {
|
|
if (gHandlers[i].path == handler.path) {
|
|
gHandlers[i] = handler;
|
|
added = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!added) {
|
|
gHandlers.push(handler);
|
|
added = true;
|
|
}
|
|
}
|
|
|
|
function all(prefix, handler) {
|
|
addHandler({
|
|
owner: this,
|
|
path: prefix,
|
|
invoke: handler,
|
|
});
|
|
}
|
|
|
|
function registerSocketHandler(prefix, handler) {
|
|
gSocketHandlers.push({
|
|
owner: this,
|
|
path: prefix,
|
|
invoke: handler,
|
|
});
|
|
}
|
|
|
|
function Request(method, uri, version, headers, body, client) {
|
|
this.method = method;
|
|
var index = uri.indexOf("?");
|
|
if (index != -1) {
|
|
this.uri = uri.slice(0, index);
|
|
this.query = uri.slice(index + 1);
|
|
} else {
|
|
this.uri = uri;
|
|
this.query = undefined;
|
|
}
|
|
this.version = version || '';
|
|
this.headers = headers;
|
|
this.client = {peerName: client.peerName, tls: client.tls};
|
|
this.body = body;
|
|
return this;
|
|
}
|
|
|
|
function findHandler(request) {
|
|
var matchedHandler = null;
|
|
for (var name in gHandlers) {
|
|
var handler = gHandlers[name];
|
|
if (request.uri == handler.path || request.uri.slice(0, handler.path.length + 1) == handler.path + '/') {
|
|
matchedHandler = handler;
|
|
break;
|
|
}
|
|
}
|
|
return matchedHandler;
|
|
}
|
|
|
|
function findSocketHandler(request) {
|
|
var matchedHandler = null;
|
|
for (var name in gSocketHandlers) {
|
|
var handler = gSocketHandlers[name];
|
|
if (request.uri == handler.path || request.uri.slice(0, handler.path.length + 1) == handler.path + '/') {
|
|
matchedHandler = handler;
|
|
break;
|
|
}
|
|
}
|
|
return matchedHandler;
|
|
}
|
|
|
|
function Response(request, client) {
|
|
var kStatusText = {
|
|
101: "Switching Protocols",
|
|
200: 'OK',
|
|
303: 'See other',
|
|
403: 'Forbidden',
|
|
404: 'File not found',
|
|
500: 'Internal server error',
|
|
};
|
|
var _started = false;
|
|
var _finished = false;
|
|
var _keepAlive = false;
|
|
var _chunked = false;
|
|
return {
|
|
writeHead: function(status) {
|
|
if (_started) {
|
|
throw new Error("Response.writeHead called multiple times.");
|
|
}
|
|
var reason;
|
|
var headers;
|
|
if (arguments.length == 3) {
|
|
reason = arguments[1];
|
|
headers = arguments[2];
|
|
} else {
|
|
reason = kStatusText[status];
|
|
headers = arguments[1];
|
|
}
|
|
var lowerHeaders = {};
|
|
var requestVersion = request.version.split("/")[1].split(".");
|
|
var responseVersion = (requestVersion[0] >= 1 && requestVersion[0] >= 1) ? "1.1" : "1.0";
|
|
var headerString = "HTTP/" + responseVersion + " " + status + " " + reason + "\r\n";
|
|
for (var i in headers) {
|
|
headerString += i + ": " + headers[i] + "\r\n";
|
|
lowerHeaders[i.toLowerCase()] = headers[i];
|
|
}
|
|
if ("connection" in lowerHeaders) {
|
|
_keepAlive = lowerHeaders["connection"].toLowerCase() == "keep-alive";
|
|
} else {
|
|
_keepAlive = ((request.version == "HTTP/1.0" && ("connection" in lowerHeaders && lowerHeaders["connection"].toLowerCase() == "keep-alive")) ||
|
|
(request.version == "HTTP/1.1" && (!("connection" in lowerHeaders) || lowerHeaders["connection"].toLowerCase() != "close")));
|
|
headerString += "Connection: " + (_keepAlive ? "keep-alive" : "close") + "\r\n";
|
|
}
|
|
_chunked = _keepAlive && !("content-length" in lowerHeaders);
|
|
if (_chunked) {
|
|
headerString += "Transfer-Encoding: chunked\r\n";
|
|
}
|
|
headerString += "\r\n";
|
|
_started = true;
|
|
client.write(headerString).catch(function() {});
|
|
},
|
|
end: function(data) {
|
|
if (_finished) {
|
|
throw new Error("Response.end called multiple times.");
|
|
}
|
|
if (data) {
|
|
if (_chunked) {
|
|
client.write(data.length.toString(16) + "\r\n" + data + "\r\n" + "0\r\n\r\n").catch(function() {});
|
|
} else {
|
|
client.write(data).catch(function() {});
|
|
}
|
|
} else if (_chunked) {
|
|
client.write("0\r\n\r\n").catch(function() {});
|
|
}
|
|
_finished = true;
|
|
if (!_keepAlive) {
|
|
client.shutdown().catch(function() {});
|
|
}
|
|
},
|
|
reportError: function(error) {
|
|
if (!_started) {
|
|
client.write("HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n").catch(function() {});
|
|
}
|
|
if (!_finished) {
|
|
client.write("500 Internal Server Error\r\n\r\n" + error?.stackTrace).catch(function() {});
|
|
}
|
|
logError(client.peerName + " - - [" + new Date() + "] " + error);
|
|
},
|
|
isConnected: function() { return client.isConnected; },
|
|
};
|
|
}
|
|
|
|
function handleRequest(request, response) {
|
|
var handler = findHandler(request);
|
|
|
|
print(request.client.peerName + " - - [" + new Date() + "] " + request.method + " " + request.uri + " " + request.version + " \"" + request.headers["user-agent"] + "\"");
|
|
|
|
if (handler) {
|
|
try {
|
|
var promise = handler.invoke(request, response);
|
|
if (promise) {
|
|
promise.catch(function(error) {
|
|
response.reportError(error);
|
|
request.client.close();
|
|
});
|
|
}
|
|
} catch (error) {
|
|
response.reportError(error);
|
|
request.client.close();
|
|
}
|
|
} else {
|
|
response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8"});
|
|
response.end("No handler found for request: " + request.uri);
|
|
}
|
|
}
|
|
|
|
function handleWebSocketRequest(request, response, client) {
|
|
var buffer = new Uint8Array(0);
|
|
var frame = new Uint8Array(0);
|
|
var frameOpCode = 0x0;
|
|
|
|
var handler = findSocketHandler(request);
|
|
if (!handler) {
|
|
client.close();
|
|
return;
|
|
}
|
|
|
|
response.send = function(message, opCode) {
|
|
if (opCode === undefined) {
|
|
opCode = 0x2;
|
|
}
|
|
if (opCode == 0x1 && (typeof message == "string" || message instanceof String)) {
|
|
message = utf8Encode(message);
|
|
}
|
|
var fin = true;
|
|
var packet = [(fin ? (1 << 7) : 0) | (opCode & 0xf)];
|
|
var mask = false;
|
|
if (message.length < 126) {
|
|
packet.push((mask ? (1 << 7) : 0) | message.length);
|
|
} else if (message.length < (1 << 16)) {
|
|
packet.push((mask ? (1 << 7) : 0) | 126);
|
|
packet.push((message.length >> 8) & 0xff);
|
|
packet.push(message.length & 0xff);
|
|
} else {
|
|
var high = 0; //(message.length / (1 ** 32)) & 0xffffffff;
|
|
var low = message.length & 0xffffffff;
|
|
packet.push((mask ? (1 << 7) : 0) | 127);
|
|
packet.push((high >> 24) & 0xff);
|
|
packet.push((high >> 16) & 0xff);
|
|
packet.push((high >> 8) & 0xff);
|
|
packet.push((high >> 0) & 0xff);
|
|
packet.push((low >> 24) & 0xff);
|
|
packet.push((low >> 16) & 0xff);
|
|
packet.push((low >> 8) & 0xff);
|
|
packet.push(low & 0xff);
|
|
}
|
|
|
|
var array = new Uint8Array(packet.length + message.length);
|
|
array.set(packet, 0);
|
|
array.set(message, packet.length);
|
|
return client.write(array);
|
|
}
|
|
response.onMessage = null;
|
|
|
|
handler.invoke(request, response);
|
|
|
|
client.read(function(data) {
|
|
if (data) {
|
|
var newBuffer = new Uint8Array(buffer.length + data.length);
|
|
newBuffer.set(buffer, 0);
|
|
newBuffer.set(data, buffer.length);
|
|
buffer = newBuffer;
|
|
|
|
while (buffer.length >= 2) {
|
|
var bits0 = buffer[0];
|
|
var bits1 = buffer[1];
|
|
if (bits1 & (1 << 7) == 0) {
|
|
// Unmasked message.
|
|
client.close();
|
|
}
|
|
var opCode = bits0 & 0xf;
|
|
var fin = bits0 & (1 << 7);
|
|
var payloadLength = bits1 & 0x7f;
|
|
var maskStart = 2;
|
|
|
|
if (payloadLength == 126) {
|
|
payloadLength = 0;
|
|
for (var i = 0; i < 2; i++) {
|
|
payloadLength <<= 8;
|
|
payloadLength |= buffer[2 + i];
|
|
}
|
|
maskStart = 4;
|
|
} else if (payloadLength == 127) {
|
|
payloadLength = 0;
|
|
for (var i = 0; i < 8; i++) {
|
|
payloadLength <<= 8;
|
|
payloadLength |= buffer[2 + i];
|
|
}
|
|
maskStart = 10;
|
|
}
|
|
var havePayload = buffer.length >= payloadLength + 2 + 4;
|
|
if (havePayload) {
|
|
var mask = buffer.slice(maskStart, maskStart + 4);
|
|
var dataStart = maskStart + 4;
|
|
var decoded = new Array(payloadLength);
|
|
var payload = buffer.slice(dataStart, dataStart + payloadLength);
|
|
buffer = buffer.slice(dataStart + payloadLength);
|
|
for (var i = 0; i < payloadLength; i++) {
|
|
decoded[i] = payload[i] ^ mask[i % 4];
|
|
}
|
|
|
|
var newBuffer = new Uint8Array(frame.length + decoded.length);
|
|
newBuffer.set(frame, 0);
|
|
newBuffer.set(decoded, frame.length);
|
|
frame = newBuffer;
|
|
|
|
if (opCode) {
|
|
frameOpCode = opCode;
|
|
}
|
|
|
|
if (fin) {
|
|
if (response.onMessage) {
|
|
response.onMessage({
|
|
data: frameOpCode == 0x1 ? utf8Decode(frame) : frame,
|
|
opCode: frameOpCode,
|
|
});
|
|
}
|
|
frame = new Uint8Array(0);
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
response.onClose();
|
|
client.close();
|
|
}
|
|
});
|
|
client.onError(function(error) {
|
|
logError(client.peerName + " - - [" + new Date() + "] " + error);
|
|
response.onError(error);
|
|
});
|
|
|
|
let headers = {
|
|
"Upgrade": "websocket",
|
|
"Connection": "Upgrade",
|
|
"Sec-WebSocket-Accept": webSocketAcceptResponse(request.headers["sec-websocket-key"]),
|
|
};
|
|
if (request.headers["sec-websocket-version"] != "13") {
|
|
headers["Sec-WebSocket-Version"] = "13";
|
|
}
|
|
response.writeHead(101, headers);
|
|
}
|
|
|
|
function webSocketAcceptResponse(key) {
|
|
var kMagic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
var kAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
|
var hex = require("sha1").hash(key + kMagic)
|
|
var binary = "";
|
|
for (var i = 0; i < hex.length; i += 6) {
|
|
var characters = hex.substring(i, i + 6);
|
|
if (characters.length < 6) {
|
|
characters += "0".repeat(6 - characters.length);
|
|
}
|
|
var value = parseInt(characters, 16);
|
|
for (var bit = 0; bit < 8 * 3; bit += 6) {
|
|
if (i * 8 / 2 + bit >= 8 * hex.length / 2) {
|
|
binary += kAlphabet.charAt(64);
|
|
} else {
|
|
binary += kAlphabet.charAt((value >> (18 - bit)) & 63);
|
|
}
|
|
}
|
|
}
|
|
return binary;
|
|
}
|
|
|
|
function badRequest(client, reason) {
|
|
var now = new Date();
|
|
var count = 0;
|
|
var old = gBadRequests[client.peerName];
|
|
if (!old) {
|
|
gBadRequests[client.peerName] = {
|
|
expire: new Date(now.getTime() + 10 * 60 * 1000),
|
|
count: 1,
|
|
reason: reason,
|
|
};
|
|
count = 1;
|
|
} else {
|
|
old.count++;
|
|
old.reason = reason;
|
|
count = old.count;
|
|
}
|
|
new Response({version: '1.0'}, client).reportError(reason + ': ' + count);
|
|
client.close();
|
|
}
|
|
|
|
function allowRequest(client) {
|
|
var old = gBadRequests[client.peerName];
|
|
if (old) {
|
|
var now = new Date();
|
|
if (old.expire < now) {
|
|
delete gBadRequests[client.peerName];
|
|
return true;
|
|
} else {
|
|
return old.count < 3;
|
|
}
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function handleConnection(client) {
|
|
if (!allowRequest(client)) {
|
|
print('Rejecting client for too many bad requests: ', client.peerName, gBadRequests[client.peerName].reason);
|
|
client.info = 'rejected';
|
|
client.close();
|
|
return;
|
|
}
|
|
|
|
client.info = 'accepted';
|
|
var inputBuffer = new Uint8Array(0);
|
|
var request;
|
|
var headers = {};
|
|
var lineByLine = true;
|
|
var bodyToRead = -1;
|
|
var body;
|
|
var requestCount = -1;
|
|
var readCount = 0;
|
|
var isWebsocket = false;
|
|
|
|
function resetTimeout(requestIndex) {
|
|
if (isWebsocket) {
|
|
return;
|
|
}
|
|
if (bodyToRead == -1) {
|
|
setTimeout(function() {
|
|
if (requestCount == requestIndex) {
|
|
client.info = 'timed out';
|
|
if (requestCount == 0) {
|
|
badRequest(client, 'Timed out waiting for request.');
|
|
} else {
|
|
client.close();
|
|
}
|
|
}
|
|
}, kRequestTimeout);
|
|
} else {
|
|
var lastReadCount = readCount;
|
|
setTimeout(function() {
|
|
if (readCount == lastReadCount) {
|
|
client.info = 'stalled';
|
|
if (requestCount == 0) {
|
|
badRequest(client, 'Request stalled.');
|
|
} else {
|
|
client.close();
|
|
}
|
|
}
|
|
}, kStallTimeout);
|
|
}
|
|
}
|
|
|
|
resetTimeout(++requestCount);
|
|
|
|
function reset() {
|
|
inputBuffer = new Uint8Array(0);
|
|
request = undefined;
|
|
headers = {};
|
|
lineByLine = true;
|
|
bodyToRead = -1;
|
|
body = undefined;
|
|
client.info = 'reset';
|
|
resetTimeout(++requestCount);
|
|
}
|
|
|
|
function finish() {
|
|
client.info = 'finishing';
|
|
var requestObject = new Request(request[0], request[1], request[2], headers, body, client);
|
|
var response = new Response(requestObject, client);
|
|
try {
|
|
handleRequest(requestObject, response)
|
|
if (client.isConnected) {
|
|
reset();
|
|
}
|
|
} catch (error) {
|
|
response.reportError(error);
|
|
client.close();
|
|
}
|
|
}
|
|
|
|
function handleLine(line, length) {
|
|
if (bodyToRead == -1) {
|
|
line = utf8Decode(line);
|
|
if (!request) {
|
|
if (!line) {
|
|
badRequest(client, 'Empty request.');
|
|
return false;
|
|
}
|
|
request = line.split(' ');
|
|
if (request.length != 3 || !request[2].startsWith('HTTP/1.')) {
|
|
badRequest(client, 'Bad request.');
|
|
request = null;
|
|
return false;
|
|
}
|
|
return true;
|
|
} else if (line) {
|
|
var colon = line.indexOf(':');
|
|
var key = line.slice(0, colon).trim();
|
|
var value = line.slice(colon + 1).trim();
|
|
headers[key.toLowerCase()] = value;
|
|
return true;
|
|
} else {
|
|
if (headers["content-length"] != undefined) {
|
|
bodyToRead = parseInt(headers["content-length"]);
|
|
lineByLine = false;
|
|
if (bodyToRead > 16 * 1024 * 1024) {
|
|
badRequest(client, 'Request too large: ' + bodyToRead + '.');
|
|
return false;
|
|
}
|
|
body = new Uint8Array(bodyToRead);
|
|
client.info = 'waiting for body';
|
|
return true;
|
|
} else if (headers["connection"]
|
|
&& headers["connection"].toLowerCase().split(",").map(x => x.trim()).indexOf("upgrade") != -1
|
|
&& headers["upgrade"]
|
|
&& headers["upgrade"].toLowerCase() == "websocket") {
|
|
isWebsocket = true;
|
|
client.info = 'websocket';
|
|
var requestObject = new Request(request[0], request[1], request[2], headers, body, client);
|
|
var response = new Response(requestObject, client);
|
|
handleWebSocketRequest(requestObject, response, client);
|
|
/* Prevent the timeout from disconnecting us. */
|
|
requestCount++;
|
|
return false;
|
|
} else {
|
|
finish();
|
|
return false;
|
|
}
|
|
}
|
|
} else {
|
|
var offset = body.length - bodyToRead;
|
|
if (line.length > body.length - offset) {
|
|
line = line.slice(0, body.length - offset);
|
|
}
|
|
body.set(line, offset);
|
|
bodyToRead -= line.length;
|
|
if (bodyToRead <= 0) {
|
|
finish();
|
|
}
|
|
}
|
|
}
|
|
|
|
client.noDelay = true;
|
|
|
|
client.onError(function(error) {
|
|
logError(client.peerName + " - - [" + new Date() + "] " + error);
|
|
});
|
|
|
|
client.read(function(data) {
|
|
readCount++;
|
|
if (data) {
|
|
if (bodyToRead != -1 && !isWebsocket) {
|
|
resetTimeout(requestCount);
|
|
}
|
|
const kMaxLineLength = 4096;
|
|
var newBuffer = new Uint8Array(inputBuffer.length + data.length);
|
|
newBuffer.set(inputBuffer, 0);
|
|
newBuffer.set(data, inputBuffer.length);
|
|
inputBuffer = newBuffer;
|
|
|
|
var newLine = '\n'.charCodeAt(0);
|
|
var carriageReturn = '\r'.charCodeAt(0);
|
|
|
|
var more = true;
|
|
while (more) {
|
|
if (lineByLine) {
|
|
more = false;
|
|
var end = inputBuffer.indexOf(newLine);
|
|
var realEnd = end;
|
|
if (end > 0 && inputBuffer[end - 1] == carriageReturn) {
|
|
--end;
|
|
}
|
|
if (end > kMaxLineLength || end == -1 && inputBuffer.length > kMaxLineLength) {
|
|
badRequest(client, 'Request too long.');
|
|
return;
|
|
}
|
|
if (end != -1) {
|
|
var line = inputBuffer.slice(0, end);
|
|
inputBuffer = inputBuffer.slice(realEnd + 1);
|
|
more = handleLine(line, realEnd + 1);
|
|
}
|
|
} else {
|
|
more = handleLine(inputBuffer, inputBuffer.length);
|
|
inputBuffer = new Uint8Array(0);
|
|
}
|
|
}
|
|
} else {
|
|
client.info = 'EOF';
|
|
client.close();
|
|
}
|
|
});
|
|
}
|
|
|
|
var kBacklog = 8;
|
|
var kHost = "0.0.0.0"
|
|
|
|
var socket = new Socket();
|
|
socket.bind(kHost, tildefriends.http_port).then(function() {
|
|
var listenResult = socket.listen(kBacklog, function() {
|
|
socket.accept().then(handleConnection).catch(function(error) {
|
|
logError("[" + new Date() + "] accept error " + error);
|
|
});
|
|
});
|
|
}).catch(function(error) {
|
|
logError("[" + new Date() + "] bind error " + error);
|
|
});
|
|
|
|
if (tildefriends.https_port) {
|
|
var tls = {};
|
|
var secureSocket = new Socket();
|
|
secureSocket.bind(kHost, tildefriends.https_port).then(function() {
|
|
return secureSocket.listen(kBacklog, async function() {
|
|
try {
|
|
var client = await secureSocket.accept();
|
|
client.tls = true;
|
|
const kCertificatePath = "data/httpd/certificate.pem";
|
|
const kPrivateKeyPath = "data/httpd/privatekey.pem";
|
|
|
|
var stat = await Promise.all([
|
|
await File.stat(kCertificatePath),
|
|
await File.stat(kPrivateKeyPath),
|
|
]);
|
|
if (!tls.context ||
|
|
tls.certStat.mtime != stat[0].mtime ||
|
|
tls.certStat.size != stat[0].size ||
|
|
tls.keyStat.mtime != stat[1].mtime ||
|
|
tls.keyStat.size != stat[1].size) {
|
|
print("Reloading " + kCertificatePath + " and " + kPrivateKeyPath);
|
|
var privateKey = utf8Decode(await File.readFile(kPrivateKeyPath));
|
|
var certificate = utf8Decode(await File.readFile(kCertificatePath));
|
|
|
|
tls.context = new TlsContext();
|
|
tls.context.setPrivateKey(privateKey);
|
|
tls.context.setCertificate(certificate);
|
|
tls.certStat = stat[0];
|
|
tls.keyStat = stat[1];
|
|
}
|
|
|
|
let result = client.startTls(tls.context);
|
|
handleConnection(client);
|
|
return result;
|
|
} catch (error) {
|
|
logError("[" + new Date() + "] [" + client.peerName + "] " + error);
|
|
}
|
|
});
|
|
}).catch(function(error) {
|
|
logError("[" + new Date() + "] bind error " + error);
|
|
});
|
|
}
|
|
|
|
exports.all = all;
|
|
exports.registerSocketHandler = registerSocketHandler;
|