tildefriends/core/core.js

428 lines
12 KiB
JavaScript

"use strict";
var terminal = require("terminal");
var auth = require("auth");
var network = require("network");
var gProcessIndex = 0;
var gProcesses = {};
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() {
var id = "";
for (var i = 0; i < 64; i++) {
id += (Math.floor(Math.random() * 16)).toString(16);
}
return id;
}
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();
}
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]});
}
}
}
}
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 ping() {
var process = this;
var now = Date.now();
var again = true;
if (now - process.lastActive < process.timeout) {
// Active.
} else if (process.lastPing > process.lastActive) {
// We lost them.
process.task.kill();
again = false;
} else {
// Idle. Ping them.
process.terminal.ping();
process.lastPing = now;
}
if (again) {
setTimeout(ping.bind(process), process.timeout);
}
}
function postMessageInternal(from, to, message) {
return invoke(to.eventHandlers['onMessage'], [getUser(from, from), message]);
}
function getService(service) {
var process = this;
var serviceProcess = getServiceProcess(process.packageOwner, process.packageName, service);
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 getManifest(fileName) {
var manifest = [];
var lines = File.readFile(fileName).split("\n").map(x => x.trimRight());
for (var i = 0; i < lines.length; i++) {
if (lines[i].substring(0, 4) == "//! ") {
manifest.push(lines[i].substring(4));
}
}
return manifest.length ? JSON.parse(manifest.join("\n")) : null;
}
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 = 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];
};
if (process.timeout > 0) {
setTimeout(ping.bind(process), process.timeout);
}
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),
'clear': process.terminal.clear.bind(process.terminal),
'readLine': process.terminal.readLine.bind(process.terminal),
'notify': process.terminal.notify.bind(process.terminal),
'setEcho': process.terminal.setEcho.bind(process.terminal),
'setTitle': process.terminal.setTitle.bind(process.terminal),
'setPrompt': process.terminal.setPrompt.bind(process.terminal),
'setPassword': process.terminal.setPassword.bind(process.terminal),
'setHash': process.terminal.setHash.bind(process.terminal),
'split': process.terminal.split.bind(process.terminal),
'select': process.terminal.select.bind(process.terminal),
'postMessageToIframe': process.terminal.postMessageToIframe.bind(process.terminal),
'cork': process.terminal.cork.bind(process.terminal),
'uncork': process.terminal.uncork.bind(process.terminal),
};
}
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'.");
}
}
process.task.setImports(imports);
print("Activating task");
process.task.activate();
print("Executing task");
process.task.execute(fileName).then(function() {
print("Task ready");
broadcastEvent('onSessionBegin', [getUser(process, process)]);
resolveReady(process);
}).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(File.readFile(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 {
var data = "File not found.";
response.writeHead(404, {"Content-Type": "text/plain; charset=utf-8", "Content-Length": data.length.toString()});
return response.end(data);
}
});