"use strict"; var auth = require("auth"); var app = require("app"); var gProcessIndex = 0; var gProcesses = {}; var gSessionIndex = 0; var gStatsTimer = false; var gGlobalSettings = { index: "/~core/index", }; var kGlobalSettingsFile = "data/global/settings.json"; var kPingInterval = 60 * 1000; function getCookies(headers) { var cookies = {}; if (headers.cookie) { var parts = headers.cookie.split(/,|;/); for (var i in parts) { var equals = parts[i].indexOf("="); var name = parts[i].substring(0, equals).trim(); var value = parts[i].substring(equals + 1).trim(); cookies[name] = value; } } return cookies; } function makeSessionId() { return (gSessionIndex++).toString(); } function printError(out, error) { if (error.stackTrace) { out.print(error.fileName + ":" + error.lineNumber + ": " + error.message); out.print(error.stackTrace); } else { for (var i in error) { out.print(i); } out.print(error.toString()); } } function invoke(handlers, argv) { var promises = []; if (handlers) { for (var i = 0; i < handlers.length; ++i) { try { promises.push(handlers[i](...argv)); } catch (error) { handlers.splice(i, 1); i--; promises.push(new Promise(function(resolve, reject) { reject(error); })); } } } return Promise.all(promises); } function broadcastEvent(eventName, argv) { var promises = []; for (var i in gProcesses) { var process = gProcesses[i]; if (process.eventHandlers[eventName]) { promises.push(invoke(process.eventHandlers[eventName], argv)); } } return Promise.all(promises); } function broadcast(message) { var sender = this; var promises = []; for (var i in gProcesses) { var process = gProcesses[i]; if (process != sender && process.packageOwner == sender.packageOwner && process.packageName == sender.packageName) { var from = getUser(process, sender); promises.push(postMessageInternal(from, process, message)); } } return Promise.all(promises); } function getUser(caller, process) { return { name: process.userName, key: process.key, index: process.index, packageOwner: process.packageOwner, packageName: process.packageName, credentials: process.credentials, postMessage: postMessageInternal.bind(caller, caller, process), }; } function getApps(user, process) { if (process.credentials && process.credentials.session && process.credentials.session.name) { if (user && user !== process.credentials.session.name && user !== 'core') { return {}; } else if (!user) { user = process.credentials.session.name; } } if (user) { var db = new Database(user); try { var names = JSON.parse(db.get('apps')); return Object.fromEntries(names.map(name => [name, db.get('path:' + name)])); } catch { } } return {}; } function postMessageInternal(from, to, message) { if (to.eventHandlers['message']) { return invoke(to.eventHandlers['message'], [getUser(from, from), message]); } } async function getSessionProcessBlob(blobId, session, options) { var actualOptions = {timeout: kPingInterval}; if (options) { for (var i in options) { actualOptions[i] = options[i]; } } return getProcessBlob(blobId, 'session_' + session, actualOptions); } let gManifestCache = {}; async function getProcessBlob(blobId, key, options) { var process = gProcesses[key]; if (!process && !(options && "create" in options && !options.create)) { try { print("Creating task for " + blobId + " " + key); process = {}; process.key = key; process.index = gProcessIndex++; process.userName = 'user' + process.index; process.credentials = options.credentials || {}; process.task = new Task(); process.eventHandlers = {}; process.app = new App(); process.lastActive = Date.now(); process.lastPing = null; process.timeout = options.timeout; process.stats = false; var resolveReady; var rejectReady; process.ready = new Promise(function(resolve, reject) { resolveReady = resolve; rejectReady = reject; }); gProcesses[key] = process; process.task.onExit = function(exitCode, terminationSignal) { broadcastEvent('onSessionEnd', [getUser(process, process)]); process.task = null; delete gProcesses[key]; }; var imports = { 'core': { 'broadcast': broadcast.bind(process), 'register': function(eventName, handler) { if (!process.eventHandlers[eventName]) { process.eventHandlers[eventName] = []; } process.eventHandlers[eventName].push(handler); }, 'unregister': function(eventHandle, handler) { if (process.eventHandlers[eventName]) { let index = process.eventHandlers[eventName].indexOf(handler); if (index != -1) { process.eventHandlers[eventName].splice(index, 1); } if (process.eventHandlers[eventName].length == 0) { delete process.eventHandlers[eventName]; } } }, 'user': getUser(process, process), 'apps': user => getApps(user, process), } }; if (options.api) { imports.app = {}; for (let i in options.api) { let api = options.api[i]; imports.app[api[0]] = process.app.makeFunction(api); } } process.task.onPrint = function(args) { process.app.send({action: 'print', args: args}); }; process.task.onError = function(error) { try { process.app.send({action: 'error', error: error}); } catch(e) { print(e); } }; imports.ssb = Object.fromEntries(Object.keys(ssb).map(key => [key, ssb[key].bind(ssb)])); if (process.credentials && process.credentials.session && process.credentials.session.name) { imports.database = function(key) { var db = new Database(process.credentials.session.name + ':' + key); return Object.fromEntries(Object.keys(db).map(x => [x, db[x].bind(db)])); }; } process.task.setImports(imports); process.task.activate(); let source = await getBlobOrContent(blobId); var appSourceName = blobId; var appSource = utf8Decode(source); try { var app = JSON.parse(appSource); if (app.type == "tildefriends-app") { appSourceName = 'app.js'; var id = app.files[appSourceName]; var blob = await getBlobOrContent(id); appSource = utf8Decode(blob); await Promise.all(Object.keys(app.files).map(async function(f) { await process.task.loadFile([f, await getBlobOrContent(app.files[f])]); })); } } catch (e) { printError({print: print}, e); } broadcastEvent('onSessionBegin', [getUser(process, process)]); resolveReady(process); if (process.app) { process.app.send({action: "ready"}); } await process.task.execute({name: appSourceName, source: appSource}); } catch (error) { if (process.app) { process.app.send({action: 'error', error: error}); } else { printError({print: print}, error); } rejectReady(); } } return process; } function setGlobalSettings(settings) { gGlobalSettings = settings; try { return new Database('core').set('settings', JSON.stringify(settings)); } catch (error) { print('Error storing settings:', error); } } var kStaticFiles = [ {uri: '/', path: 'index.html', type: 'text/html; charset=UTF-8'}, {uri: '/style.css', type: 'text/css; charset=UTF-8'}, {uri: '/favicon.png', type: 'image/png'}, {uri: '/client.js', type: 'text/javascript; charset=UTF-8'}, {uri: '/robots.txt', type: 'text/plain; charset=UTF-8'}, {uri: '/split.min.js'}, {uri: '/split.min.js.map'}, {uri: '/smoothie.js'}, {uri: '/codemirror/codemirror.min.js'}, {uri: '/codemirror/base16-dark.min.css'}, {uri: '/codemirror/matchesonscrollbar.min.css'}, {uri: '/codemirror/dialog.min.css'}, {uri: '/codemirror/codemirror.min.css'}, {uri: '/codemirror/trailingspace.min.js'}, {uri: '/codemirror/dialog.min.js'}, {uri: '/codemirror/search.min.js'}, {uri: '/codemirror/searchcursor.min.js'}, {uri: '/codemirror/jump-to-line.min.js'}, {uri: '/codemirror/matchesonscrollbar.min.js'}, {uri: '/codemirror/annotatescrollbar.min.js'}, {uri: '/codemirror/javascript.min.js'}, {uri: '/codemirror/css.min.js'}, {uri: '/codemirror/xml.min.js'}, {uri: '/codemirror/htmlmixed.min.js'}, ]; function startsWithBytes(data, bytes) { if (data.byteLength >= bytes.length) { var dataBytes = new Uint8Array(data.slice(0, bytes.length)); for (var i = 0; i < bytes.length; i++) { if (dataBytes[i] != bytes[i] && bytes[i] !== null) { return; } } return true; } } async function staticFileHandler(request, response, blobId, uri) { for (var i in kStaticFiles) { if (uri === kStaticFiles[i].uri) { var path = kStaticFiles[i].path || uri.substring(1); var type = kStaticFiles[i].type || guessType(path); var data = await File.readFile("core/" + path); response.writeHead(200, {"Content-Type": type, "Content-Length": data.byteLength}); response.end(data); return; } } response.writeHead(404, {"Content-Type": "text/plain; charset=utf-8", "Content-Length": "File not found".length}); response.end("File not found"); } const k_mime_types = { 'json': 'text/json', 'js': 'text/javascript', 'html': 'text/html', 'css': 'text/css', 'map': 'application/json', }; async function perfettoHandler(request, response, uri) { var filename = uri || 'index.html'; if (filename.indexOf('..') != -1) { response.writeHead(404, {"Content-Type": "text/plain; charset=utf-8", "Content-Length": "File not found".length}); response.end("File not found"); return; } try { var data = await File.readFile("deps/perfetto/" + filename); response.writeHead(200, {"Content-Type": k_mime_types[filename.split('.').pop()] || 'text/plain', "Content-Length": data.byteLength}); response.end(data); } catch { response.writeHead(404, {"Content-Type": "text/plain; charset=utf-8", "Content-Length": "File not found".length}); response.end("File not found"); } } async function wellKnownHandler(request, response, path) { var data = await File.readFile("data/global/.well-known/" + path); if (data) { response.writeHead(200, {"Content-Type": "text/plain", "Content-Length": data.length}); response.end(data); } else { response.writeHead(404, {"Content-Type": "text/plain", "Content-Length": "File not found".length}); response.end("File not found"); } } function sendData(response, data, type, headers) { if (data) { if (startsWithBytes(data, [0xff, 0xd8, 0xff, 0xdb]) || startsWithBytes(data, [0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01]) || startsWithBytes(data, [0xff, 0xd8, 0xff, 0xee]) || startsWithBytes(data, [0xff, 0xd8, 0xff, 0xe1, null, null, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00])) { response.writeHead(200, Object.assign({"Content-Type": "image/jpeg", "Content-Length": data.byteLength}, headers || {})); response.end(data); } else if (startsWithBytes(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) { response.writeHead(200, Object.assign({"Content-Type": "image/png", "Content-Length": data.byteLength}, headers || {})); response.end(data); } else if (startsWithBytes(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) || startsWithBytes(data, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])) { response.writeHead(200, Object.assign({"Content-Type": "image/gif", "Content-Length": data.byteLength}, headers || {})); response.end(data); } else { response.writeHead(200, Object.assign({"Content-Type": type || "text/javascript; charset=utf-8", "Content-Length": data.byteLength}, headers || {})); response.end(data); } } else { response.writeHead(404, Object.assign({"Content-Type": "text/plain; charset=utf-8", "Content-Length": "File not found".length}, headers || {})); response.end("File not found"); } } async function getBlobOrContent(id) { if (!id) { return; } else if (id.startsWith('&')) { return ssb.blobGet(id); } else if (id.startsWith('%')) { return ssb.messageContentGet(id); } } function guessType(path) { const k_extension_to_type = { 'css': 'text/css', 'html': 'text/html', 'js': 'text/javascript', }; var extension = path.split('.').pop(); return k_extension_to_type[extension]; } async function blobHandler(request, response, blobId, uri) { var found = false; if (!found) { for (var i in kStaticFiles) { if (uri === kStaticFiles[i].uri) { found = true; var data = await File.readFile("core/" + kStaticFiles[i].path); response.writeHead(200, {"Content-Type": kStaticFiles[i].type, "Content-Length": data.byteLength}); response.end(data); break; } } } if (!uri) { response.writeHead(303, {"Location": (request.client.tls ? 'https://' : 'http://') + request.headers.host + blobId + '/', "Content-Length": "0"}); response.end(data); return; } if (!found) { var process; if (uri == "/view") { var data; if (match = /^\/\~(\w+)\/(\w+)$/.exec(blobId)) { var id = await new Database(match[1]).get('path:' + match[2]); if (id) { if (request.headers['if-none-match'] && request.headers['if-none-match'] == '"' + id + '"') { response.writeHead(304, {}); response.end(); } else { data = await getBlobOrContent(id); if (match[3]) { var app = JSON.parse(data); data = app.files[match[3]]; } sendData(response, data, undefined, {etag: '"' + id + '"'}); } } else { sendData(response, data); } } else { data = await getBlobOrContent(blobId); sendData(response, data); } } else if (uri == "/save") { let newBlobId = await ssb.blobStore(request.body); var match; if (match = /^\/\~(\w+)\/(\w+)$/.exec(blobId)) { var user = match[1]; var app = match[2]; var credentials = auth.query(request.headers); if (credentials && credentials.session && (credentials.session.name == user || (credentials.permissions.administration && user == 'core'))) { var database = new Database(user); var apps = new Set(); try { apps = new Set(JSON.parse(database.get('apps'))); } catch { } if (!apps.has(app)) { apps.add(app); database.set('apps', JSON.stringify([...apps])); } database.set('path:' + app, newBlobId); } else { response.writeHead(401, {"Content-Type": "text/plain; charset=utf-8"}); response.end("401 Unauthorized"); return; } } response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8"}); response.end("/" + newBlobId); } else { var data; var type; var headers; if (match = /^\/\~(\w+)\/(\w+)$/.exec(blobId)) { var db = new Database(match[1]); var id = await db.get('path:' + match[2]); if (id) { if (request.headers['if-none-match'] && request.headers['if-none-match'] == '"' + id + '"') { response.writeHead(304, {}); response.end(); } else { data = utf8Decode(await getBlobOrContent(id)); var app = JSON.parse(data); data = app.files[uri.substring(1)]; data = await getBlobOrContent(data); type = guessType(uri); headers = {'ETag': '"' + id + '"'}; sendData(response, data, type, headers); } } else { sendData(response, data, type, headers); } } else { data = utf8Decode(await getBlobOrContent(blobId)); var app = JSON.parse(data); data = app.files[uri.substring(1)]; data = await getBlobOrContent(data); sendData(response, data, type, headers); } } } } ssb.addEventListener('broadcasts', function() { broadcastEvent('onBroadcastsChanged', []); }); ssb.addEventListener('connections', function() { broadcastEvent('onConnectionsChanged', []); }); async function loadSettings() { var data; try { var settings = new Database('core').get('settings'); if (settings) { data = JSON.parse(settings); } } catch (error) { print("Settings not found in database:", error); } if (!data) { try { data = JSON.parse(utf8Decode(await File.readFile(kGlobalSettingsFile))); new Database('core').set('settings', JSON.stringify(data)); } catch (error) { print("Unable to load settings from " + kGlobalSettingsFile + ":", error); } } if (data) { gGlobalSettings = data; } } function sendStats() { var any = false; for (var process of Object.values(gProcesses)) { if (process.app && process.stats) { process.app.send({action: 'stats', stats: getStats()}); any = true; } } if (any) { setTimeout(sendStats, 1000); } else { gStatsTimer = false; } } loadSettings().then(function() { var auth = require("auth"); var httpd = require("httpd"); httpd.all("/login", auth.handler); httpd.all("", function(request, response) { var match; if (request.uri === "/" || request.uri === "") { response.writeHead(303, {"Location": (request.client.tls ? 'https://' : 'http://') + request.headers.host + gGlobalSettings.index, "Content-Length": "0"}); return response.end(); } else if (match = /^(\/~[^\/]+\/[^\/]+)(\/?.*)$/.exec(request.uri)) { return blobHandler(request, response, match[1], match[2]); } else if (match = /^\/([&\%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(request.uri)) { return blobHandler(request, response, match[1], match[2]); } else if (match = /^\/static(\/.*)/.exec(request.uri)) { return staticFileHandler(request, response, null, match[1]); } else if (match = /^\/perfetto\/([\.\w-/]*)$/.exec(request.uri)) { return perfettoHandler(request, response, match[1]); } else if (match = /^(.*)(\/save?)$/.exec(request.uri)) { return blobHandler(request, response, match[1], match[2]); } else if (match = /^\/trace$/.exec(request.uri)) { var data = trace(); response.writeHead(200, {"Content-Type": "application/json; charset=utf-8", "Content-Length": data.length.toString()}); return response.end(data); } else if (request.uri == "/robots.txt") { return blobHandler(request, response, null, request.uri); } else if ((match = /^\/.well-known\/(.*)/.exec(request.uri)) && request.uri.indexOf("..") == -1) { return wellKnownHandler(request, response, match[1]); } else { var data = "File not found."; response.writeHead(404, {"Content-Type": "text/plain; charset=utf-8", "Content-Length": data.length.toString()}); return response.end(data); } }); httpd.registerSocketHandler("/app/socket", app.socket); }).catch(function(error) { print('Failed to load settings.'); });