Merge branches/quickjs to trunk. This is the way.

git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@3621 ed5197a5-7fde-0310-b194-c3ffbd925b24
This commit is contained in:
2021-01-02 18:10:00 +00:00
parent d293637741
commit 79022e1e1f
703 changed files with 419987 additions and 30640 deletions

View File

@ -3,9 +3,8 @@
require("encoding-indexes");
require("encoding");
var terminal = require("terminal");
var auth = require("auth");
var network = require("network");
var app = require("app");
var gProcessIndex = 0;
var gProcesses = {};
@ -51,11 +50,29 @@ function printError(out, error) {
}
}
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];
promises.push(invoke(process.eventHandlers[eventName], argv));
if (process.eventHandlers[eventName]) {
promises.push(invoke(process.eventHandlers[eventName], argv));
}
}
return Promise.all(promises);
}
@ -75,53 +92,6 @@ function broadcast(message) {
return Promise.all(promises);
}
function getDatabase(process) {
if (!process.database) {
File.makeDirectory("data");
File.makeDirectory("data/" + process.packageOwner);
File.makeDirectory("data/" + process.packageOwner + "/" + process.packageName);
File.makeDirectory("data/" + process.packageOwner + "/" + process.packageName + "/db");
process.database = new Database("data/" + process.packageOwner + "/" + process.packageName + "/db");
}
return process.database;
}
function databaseGet(key) {
return getDatabase(this).get(key);
}
function databaseSet(key, value) {
return getDatabase(this).set(key, value);
}
function databaseRemove(key) {
return getDatabase(this).remove(key);
}
function databaseGetAll() {
return getDatabase(this).getAll();
}
async function getPackages() {
var packages = [];
var packageOwners = File.readDirectory("packages/");
for (var i = 0; i < packageOwners.length; i++) {
if (packageOwners[i].charAt(0) != ".") {
var packageNames = File.readDirectory("packages/" + packageOwners[i] + "/");
for (var j = 0; j < packageNames.length; j++) {
if (packageNames[j].charAt(0) != ".") {
packages.push({
owner: packageOwners[i],
name: packageNames[j],
manifest: await getManifest("packages/" + packageOwners[i] + "/" + packageNames[j] + "/" + packageNames[j] + ".js"),
});
}
}
}
}
return packages;
}
function getUser(caller, process) {
return {
name: process.userName,
@ -147,7 +117,9 @@ function getUsers(packageOwner, packageName) {
}
function postMessageInternal(from, to, message) {
return invoke(to.eventHandlers['onMessage'], [getUser(from, from), message]);
if (to.eventHandlers['message']) {
return invoke(to.eventHandlers['message'], [getUser(from, from), message]);
}
}
function killProcess(process) {
@ -156,47 +128,20 @@ function killProcess(process) {
}
}
async function getService(service, packageName) {
let process = this;
let serviceName = process.packageName + '_' + service;
let serviceProcess = await getServiceProcess(process.packageOwner, packageName || process.packageName, serviceName);
return serviceProcess.ready.then(function() {
return {
postMessage: postMessageInternal.bind(process, process, serviceProcess),
kill: killProcess.bind(process, serviceProcess),
}
});
}
function getSessionProcess(packageOwner, packageName, session, options) {
var actualOptions = {terminal: true, timeout: kPingInterval};
async function getSessionProcessBlob(blobId, session, options) {
var actualOptions = {timeout: kPingInterval};
if (options) {
for (var i in options) {
actualOptions[i] = options[i];
}
}
return getProcess(packageOwner, packageName, 'session_' + session, actualOptions);
}
function getServiceProcess(packageOwner, packageName, service, options) {
return getProcess(packageOwner, packageName, 'service_' + packageOwner + '_' + packageName + '_' + service, options || {});
}
function badName(name) {
var bad = false;
if (name) {
for (var i = 0; i < name.length; i++) {
if ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890-_".indexOf(name.charAt(i)) == -1) {
bad = true;
break;
}
}
}
return bad;
return getProcessBlob(blobId, 'session_' + session, actualOptions);
}
function readFileUtf8(fileName) {
return new TextDecoder("UTF-8").decode(File.readFile(fileName));
let data = File.readFile(fileName);
data = utf8Decode(data);
return data;
}
let gManifestCache = {};
@ -235,33 +180,23 @@ async function getManifest(fileName) {
return result;
}
async function getProcess(packageOwner, packageName, key, options) {
async function getProcessBlob(blobId, key, options) {
var process = gProcesses[key];
if (!process
&& !(options && "create" in options && !options.create)
&& !badName(packageOwner)
&& !badName(packageName)) {
&& !(options && "create" in options && !options.create)) {
try {
print("Creating task for " + packageName + " " + key);
var fileName = "packages/" + packageOwner + "/" + packageName + "/" + packageName + ".js";
var manifest = await getManifest(fileName);
print("Creating task for " + blobId + " " + key);
process = {};
process.key = key;
process.index = gProcessIndex++;
process.userName = options.userName || ('user' + process.index);
process.userName = 'user' + process.index;
process.credentials = options.credentials || {};
process.task = new Task();
process.eventHandlers = {};
process.packageOwner = packageOwner;
process.packageName = packageName;
if (options.terminal) {
process.terminal = new Terminal();
}
process.database = null;
process.app = new App();
process.lastActive = Date.now();
process.lastPing = null;
process.timeout = options.timeout;
process.connections = [];
var resolveReady;
var rejectReady;
process.ready = new Promise(function(resolve, reject) {
@ -271,24 +206,12 @@ async function getProcess(packageOwner, packageName, key, options) {
gProcesses[key] = process;
process.task.onExit = function(exitCode, terminationSignal) {
broadcastEvent('onSessionEnd', [getUser(process, process)]);
if (process.terminal) {
if (terminationSignal) {
process.terminal.print("Process terminated with signal " + terminationSignal + ".");
} else {
process.terminal.print("Process ended with exit code " + exitCode + ".");
}
}
for (let i = 0; i < process.connections.length; i++) {
process.connections[i].close();
}
process.connections.length = 0;
process.task = null;
delete gProcesses[key];
};
var imports = {
'core': {
'broadcast': broadcast.bind(process),
'getService': getService.bind(process),
'getPackages': getPackages.bind(process),
'getUsers': getUsers.bind(process),
'register': function(eventName, handler) {
if (!process.eventHandlers[eventName]) {
@ -297,7 +220,7 @@ async function getProcess(packageOwner, packageName, key, options) {
process.eventHandlers[eventName].push(handler);
},
'unregister': function(eventHandle, handler) {
if (process.eventHandlers(eventName)) {
if (process.eventHandlers[eventName]) {
let index = process.eventHandlers[eventName].indexOf(handler);
if (index != -1) {
process.eventHandlers[eventName].splice(index, 1);
@ -309,97 +232,62 @@ async function getProcess(packageOwner, packageName, key, options) {
},
'getUser': getUser.bind(null, process, process),
'user': getUser(process, process),
},
'database': {
'get': databaseGet.bind(process),
'set': databaseSet.bind(process),
'remove': databaseRemove.bind(process),
'getAll': databaseGetAll.bind(process),
},
}
};
if (options.terminal) {
imports.terminal = {
'print': process.terminal.print.bind(process.terminal),
'readLine': process.terminal.readLine.bind(process.terminal),
'setEcho': process.terminal.setEcho.bind(process.terminal),
'select': process.terminal.select.bind(process.terminal),
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.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)]));
};
if (options.terminalApi) {
for (let i in options.terminalApi) {
let api = options.terminalApi[i];
imports.terminal[api[0]] = process.terminal.makeFunction(api);
}
}
}
if (manifest
&& manifest.permissions
&& manifest.permissions.indexOf("administration") != -1) {
if (getPermissionsForUser(packageOwner).administration) {
imports.administration = {
'setGlobalSettings': setGlobalSettings.bind(process),
'getGlobalSettings': getGlobalSettings.bind(process),
'getStatistics': function() { return statistics; },
};
} else {
throw new Error(packageOwner + " does not have right to permission 'administration'.");
}
}
if (manifest
&& manifest.permissions
&& manifest.permissions.indexOf("network") != -1) {
if (getPermissionsForUser(packageOwner).network) {
imports.network = {
'newConnection': newConnection.bind(process),
};
} else {
throw new Error(packageOwner + " does not have right to permission 'network'.");
}
}
if (manifest && manifest.require) {
let source = {};
for (let i in manifest.require) {
let name = manifest.require[i];
source[name] = readFileUtf8("packages/" + process.packageOwner + "/" + name + "/" + name + ".js");
}
process.task.setRequires(source);
}
process.task.setImports(imports);
print("Activating task");
process.task.activate();
print("Executing task");
await process.task.execute({name: fileName, source: readFileUtf8(fileName)});
print("Task ready");
let source = await ssb.blobGet(blobId);
var appSource = utf8Decode(source);
try {
var app = JSON.parse(appSource);
if (app.type == "tildefriends-app") {
var id = app.files["app.js"];
var blob = await ssb.blobGet(id);
appSource = utf8Decode(blob);
await Promise.all(Object.keys(app.files).map(async function(f) {
await process.task.loadFile([f, await ssb.blobGet(app.files[f])]);
}));
}
} catch (e) {
printError({print: print}, e);
}
broadcastEvent('onSessionBegin', [getUser(process, process)]);
resolveReady(process);
if (process.terminal) {
process.terminal.print({action: "ready"});
if (process.app) {
process.app.send({action: "ready"});
}
await process.task.execute({name: blobId, source: appSource});
} catch (error) {
printError(process.terminal, error);
printError({print: print}, error);
rejectReady();
}
}
return process;
}
function updateProcesses(packageOwner, packageName) {
for (var i in gProcesses) {
var process = gProcesses[i];
if (process.packageOwner == packageOwner
&& process.packageName == packageName) {
try {
if (process.terminal) {
process.terminal.notifyUpdate();
} else {
process.task.kill();
}
} catch (error) {
print(error);
}
}
}
}
function makeDirectoryForFile(fileName) {
var parts = fileName.split("/");
var path = "";
@ -429,6 +317,147 @@ try {
print("Error loading settings from " + kGlobalSettingsFile + ": " + error);
}
var kStaticFiles = [
{uri: '/', path: 'index.html', type: 'text/html; charset=UTF-8'},
{uri: '/style.css', path: 'style.css', type: 'text/css; charset=UTF-8'},
{uri: '/favicon.png', path: 'favicon.png', type: 'image/png'},
{uri: '/client.js', path: 'client.js', type: 'text/javascript; charset=UTF-8'},
{uri: '/robots.txt', path: 'robots.txt', type: 'text/plain; charset=UTF-8'},
];
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 data = File.readFile("core/" + kStaticFiles[i].path);
response.writeHead(200, {"Content-Type": kStaticFiles[i].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");
}
function sendData(response, data) {
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, {"Content-Type": "image/jpeg", "Content-Length": data.byteLength});
response.end(data);
} else if (startsWithBytes(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
response.writeHead(200, {"Content-Type": "image/png", "Content-Length": data.byteLength});
response.end(data);
} else if (startsWithBytes(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) ||
startsWithBytes(data, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])) {
response.writeHead(200, {"Content-Type": "image/gif", "Content-Length": data.byteLength});
response.end(data);
} else {
response.writeHead(200, {"Content-Type": "text/javascript; charset=utf-8", "Content-Length": data.byteLength});
response.end(data);
}
} else {
response.writeHead(404, {"Content-Type": "text/plain; charset=utf-8", "Content-Length": "File not found".length});
response.end("File not found");
}
}
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 = 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(301, {"Location": blobId + "/"});
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) {
data = await ssb.blobGet(id);
if (match[3]) {
var app = JSON.parse(data);
data = app.files[match[3]];
}
}
} else {
data = await ssb.blobGet(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) {
response.writeHead(401, {"Content-Type": "text/plain; charset=utf-8"});
response.end("401 Unauthorized");
return;
}
var database = new Database(user);
await database.set('path:' + app, newBlobId);
}
response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8"});
response.end("/" + newBlobId);
} else {
var data;
if (match = /^\/\~(\w+)\/(\w+)$/.exec(blobId)) {
var db = new Database(match[1]);
var id = await db.get('path:' + match[2]);
if (id) {
data = utf8Decode(await ssb.blobGet(id));
var app = JSON.parse(data);
print(JSON.stringify(app));
data = app.files[uri.substring(1)];
data = await ssb.blobGet(data);
}
}
sendData(response, data);
}
}
}
ssb.onBroadcastsChanged = function() {
broadcastEvent('onBroadcastsChanged', []);
}
ssb.onConnectionsChanged = function() {
broadcastEvent('onConnectionsChanged', []);
}
var auth = require("auth");
var httpd = require("httpd");
httpd.all("/login", auth.handler);
@ -437,12 +466,20 @@ httpd.all("", function(request, response) {
if (request.uri === "/" || request.uri === "") {
response.writeHead(303, {"Location": gGlobalSettings.index, "Content-Length": "0"});
return response.end();
} else if (match = /^\/terminal(\/.*)/.exec(request.uri)) {
return terminal.handler(request, response, null, null, match[1]);
} else if (match = /^\/\~([^\/]+)\/([^\/]+)(.*)/.exec(request.uri)) {
return terminal.handler(request, response, match[1], match[2], match[3]);
} else if (match = /^(\/~[^\/]+\/[^\/]+)(\/?.*)$/.exec(request.uri)) {
return blobHandler(request, response, match[1], match[2]);
} else if (match = /^\/(&[^\.]*\.\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 = /^(.*)(\/save)$/.exec(request.uri)) {
return blobHandler(request, response, match[1], match[2]);
} else if (match = /^\/trace$/.exec(request.uri)) {
var data = trace();
response.writeHead(404, {"Content-Type": "application/json; charset=utf-8", "Content-Length": data.length.toString()});
return response.end(data);
} else if (request.uri == "/robots.txt") {
return terminal.handler(request, response, null, null, request.uri);
return blobHandler(request, response, null, request.uri);
} else if ((match = /^\/.well-known\/(.*)/.exec(request.uri)) && request.uri.indexOf("..") == -1) {
var data = File.readFile("data/global/.well-known/" + match[1]);
if (data) {
@ -458,4 +495,4 @@ httpd.all("", function(request, response) {
return response.end(data);
}
});
httpd.registerSocketHandler("/terminal/socket", terminal.socket);
httpd.registerSocketHandler("/app/socket", app.socket);