Cory McWilliams
f9940fc436
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4685 ed5197a5-7fde-0310-b194-c3ffbd925b24
597 lines
16 KiB
JavaScript
597 lines
16 KiB
JavaScript
import * as core from './core.js';
|
|
|
|
let gHandlers = [];
|
|
let gSocketHandlers = [];
|
|
let gBadRequests = {};
|
|
|
|
const kRequestTimeout = 5000;
|
|
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";
|
|
headers['Server'] = `Tilde Friends/${version().number}`;
|
|
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 readCount = 0;
|
|
let isWebsocket = false;
|
|
|
|
client.setActivityTimeout(kRequestTimeout);
|
|
|
|
function reset() {
|
|
request = undefined;
|
|
headers = {};
|
|
parsing_header = true;
|
|
bodyToRead = -1;
|
|
body = undefined;
|
|
client.info = 'reset';
|
|
client.setActivityTimeout(kRequestTimeout);
|
|
}
|
|
|
|
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.onError(function(error) {
|
|
logError(client.peerName + " - - [" + new Date() + "] " + error);
|
|
});
|
|
|
|
client.read(function(data) {
|
|
readCount++;
|
|
if (data) {
|
|
let newBuffer = new Uint8Array(inputBuffer.length + data.length);
|
|
newBuffer.set(inputBuffer, 0);
|
|
newBuffer.set(data, inputBuffer.length);
|
|
inputBuffer = newBuffer;
|
|
|
|
if (parsing_header)
|
|
{
|
|
let result = parseHttpRequest(inputBuffer, inputBuffer.length - data.length);
|
|
if (result) {
|
|
if (typeof result === 'number') {
|
|
if (result == -2) {
|
|
/* More. */
|
|
} else {
|
|
badRequest(client, `Bad request(parse=${result}, length=${inputBuffer.length - data.length}).`);
|
|
return;
|
|
}
|
|
} else if (typeof result === 'object') {
|
|
client.setActivityTimeout(kStallTimeout);
|
|
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 && !result.path.startsWith('/.well-known/')) {
|
|
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';
|
|
} 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. */
|
|
client.setActivityTimeout();
|
|
} 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 = platform() == 'haiku' ? 'localhost' : '::';
|
|
|
|
function start() {
|
|
print('ACTUAL START');
|
|
let socket = new Socket();
|
|
socket.bind(kHost, tildefriends.http_port).then(function(port) {
|
|
print("bound to", port);
|
|
print("checking", tildefriends.args.out_http_port_file);
|
|
if (tildefriends.args.out_http_port_file) {
|
|
print("going to write the file");
|
|
File.writeFile(tildefriends.args.out_http_port_file, port.toString() + '\n').then(function(r) {
|
|
print("wrote port file", tildefriends.args.out_http_port_file, r);
|
|
}).catch(function() {
|
|
print("failed to write port file");
|
|
});
|
|
}
|
|
let listenResult = socket.listen(kBacklog, async function() {
|
|
try {
|
|
let client = await socket.accept();
|
|
client.noDelay = true;
|
|
handleConnection(client);
|
|
} catch (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.noDelay = true;
|
|
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() + "] " + error);
|
|
}
|
|
});
|
|
}).catch(function(error) {
|
|
logError("[" + new Date() + "] bind error " + error);
|
|
});
|
|
}
|
|
}
|
|
|
|
export { all, start, registerSocketHandler };
|