tildefriends/core/httpd.js

614 lines
16 KiB
JavaScript

import * as core from './core.js';
let gHandlers = [];
let gSocketHandlers = [];
let gBadRequests = {};
const kRequestTimeout = 15000;
const kStallTimeout = 60000;
function logError(error) {
print("ERROR " + error);
if (error.stackTrace) {
print(error.stackTrace);
}
}
function addHandler(handler) {
let added = false;
for (let 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;
let 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) {
let matchedHandler = null;
for (let name in gHandlers) {
let 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) {
let matchedHandler = null;
for (let name in gSocketHandlers) {
let 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) {
let kStatusText = {
101: "Switching Protocols",
200: 'OK',
303: 'See other',
304: 'Not Modified',
400: 'Bad Request',
401: 'Unauthorized',
403: 'Forbidden',
404: 'File not found',
500: 'Internal server error',
};
let _started = false;
let _finished = false;
let _keepAlive = false;
let _chunked = false;
return {
writeHead: function(status) {
if (_started) {
throw new Error("Response.writeHead called multiple times.");
}
let reason;
let headers;
if (arguments.length == 3) {
reason = arguments[1];
headers = arguments[2];
} else {
reason = kStatusText[status];
headers = arguments[1];
}
let lowerHeaders = {};
let requestVersion = request.version.split("/")[1].split(".");
let responseVersion = (requestVersion[0] >= 1 && requestVersion[0] >= 1) ? "1.1" : "1.0";
let headerString = "HTTP/" + responseVersion + " " + status + " " + reason + "\r\n";
for (let 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) {
let handler = findHandler(request);
print(request.client.peerName + " - - [" + new Date() + "] " + request.method + " " + request.uri + " " + request.version + " \"" + request.headers["user-agent"] + "\"");
if (handler) {
try {
Promise.resolve(handler.invoke(request, response)).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) {
let buffer = new Uint8Array(0);
let frame;
let frameOpCode = 0x0;
let 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);
}
let fin = true;
let packet = [(fin ? (1 << 7) : 0) | (opCode & 0xf)];
let 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 {
let high = 0; //(message.length / (1 ** 32)) & 0xffffffff;
let 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);
}
let array = new Uint8Array(packet.length + message.length);
array.set(packet, 0);
array.set(message, packet.length);
try {
return client.write(array);
} catch (error) {
client.close();
throw error;
}
}
response.onMessage = null;
let extra_headers = handler.invoke(request, response);
client.read(function(data) {
if (data) {
let newBuffer = new Uint8Array(buffer.length + data.length);
newBuffer.set(buffer, 0);
newBuffer.set(data, buffer.length);
buffer = newBuffer;
while (buffer.length >= 2) {
let bits0 = buffer[0];
let bits1 = buffer[1];
if (bits1 & (1 << 7) == 0) {
// Unmasked message.
client.close();
}
let opCode = bits0 & 0xf;
let fin = bits0 & (1 << 7);
let payloadLength = bits1 & 0x7f;
let maskStart = 2;
if (payloadLength == 126) {
payloadLength = 0;
for (let i = 0; i < 2; i++) {
payloadLength <<= 8;
payloadLength |= buffer[2 + i];
}
maskStart = 4;
} else if (payloadLength == 127) {
payloadLength = 0;
for (let i = 0; i < 8; i++) {
payloadLength <<= 8;
payloadLength |= buffer[2 + i];
}
maskStart = 10;
}
let havePayload = buffer.length >= payloadLength + 2 + 4;
if (havePayload) {
let mask =
buffer[maskStart + 0] |
buffer[maskStart + 1] << 8 |
buffer[maskStart + 2] << 16 |
buffer[maskStart + 3] << 24;
let dataStart = maskStart + 4;
let payload = buffer.slice(dataStart, dataStart + payloadLength);
let decoded = maskBytes(payload, mask);
buffer = buffer.slice(dataStart + payloadLength);
if (frame) {
let newBuffer = new Uint8Array(frame.length + decoded.length);
newBuffer.set(frame, 0);
newBuffer.set(decoded, frame.length);
frame = newBuffer;
} else {
frame = decoded;
}
if (opCode) {
frameOpCode = opCode;
}
if (fin) {
if (response.onMessage) {
response.onMessage({
data: frameOpCode == 0x1 ? utf8Decode(frame) : frame,
opCode: frameOpCode,
});
}
frame = undefined;
}
} 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, Object.assign({}, headers, extra_headers));
}
function webSocketAcceptResponse(key) {
let kMagic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
let kAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
return base64Encode(sha1Digest(key + kMagic));
}
function badRequest(client, reason) {
let now = new Date();
let count = 0;
let old = gBadRequests[client.peerName];
if (!old) {
gBadRequests[client.peerName] = {
expire: new Date(now.getTime() + 1 * 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) {
let old = gBadRequests[client.peerName];
if (old) {
let 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';
let inputBuffer = new Uint8Array(0);
let request;
let headers = {};
let parsing_header = true;
let bodyToRead = -1;
let body;
let requestCount = -1;
let readCount = 0;
let 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 {
let 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() {
request = undefined;
headers = {};
parsing_header = true;
bodyToRead = -1;
body = undefined;
client.info = 'reset';
resetTimeout(++requestCount);
}
function finish() {
client.info = 'finishing';
let requestObject = new Request(request[0], request[1], request[2], headers, body, client);
let response = new Response(requestObject, client);
try {
handleRequest(requestObject, response)
if (client.isConnected) {
reset();
}
} catch (error) {
response.reportError(error);
client.close();
}
}
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);
}
let newBuffer = new Uint8Array(inputBuffer.length + data.length);
newBuffer.set(inputBuffer, 0);
newBuffer.set(data, inputBuffer.length);
inputBuffer = newBuffer;
if (parsing_header)
{
let result = parseHttp(inputBuffer, inputBuffer.length - data.length);
if (result) {
if (typeof result === 'number') {
if (result == -2) {
/* More. */
} else {
badRequest(client, 'Bad request.');
return;
}
} else if (typeof result === 'object') {
request = [
result.method,
result.path,
`HTTP/1.${result.minor_version}`,
];
headers = Object.fromEntries(Object.entries(result.headers).map(x => [x[0].toLowerCase(), x[1]]));
parsing_header = false;
inputBuffer = inputBuffer.slice(result.bytes_parsed);
if (!client.tls && tildefriends.https_port && core.globalSettings.http_redirect) {
let requestObject = new Request(request[0], request[1], request[2], headers, body, client);
let response = new Response(requestObject, client);
response.writeHead(303, {"Location": `${core.globalSettings.http_redirect}${result.path}`, "Content-Length": "0"});
response.end();
return;
}
if (headers["content-length"] != undefined) {
bodyToRead = parseInt(headers["content-length"]);
if (bodyToRead > 16 * 1024 * 1024) {
badRequest(client, 'Request too large: ' + bodyToRead + '.');
return;
}
body = new Uint8Array(bodyToRead);
client.info = 'waiting for body';
resetTimeout(requestCount);
} 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';
let requestObject = new Request(request[0], request[1], request[2], headers, body, client);
let response = new Response(requestObject, client);
handleWebSocketRequest(requestObject, response, client);
/* Prevent the timeout from disconnecting us. */
requestCount++;
} else {
finish();
}
}
}
}
if (!parsing_header && inputBuffer.length)
{
let offset = body.length - bodyToRead;
let length = Math.min(inputBuffer.length, body.length - offset);
if (inputBuffer.length > body.length - offset) {
body.set(inputBuffer.slice(0, length), offset);
inputBuffer = inputBuffer.slice(length);
} else {
body.set(inputBuffer, offset);
inputBuffer = inputBuffer.slice(inputBuffer.length);
}
bodyToRead -= length;
if (bodyToRead <= 0) {
finish();
}
}
} else {
client.info = 'EOF';
client.close();
}
});
}
let kBacklog = 8;
let kHost = "0.0.0.0"
let socket = new Socket();
socket.bind(kHost, tildefriends.http_port).then(function() {
let 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) {
let tls = {};
let secureSocket = new Socket();
secureSocket.bind(kHost, tildefriends.https_port).then(function() {
return secureSocket.listen(kBacklog, async function() {
try {
let client = await secureSocket.accept();
client.tls = true;
const kCertificatePath = "data/httpd/certificate.pem";
const kPrivateKeyPath = "data/httpd/privatekey.pem";
let 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);
let privateKey = utf8Decode(await File.readFile(kPrivateKeyPath));
let 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);
});
}
export { all, registerSocketHandler };