import * as app from './app.js'; import * as auth from './auth.js'; import * as form from './form.js'; import * as http from './http.js'; let gProcessIndex = 0; let gProcesses = {}; let gStatsTimer = false; const k_mime_types = { 'css': 'text/css', 'html': 'text/html', 'js': 'text/javascript', 'json': 'text/json', 'map': 'application/json', 'svg': 'image/svg+xml', }; const k_magic_bytes = [ {bytes: [0xff, 0xd8, 0xff, 0xdb], type: 'image/jpeg'}, {bytes: [0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01], type: 'image/jpeg'}, {bytes: [0xff, 0xd8, 0xff, 0xee], type: 'image/jpeg'}, {bytes: [0xff, 0xd8, 0xff, 0xe1, null, null, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00], type: 'image/jpeg'}, {bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], type: 'image/png'}, {bytes: [0x47, 0x49, 0x46, 0x38, 0x37, 0x61], type: 'image/gif'}, {bytes: [0x47, 0x49, 0x46, 0x38, 0x39, 0x61], type: 'image/gif'}, {bytes: [0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50], type: 'image/webp'}, {bytes: [0x3c, 0x73, 0x76, 0x67], type: 'image/svg+xml'}, {bytes: [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32], type: 'audio/mpeg'}, {bytes: [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d], type: 'video/mp4'}, {bytes: [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32], type: 'video/mp4'}, {bytes: [0x4d, 0x54, 0x68, 0x64], type: 'audio/midi'}, ]; let k_static_files = [ {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: '/tfrpc.js', type: 'text/javascript; charset=UTF-8', headers: {'Access-Control-Allow-Origin': 'null'}}, {uri: '/robots.txt', type: 'text/plain; charset=UTF-8'}, ]; const k_global_settings = { index: { type: 'string', default_value: '/~core/apps/', description: 'Default path.', }, index_map: { type: 'textarea', default_value: undefined, description: 'Mappings from hostname to redirect path, one per line, as in: "www.tildefriends.net=/~core/index/"', }, room: { type: 'boolean', default_value: true, description: 'Whether this instance should behave as a room.', }, room_name: { type: 'string', default_value: 'tilde friends tunnel', description: 'Name of the room.', }, code_of_conduct: { type: 'textarea', default_value: undefined, description: 'Code of conduct presented at sign-in.', }, http_redirect: { type: 'string', default_value: undefined, description: 'If connecting by HTTP and HTTPS is configured, Location header prefix (ie, "https://example.com")', }, fetch_hosts: { type: 'string', default_value: undefined, description: 'Comma-separated list of host names to which HTTP fetch requests are allowed. None if empty.', }, blob_fetch_age_seconds: { type: 'integer', default_value: (platform() == 'android' || platform() == 'iphone' ? 0.5 * 365 * 24 * 60 * 60 : undefined), description: 'Only blobs mentioned more recently than this age will be automatically fetched.', }, blob_expire_age_seconds: { type: 'integer', default_value: (platform() == 'android' || platform() == 'iphone' ? 1.0 * 365 * 24 * 60 * 60 : undefined), description: 'Blobs older than this will be automatically deleted.', }, }; let gGlobalSettings = { index: "/~core/apps/", }; let kPingInterval = 60 * 1000; function printError(out, error) { if (error.stackTrace) { out.print(error.fileName + ":" + error.lineNumber + ": " + error.message); out.print(error.stackTrace); } else { for (let [k, v] of Object.entries(error)) { out.print(k, v); } out.print(error.toString()); } } function invoke(handlers, argv) { let promises = []; if (handlers) { for (let 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) { let promises = []; for (let i in gProcesses) { let process = gProcesses[i]; if (process.eventHandlers[eventName]) { promises.push(invoke(process.eventHandlers[eventName], argv)); } } return Promise.all(promises); } function broadcast(message) { let sender = this; let promises = []; for (let i in gProcesses) { let process = gProcesses[i]; if (process != sender && process.packageOwner == sender.packageOwner && process.packageName == sender.packageName) { let 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) { let db = new Database(user); try { let 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) { let actualOptions = {timeout: kPingInterval}; if (options) { for (let i in options) { actualOptions[i] = options[i]; } } return getProcessBlob(blobId, 'session_' + session, actualOptions); } async function getProcessBlob(blobId, key, options) { let process = gProcesses[key]; if (!process && !(options && "create" in options && !options.create)) { let resolveReady; let rejectReady; 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 = {}; if (!options?.script || options?.script === 'app.js') { process.app = new app.App(); } process.lastActive = Date.now(); process.lastPing = null; process.timeout = options.timeout; process.stats = false; 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]; }; let imports = { 'core': { 'broadcast': broadcast.bind(process), 'register': function(eventName, handler) { if (!process.eventHandlers[eventName]) { process.eventHandlers[eventName] = []; } process.eventHandlers[eventName].push(handler); }, 'unregister': function(eventName, 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), 'users': function() { try { return JSON.parse(new Database('auth').get('users')); } catch { return []; } }, 'permissionsGranted': function() { let user = process?.credentials?.session?.name; if (user && options?.packageOwner && options?.packageName && gGlobalSettings.userPermissions && gGlobalSettings.userPermissions[user] && gGlobalSettings.userPermissions[user][options.packageOwner]) { return gGlobalSettings.userPermissions[user][options.packageOwner][options.packageName]; } }, 'allPermissionsGranted': function() { let user = process?.credentials?.session?.name; if (user && options?.packageOwner && options?.packageName && gGlobalSettings.userPermissions && gGlobalSettings.userPermissions[user]) { return gGlobalSettings.userPermissions[user]; } }, 'permissionsForUser': function(user) { return (gGlobalSettings?.permissions ? gGlobalSettings.permissions[user] : []) ?? []; }, 'apps': user => getApps(user, process), 'getSockets': getSockets, 'permissionTest': function(permission) { let user = process?.credentials?.session?.name; if (!user || !options?.packageOwner || !options?.packageName) { return; } else if (gGlobalSettings.userPermissions && gGlobalSettings.userPermissions[user] && gGlobalSettings.userPermissions[user][options.packageOwner] && gGlobalSettings.userPermissions[user][options.packageOwner][options.packageName] && gGlobalSettings.userPermissions[user][options.packageOwner][options.packageName][permission] !== undefined) { if (gGlobalSettings.userPermissions[user][options.packageOwner][options.packageName][permission]) { return true; } else { throw Error(`Permission denied: ${permission}.`); } } else if (process.app) { return process.app.makeFunction(['requestPermission'])(permission).then(function(value) { if (value == 'allow') { storePermission(user, options.packageOwner, options.packageName, permission, true); process.sendPermissions(); return true; } else if (value == 'allow once') { return true; } else if (value == 'deny') { storePermission(user, options.packageOwner, options.packageName, permission, false); process.sendPermissions(); throw Error(`Permission denied: ${permission}.`); } else if (value == 'deny once') { throw Error(`Permission denied: ${permission}.`); } throw Error(`Permission denied: ${permission}.`); }); } else { throw Error(`Permission denied: ${permission}.`); } }, app: { owner: options?.packageOwner, name: options?.packageName, }, url: options?.url, } }; if (process.credentials?.permissions?.administration) { imports.core.globalSettingsDescriptions = function() { let settings = Object.assign({}, k_global_settings); for (let [key, value] of Object.entries(gGlobalSettings)) { if (settings[key]) { settings[key].value = value; } } return settings; }; imports.core.globalSettingsGet = function(key) { return gGlobalSettings[key]; }; imports.core.globalSettingsSet = function(key, value) { print('Setting', key, value); gGlobalSettings[key] = value; setGlobalSettings(gGlobalSettings); print('Done.'); }; imports.core.deleteUser = function(user) { return Promise.resolve(imports.core.permissionTest('delete_user')).then(function() { let db = new Database('auth'); db.remove('user:' + user); let users = new Set(); let users_original = db.get('users'); try { users = new Set(JSON.parse(users_original)); } catch { } users.delete(user); users = JSON.stringify([...users].sort()); if (users !== users_original) { db.set('users', users); } }); }; } if (options.api) { imports.app = {}; for (let i in options.api) { let api = options.api[i]; imports.app[api[0]] = process.app.makeFunction(api); } } for (let [name, f] of Object.entries(options?.imports || {})) { imports[name] = f; } process.task.onPrint = function(args) { if (imports.app) { imports.app.print(...args); } }; process.task.onError = function(error) { try { if (process.app) { process.app.makeFunction(['error'])(error); } else { printError({print: print}, error); } } catch (e) { printError({print: print}, error); } }; imports.ssb = Object.fromEntries(Object.keys(ssb).map(key => [key, ssb[key].bind(ssb)])); imports.ssb.createIdentity = function() { if (process.credentials && process.credentials.session && process.credentials.session.name) { return ssb.createIdentity(process.credentials.session.name); } }; imports.ssb.addIdentity = function(id) { if (process.credentials && process.credentials.session && process.credentials.session.name) { return Promise.resolve(imports.core.permissionTest('ssb_id_add')).then(function() { return ssb.addIdentity(process.credentials.session.name, id); }); } }; imports.ssb.deleteIdentity = function(id) { if (process.credentials && process.credentials.session && process.credentials.session.name) { return Promise.resolve(imports.core.permissionTest('ssb_id_delete')).then(function() { return ssb.deleteIdentity(process.credentials.session.name, id); }); } }; imports.ssb.getOwnerIdentities = function() { if (options.packageOwner) { return ssb.getIdentities(options.packageOwner); } }; imports.ssb.getIdentities = function() { if (process.credentials && process.credentials.session && process.credentials.session.name) { return ssb.getIdentities(process.credentials.session.name); } }; imports.ssb.getPrivateKey = function(id) { if (process.credentials && process.credentials.session && process.credentials.session.name) { return Promise.resolve(imports.core.permissionTest('ssb_id_export')).then(function() { return ssb.getPrivateKey(process.credentials.session.name, id); }); } }; imports.ssb.appendMessageWithIdentity = function(id, message) { if (process.credentials && process.credentials.session && process.credentials.session.name) { return Promise.resolve(imports.core.permissionTest('ssb_append')).then(function() { return ssb.appendMessageWithIdentity(process.credentials.session.name, id, message); }); } }; imports.ssb.privateMessageEncrypt = function(id, recipients, message) { if (process.credentials && process.credentials.session && process.credentials.session.name) { return ssb.privateMessageEncrypt(process.credentials.session.name, id, recipients, message); } }; imports.ssb.privateMessageDecrypt = function(id, message) { if (process.credentials && process.credentials.session && process.credentials.session.name) { return ssb.privateMessageDecrypt(process.credentials.session.name, id, message); } }; imports.ssb.setServerFollowingMe = function(id, following) { if (process.credentials && process.credentials.session && process.credentials.session.name) { return ssb.setServerFollowingMe(process.credentials.session.name, id, following); } }; imports.fetch = function(url, options) { return http.fetch(url, options, gGlobalSettings.fetch_hosts); } if (process.credentials && process.credentials.session && process.credentials.session.name) { imports.database = function(key) { let db = new Database(process.credentials.session.name + ':' + key); return Object.fromEntries(Object.keys(db).map(x => [x, db[x].bind(db)])); }; imports.my_shared_database = function(packageName, key) { let db = new Database(':shared:' + process.credentials.session.name + ':' + packageName + ':' + key); return Object.fromEntries(Object.keys(db).map(x => [x, db[x].bind(db)])); }; imports.databases = function() { return [].concat(databases.list(':shared:' + process.credentials.session.name + ':%'), databases.list(process.credentials.session.name + ':%')); }; } if (options.packageOwner && options.packageName) { imports.shared_database = function(key) { let db = new Database(':shared:' + options.packageOwner + ':' + options.packageName + ':' + key); return Object.fromEntries(Object.keys(db).map(x => [x, db[x].bind(db)])); } } process.sendPermissions = function sendPermissions() { process.app.send({action: 'permissions', permissions: imports.core.permissionsGranted()}); } process.resetPermission = function resetPermission(permission) { let user = process?.credentials?.session?.name; storePermission(user, options?.packageOwner, options?.packageName, permission, undefined); process.sendPermissions(); } process.task.setImports(imports); process.task.activate(); let source = await getBlobOrContent(blobId); let appSourceName = blobId; let appSource = utf8Decode(source); try { let appObject = JSON.parse(appSource); if (appObject.type == "tildefriends-app") { appSourceName = options?.script ?? 'app.js'; let id = appObject.files[appSourceName]; let blob = await getBlobOrContent(id); appSource = utf8Decode(blob); await process.task.loadFile(['/tfrpc.js', await File.readFile('core/tfrpc.js')]); await Promise.all(Object.keys(appObject.files).map(async function(f) { await process.task.loadFile([f, await getBlobOrContent(appObject.files[f])]); })); } } catch (e) { printError({print: print}, e); } broadcastEvent('onSessionBegin', [getUser(process, process)]); if (process.app) { process.app.send({action: "ready", version: version()}); process.sendPermissions(); } await process.task.execute({name: appSourceName, source: appSource}); resolveReady(process); } catch (error) { if (process.app) { if (process?.task?.onError) { process.task.onError(error); } else { printError({print: print}, error); } } else { printError({print: print}, error); } rejectReady(error); } } return process; } function setGlobalSettings(settings) { gGlobalSettings = settings; try { return new Database('core').set('settings', JSON.stringify(settings)); } catch (error) { print('Error storing settings:', error); } } function startsWithBytes(data, bytes) { if (data.byteLength >= bytes.length) { let dataBytes = new Uint8Array(data.slice(0, bytes.length)); for (let 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 (let i in k_static_files) { if (uri === k_static_files[i].uri) { let path = k_static_files[i].path || uri.substring(1); let type = k_static_files[i].type || guessTypeFromName(path); let stat = await File.stat('core/' + path); let id = `${stat.mtime}_${stat.size}`; if (request.headers['if-none-match'] === '"' + id + '"') { response.writeHead(304, {'Content-Length': '0'}); response.end(); } else { let data = await File.readFile('core/' + path); response.writeHead(200, Object.assign( { 'Content-Type': type, 'Content-Length': data.byteLength, 'etag': '"' + id + '"', }, k_static_files[i].headers || {})); 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"); } async function staticDirectoryHandler(request, response, directory, uri) { let 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 { let stat = await File.stat(directory + filename); let id = `${stat.mtime}_${stat.size}`; if (request.headers['if-none-match'] === '"' + id + '"') { response.writeHead(304, {'Content-Length': '0'}); response.end(); } else { let data = await File.readFile(directory + filename); response.writeHead(200, { 'Content-Type': k_mime_types[filename.split('.').pop()] || 'text/plain', 'Content-Length': data.byteLength, 'etag': '"' + id + '"', }); 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) { let 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 guessTypeFromName(path) { let extension = path.split('.').pop(); return k_mime_types[extension]; } function guessTypeFromMagicBytes(data) { for (let magic of k_magic_bytes) { if (startsWithBytes(data, magic.bytes)) { return magic.type; } } } function sendData(response, data, type, headers, status_code) { if (data) { response.writeHead(status_code ?? 200, Object.assign({"Content-Type": type || guessTypeFromMagicBytes(data) || "application/binary", "Content-Length": data.byteLength}, headers || {})); response.end(data); } else { response.writeHead(status_code ?? 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); } } let g_handler_index = 0; async function useAppHandler(response, handler_blob_id, path, query, headers, packageOwner, packageName) { print('useAppHandler', packageOwner, packageName); let do_resolve; let promise = new Promise(async function(resolve, reject) { do_resolve = resolve; }); let process; let result; try { process = await getProcessBlob(handler_blob_id, 'handler_' + g_handler_index++, { script: 'handler.js', imports: { request: { path: path, query: query, }, respond: do_resolve, }, credentials: auth.query(headers), packageOwner: packageOwner, packageName: packageName, }); await process.ready; result = await promise; } finally { if (process?.task) { await process.task.kill(); } } return result; } async function blobHandler(request, response, blobId, uri) { for (let i in k_static_files) { if (uri === k_static_files[i].uri && k_static_files[i].path) { let stat = await File.stat('core/' + k_static_files[i].path); let id = `${stat.mtime}_${stat.size}`; if (request.headers['if-none-match'] === '"' + id + '"') { response.writeHead(304, {'Content-Length': '0'}); response.end(); } else { let data = await File.readFile('core/' + k_static_files[i].path); response.writeHead(200, Object.assign( { 'Content-Type': k_static_files[i].type, 'Content-Length': data.byteLength, 'etag': '"' + id + '"', }, k_static_files[i].headers || {})); response.end(data); } return; } } if (!uri) { response.writeHead(303, {"Location": (request.client.tls ? 'https://' : 'http://') + request.headers.host + blobId + '/', "Content-Length": "0"}); response.end(); return; } let process; if (uri == "/view") { let data; let match; let query = form.decodeForm(request.query); let headers = { 'Content-Security-Policy': 'sandbox', }; if (query.filename && query.filename.match(/^[A-Za-z0-9\.-]*$/)) { headers['Content-Disposition'] = `attachment; filename=${query.filename}`; } if (match = /^\/\~(\w+)\/(\w+)$/.exec(blobId)) { let id = await new Database(match[1]).get('path:' + match[2]); if (id) { if (request.headers['if-none-match'] === '"' + id + '"') { headers['Content-Length'] = '0'; response.writeHead(304, headers); response.end(); } else { data = await getBlobOrContent(id); if (match[3]) { let appObject = JSON.parse(data); data = appObject.files[match[3]]; } sendData(response, data, undefined, Object.assign({etag: '"' + id + '"'}, headers)); } } else { if (request.headers['if-none-match'] === '"' + blobId + '"') { headers['Content-Length'] = '0'; response.writeHead(304, headers); response.end(); } else { sendData(response, data, undefined, Object.assign({etag: '"' + blobId + '"'}, headers)); } } } else { if (request.headers['if-none-match'] === '"' + blobId + '"') { headers['Content-Length'] = '0'; response.writeHead(304, headers); response.end(); } else { data = await getBlobOrContent(blobId); sendData(response, data, undefined, Object.assign({etag: '"' + blobId + '"'}, headers)); } } } else if (uri == "/save") { let match; if (match = /^\/\~(\w+)\/(\w+)$/.exec(blobId)) { let user = match[1]; let appName = match[2]; let credentials = auth.query(request.headers); if (credentials && credentials.session && (credentials.session.name == user || (credentials.permissions.administration && user == 'core'))) { let database = new Database(user); let app_object = JSON.parse(utf8Decode(request.body)); let previous_id = database.get('path:' + appName); if (previous_id) { try { let previous_object = JSON.parse(utf8Decode(await ssb.blobGet(previous_id))); delete previous_object.previous; delete app_object.previous; if (JSON.stringify(previous_object) == JSON.stringify(app_object)) { response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8"}); response.end("/" + previous_id); return; } } catch { } } app_object.previous = previous_id; let newBlobId = await ssb.blobStore(JSON.stringify(app_object)); let apps = new Set(); let apps_original = database.get('apps'); try { apps = new Set(JSON.parse(apps_original)); } catch { } if (!apps.has(appName)) { apps.add(appName); } apps = JSON.stringify([...apps].sort()); if (apps != apps_original) { database.set('apps', apps); } database.set('path:' + appName, newBlobId); response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8"}); response.end("/" + newBlobId); } else { response.writeHead(401, {"Content-Type": "text/plain; charset=utf-8"}); response.end("401 Unauthorized"); return; } } else if (blobId === '') { let newBlobId = await ssb.blobStore(request.body); response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8"}); response.end("/" + newBlobId); } else { response.writeHead(400, {"Content-Type": "text/plain; charset=utf-8"}); response.end('Invalid name.'); } } else if (uri == "/delete") { let match; if (match = /^\/\~(\w+)\/(\w+)$/.exec(blobId)) { let user = match[1]; let appName = match[2]; let credentials = auth.query(request.headers); if (credentials && credentials.session && (credentials.session.name == user || (credentials.permissions.administration && user == 'core'))) { let database = new Database(user); let apps = new Set(); try { apps = new Set(JSON.parse(database.get('apps'))); } catch { } if (apps.delete(appName)) { database.set('apps', JSON.stringify([...apps].sort())); } database.remove('path:' + appName); } 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('OK'); } else { let data; let match; let id; let app_id = blobId; let packageOwner; let packageName; if (match = /^\/\~(\w+)\/(\w+)$/.exec(blobId)) { packageOwner = match[1]; packageName = match[2]; let db = new Database(match[1]); app_id = await db.get('path:' + match[2]); } let app_object = JSON.parse(utf8Decode(await getBlobOrContent(app_id))); id = app_object.files[uri.substring(1)]; if (!id && app_object.files['handler.js']) { let answer; try { answer = await useAppHandler(response, app_id, uri.substring(1), request.query ? form.decodeForm(request.query) : undefined, request.headers, packageOwner, packageName); } catch (error) { data = utf8Encode(`Internal Server Error\n\n${error?.message}\n${error?.stack}`); response.writeHead(500, {'Content-Type': 'text/plain; charset=utf-8', 'Content-Length': data.length}); response.end(data); return; } if (answer && typeof answer.data == 'string') { answer.data = utf8Encode(answer.data); } sendData(response, answer?.data, answer?.content_type, Object.assign(answer?.headers ?? {}, { 'Access-Control-Allow-Origin': '*', 'Content-Security-Policy': 'sandbox', }), answer.status_code); } else if (id) { if (request.headers['if-none-match'] && request.headers['if-none-match'] == '"' + id + '"') { let headers = { 'Access-Control-Allow-Origin': '*', 'Content-Security-Policy': 'sandbox', 'Content-Length': '0', }; response.writeHead(304, headers); response.end(); } else { let headers = { 'ETag': '"' + id + '"', 'Access-Control-Allow-Origin': '*', 'Content-Security-Policy': 'sandbox', }; data = await getBlobOrContent(id); let type = guessTypeFromName(uri) || guessTypeFromMagicBytes(data); sendData(response, data, type, headers); } } else { sendData(response, data, undefined, {}); } } } ssb.addEventListener('broadcasts', function() { broadcastEvent('onBroadcastsChanged', []); }); ssb.addEventListener('connections', function() { broadcastEvent('onConnectionsChanged', []); }); async function loadSettings() { let data = {}; try { let settings = new Database('core').get('settings'); if (settings) { data = JSON.parse(settings); } } catch (error) { print("Settings not found in database:", error); } for (let [key, value] of Object.entries(k_global_settings)) { if (data[key] === undefined) { data[key] = value.default_value; } } gGlobalSettings = data; } function sendStats() { let apps = Object.values(gProcesses).filter(process => process.app && process.stats).map(process => process.app); if (apps.length) { let stats = getStats(); for (let app of apps) { app.send({action: 'stats', stats: stats}); } setTimeout(sendStats, 1000); } else { gStatsTimer = false; } } function enableStats(process, enabled) { process.stats = enabled; if (!gStatsTimer) { gStatsTimer = true; sendStats(); } } function stringResponse(response, data) { let bytes = utf8Encode(data); response.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Content-Length": bytes.byteLength.toString(), "Access-Control-Allow-Origin": "*", }); return response.end(bytes); } loadSettings().then(function() { if (tildefriends.https_port && gGlobalSettings.http_redirect) { httpd.set_http_redirect(gGlobalSettings.http_redirect); } httpd.all("/login", auth.handler); httpd.registerSocketHandler("/app/socket", app.socket); httpd.all("", function(request, response) { let match; if (request.uri === "/" || request.uri === "") { try { for (let line of (gGlobalSettings.index_map || '').split('\n')) { let parts = line.split('='); if (parts.length == 2 && request.headers.host.match(new RegExp(parts[0], 'i'))) { response.writeHead(303, {"Location": (request.client.tls ? 'https://' : 'http://') + request.headers.host + parts[1], "Content-Length": "0"}); return response.end(); } } } catch (e) { print(e); } 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\/lit\/([\.\w-/]*)$/.exec(request.uri)) { return staticDirectoryHandler(request, response, 'deps/lit/', match[1]); } else if (match = /^\/codemirror\/([\.\w-/]*)$/.exec(request.uri)) { return staticDirectoryHandler(request, response, 'deps/codemirror/', match[1]); } else if (match = /^\/speedscope\/([\.\w-/]*)$/.exec(request.uri)) { return staticDirectoryHandler(request, response, 'deps/speedscope/', match[1]); } else if (match = /^\/static(\/.*)/.exec(request.uri)) { return staticFileHandler(request, response, null, match[1]); } else if (request.uri == "/robots.txt") { return staticFileHandler(request, response, null, request.uri); } else if (match = /^(.*)(\/(?:save|delete)?)$/.exec(request.uri)) { return blobHandler(request, response, match[1], match[2]); } else if ((match = /^\/.well-known\/(.*)/.exec(request.uri)) && request.uri.indexOf("..") == -1) { return wellKnownHandler(request, response, match[1]); } else { let data = "File not found."; response.writeHead(404, {"Content-Type": "text/plain; charset=utf-8", "Content-Length": data.length.toString()}); return response.end(data); } }); let port = httpd.start(tildefriends.http_port); if (tildefriends.args.out_http_port_file) { print("Writing the port file."); File.writeFile(tildefriends.args.out_http_port_file, port.toString() + '\n').then(function(r) { print("Wrote the port file:", tildefriends.args.out_http_port_file, r); }).catch(function() { print("Failed to write the port file."); }); } if (tildefriends.https_port) { async function start_tls() { const kCertificatePath = "data/httpd/certificate.pem"; const kPrivateKeyPath = "data/httpd/privatekey.pem"; let privateKey = utf8Decode(await File.readFile(kPrivateKeyPath)); let certificate = utf8Decode(await File.readFile(kCertificatePath)); let context = new TlsContext(); context.setPrivateKey(privateKey); context.setCertificate(certificate); httpd.start(tildefriends.https_port, context); } start_tls(); } }).catch(function(error) { print('Failed to load settings.'); printError({print: print}, error); exit(1); }); function storePermission(user, packageOwner, packageName, permission, allow) { if (!gGlobalSettings.userPermissions) { gGlobalSettings.userPermissions = {}; } if (!gGlobalSettings.userPermissions[user]) { gGlobalSettings.userPermissions[user] = {}; } if (!gGlobalSettings.userPermissions[user][packageOwner]) { gGlobalSettings.userPermissions[user][packageOwner] = {}; } if (!gGlobalSettings.userPermissions[user][packageOwner][packageName]) { gGlobalSettings.userPermissions[user][packageOwner][packageName] = {}; } if (gGlobalSettings.userPermissions[user][packageOwner][packageName][permission] !== allow) { if (allow === undefined) { delete gGlobalSettings.userPermissions[user][packageOwner][packageName][permission]; } else { gGlobalSettings.userPermissions[user][packageOwner][packageName][permission] = allow; } setGlobalSettings(gGlobalSettings); } } export { gGlobalSettings as globalSettings, setGlobalSettings, enableStats, invoke, getSessionProcessBlob, };