Cory McWilliams
8f56e754e2
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@3384 ed5197a5-7fde-0310-b194-c3ffbd925b24
464 lines
13 KiB
JavaScript
464 lines
13 KiB
JavaScript
"use strict";
|
|
|
|
require("encoding-indexes");
|
|
require("encoding");
|
|
|
|
var terminal = require("terminal");
|
|
var auth = require("auth");
|
|
var network = require("network");
|
|
|
|
var gProcessIndex = 0;
|
|
var gProcesses = {};
|
|
var gSessionIndex = 0;
|
|
|
|
var gGlobalSettings = {
|
|
index: "/~cory/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 broadcastEvent(eventName, argv) {
|
|
var promises = [];
|
|
for (var i in gProcesses) {
|
|
var process = gProcesses[i];
|
|
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 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,
|
|
key: process.key,
|
|
index: process.index,
|
|
packageOwner: process.packageOwner,
|
|
packageName: process.packageName,
|
|
credentials: process.credentials,
|
|
postMessage: postMessageInternal.bind(caller, caller, process),
|
|
};
|
|
}
|
|
|
|
function getUsers(packageOwner, packageName) {
|
|
var result = [];
|
|
for (var key in gProcesses) {
|
|
var process = gProcesses[key];
|
|
if ((!packageOwner || process.packageOwner == packageOwner)
|
|
&& (!packageName || process.packageName == packageName)) {
|
|
result.push(getUser(this, process));
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function postMessageInternal(from, to, message) {
|
|
return invoke(to.eventHandlers['onMessage'], [getUser(from, from), message]);
|
|
}
|
|
|
|
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),
|
|
}
|
|
});
|
|
}
|
|
|
|
function getSessionProcess(packageOwner, packageName, session, options) {
|
|
var actualOptions = {terminal: true, 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;
|
|
}
|
|
|
|
function readFileUtf8(fileName) {
|
|
return new TextDecoder("UTF-8").decode(File.readFile(fileName));
|
|
}
|
|
|
|
let gManifestCache = {};
|
|
|
|
async function getManifest(fileName) {
|
|
let oldEntry = gManifestCache[fileName];
|
|
let stat = await File.stat(fileName);
|
|
if (oldEntry) {
|
|
if (oldEntry.stat.mtime == stat.mtime && oldEntry.stat.size == stat.size) {
|
|
return oldEntry.manifest;
|
|
}
|
|
}
|
|
|
|
let manifest = [];
|
|
let lines = readFileUtf8(fileName).split("\n").map(x => x.trimRight());
|
|
for (let i = 0; i < lines.length; i++) {
|
|
if (lines[i].substring(0, 4) == "//! ") {
|
|
manifest.push(lines[i].substring(4));
|
|
}
|
|
}
|
|
let result;
|
|
try {
|
|
if (manifest.length) {
|
|
result = JSON.parse(manifest.join("\n"));
|
|
}
|
|
} catch (error) {
|
|
print("ERROR: getManifest(" + fileName + "): ", error);
|
|
// Oh well. No manifest.
|
|
}
|
|
|
|
gManifestCache[fileName] = {
|
|
stat: stat,
|
|
manifest: result,
|
|
};
|
|
|
|
return result;
|
|
}
|
|
|
|
function packageNameToPath(name) {
|
|
var process = this;
|
|
return "packages/" + process.packageOwner + "/" + name + "/";
|
|
}
|
|
|
|
async function getProcess(packageOwner, packageName, key, options) {
|
|
var process = gProcesses[key];
|
|
if (!process
|
|
&& !(options && "create" in options && !options.create)
|
|
&& !badName(packageOwner)
|
|
&& !badName(packageName)) {
|
|
try {
|
|
print("Creating task for " + packageName + " " + key);
|
|
var fileName = "packages/" + packageOwner + "/" + packageName + "/" + packageName + ".js";
|
|
var manifest = await getManifest(fileName);
|
|
process = {};
|
|
process.key = key;
|
|
process.index = gProcessIndex++;
|
|
process.userName = options.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.lastActive = Date.now();
|
|
process.lastPing = null;
|
|
process.timeout = options.timeout;
|
|
process.connections = [];
|
|
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)]);
|
|
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;
|
|
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]) {
|
|
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];
|
|
}
|
|
}
|
|
},
|
|
'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),
|
|
'cork': process.terminal.cork.bind(process.terminal),
|
|
'uncork': process.terminal.uncork.bind(process.terminal),
|
|
};
|
|
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");
|
|
process.task.execute({name: fileName, source: readFileUtf8(fileName)}).then(function() {
|
|
print("Task ready");
|
|
broadcastEvent('onSessionBegin', [getUser(process, process)]);
|
|
resolveReady(process);
|
|
if (process.terminal) {
|
|
process.terminal.print({action: "ready"});
|
|
}
|
|
}).catch(function(error) {
|
|
printError(process.terminal, error);
|
|
rejectReady();
|
|
});
|
|
} catch (error) {
|
|
printError(process.terminal, error);
|
|
rejectReady();
|
|
}
|
|
}
|
|
return process;
|
|
}
|
|
|
|
function updateProcesses(packageOwner, packageName) {
|
|
for (var i in gProcesses) {
|
|
var process = gProcesses[i];
|
|
if (process.packageOwner == packageOwner
|
|
&& process.packageName == packageName) {
|
|
if (process.terminal) {
|
|
process.terminal.notifyUpdate();
|
|
} else {
|
|
process.task.kill();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function makeDirectoryForFile(fileName) {
|
|
var parts = fileName.split("/");
|
|
var path = "";
|
|
for (var i = 0; i < parts.length - 1; i++) {
|
|
path += parts[i];
|
|
File.makeDirectory(path);
|
|
path += "/";
|
|
}
|
|
}
|
|
|
|
function getGlobalSettings() {
|
|
return gGlobalSettings;
|
|
}
|
|
|
|
function setGlobalSettings(settings) {
|
|
makeDirectoryForFile(kGlobalSettingsFile);
|
|
if (!File.writeFile(kGlobalSettingsFile, JSON.stringify(settings))) {
|
|
gGlobalSettings = settings;
|
|
} else {
|
|
throw new Error("Unable to save settings.");
|
|
}
|
|
}
|
|
|
|
try {
|
|
gGlobalSettings = JSON.parse(readFileUtf8(kGlobalSettingsFile));
|
|
} catch (error) {
|
|
print("Error loading settings from " + kGlobalSettingsFile + ": " + error);
|
|
}
|
|
|
|
var kIgnore = ["/favicon.ico"];
|
|
|
|
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": 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 (request.uri == "/robots.txt") {
|
|
return terminal.handler(request, response, null, 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) {
|
|
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");
|
|
}
|
|
} 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("/terminal/socket", terminal.socket);
|