2016-03-12 13:50:43 -05:00
|
|
|
"use strict";
|
|
|
|
|
|
|
|
var kStaticFiles = [
|
2016-12-22 12:38:21 -05:00
|
|
|
{uri: '', path: 'index.html', type: 'text/html; charset=UTF-8'},
|
|
|
|
{uri: '/edit', path: 'edit.html', type: 'text/html; charset=UTF-8'},
|
|
|
|
{uri: '/style.css', path: 'style.css', type: 'text/css; charset=UTF-8'},
|
2016-03-12 13:50:43 -05:00
|
|
|
{uri: '/favicon.png', path: 'favicon.png', type: 'image/png'},
|
2016-12-22 12:38:21 -05:00
|
|
|
{uri: '/client.js', path: 'client.js', type: 'text/javascript; charset=UTF-8'},
|
|
|
|
{uri: '/editor.js', path: 'editor.js', type: 'text/javascript; charset=UTF-8'},
|
2016-03-12 13:50:43 -05:00
|
|
|
{uri: '/agplv3-88x31.png', path: 'agplv3-88x31.png', type: 'image/png'},
|
2016-12-22 12:38:21 -05:00
|
|
|
{uri: '/robots.txt', path: 'robots.txt', type: 'text/plain; charset=UTF-8'},
|
2016-03-12 13:50:43 -05:00
|
|
|
];
|
|
|
|
|
|
|
|
var auth = require('auth');
|
|
|
|
var form = require('form');
|
|
|
|
|
|
|
|
function Terminal() {
|
|
|
|
this._index = 0;
|
|
|
|
this._firstLine = 0;
|
2016-04-10 20:09:21 -04:00
|
|
|
this._sentIndex = -1;
|
2016-03-12 13:50:43 -05:00
|
|
|
this._lines = [];
|
|
|
|
this._lastRead = null;
|
|
|
|
this._lastWrite = null;
|
|
|
|
this._echo = true;
|
|
|
|
this._readLine = null;
|
|
|
|
this._selected = null;
|
2016-03-20 09:29:20 -04:00
|
|
|
this._corked = 0;
|
2016-04-10 20:09:21 -04:00
|
|
|
this._onOutput = null;
|
2016-03-12 13:50:43 -05:00
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
Terminal.kBacklog = 64;
|
|
|
|
|
2016-04-10 20:09:21 -04:00
|
|
|
Terminal.prototype.readOutput = function(callback) {
|
|
|
|
this._onOutput = callback;
|
|
|
|
this.dispatch();
|
2016-03-12 13:50:43 -05:00
|
|
|
}
|
|
|
|
|
2016-04-10 20:09:21 -04:00
|
|
|
Terminal.prototype.dispatch = function(data) {
|
|
|
|
var payload = this._lines.slice(Math.max(0, this._sentIndex + 1 - this._firstLine));
|
2016-03-20 09:29:20 -04:00
|
|
|
if (data) {
|
|
|
|
payload.push(data);
|
|
|
|
}
|
2016-04-10 20:09:21 -04:00
|
|
|
if (this._onOutput && (this._sentIndex < this._index - 1 || data)) {
|
|
|
|
this._sentIndex = this._index - 1;
|
|
|
|
this._onOutput({lines: payload});
|
2016-03-20 09:29:20 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-04-10 20:09:21 -04:00
|
|
|
Terminal.prototype.feedWaiting = function(waiting, data) {
|
|
|
|
}
|
|
|
|
|
2016-03-12 13:50:43 -05:00
|
|
|
Terminal.prototype.print = function() {
|
|
|
|
var data = arguments;
|
|
|
|
if (this._selected) {
|
|
|
|
data = {
|
|
|
|
terminal: this._selected,
|
|
|
|
value: data
|
|
|
|
};
|
|
|
|
}
|
|
|
|
this._lines.push(data);
|
|
|
|
this._index++;
|
|
|
|
if (this._lines.length >= Terminal.kBacklog * 2) {
|
|
|
|
this._firstLine = this._index - Terminal.kBacklog;
|
|
|
|
this._lines = this._lines.slice(this._lines.length - Terminal.kBacklog);
|
|
|
|
}
|
2016-03-20 09:29:20 -04:00
|
|
|
if (this._corked == 0) {
|
|
|
|
this.dispatch();
|
|
|
|
}
|
2016-03-12 13:50:43 -05:00
|
|
|
this._lastWrite = new Date();
|
|
|
|
}
|
|
|
|
|
|
|
|
Terminal.prototype.notifyUpdate = function() {
|
|
|
|
this.print({action: "update"});
|
|
|
|
}
|
|
|
|
|
|
|
|
Terminal.prototype.select = function(name) {
|
|
|
|
this._selected = name;
|
|
|
|
}
|
|
|
|
|
|
|
|
Terminal.prototype.ping = function() {
|
2016-04-03 11:01:21 -04:00
|
|
|
this.dispatch({action: "ping"});
|
2016-03-12 13:50:43 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
Terminal.prototype.setEcho = function(echo) {
|
|
|
|
this._echo = echo;
|
|
|
|
}
|
|
|
|
|
|
|
|
Terminal.prototype.readLine = function() {
|
|
|
|
var self = this;
|
|
|
|
if (self._readLine) {
|
|
|
|
self._readLine[1]();
|
|
|
|
}
|
|
|
|
return new Promise(function(resolve, reject) {
|
|
|
|
self._readLine = [resolve, reject];
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-03-20 09:29:20 -04:00
|
|
|
Terminal.prototype.cork = function() {
|
|
|
|
this._corked++;
|
|
|
|
}
|
|
|
|
|
|
|
|
Terminal.prototype.uncork = function() {
|
|
|
|
if (--this._corked == 0) {
|
2016-04-10 20:09:21 -04:00
|
|
|
this.dispatch();
|
2016-03-20 09:29:20 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-04-30 06:50:43 -04:00
|
|
|
Terminal.prototype.makeFunction = function(api) {
|
|
|
|
let self = this;
|
|
|
|
return function() {
|
|
|
|
let message = {action: api[0]};
|
|
|
|
for (let i = 1; i < api.length; i++) {
|
|
|
|
message[api[i]] = arguments[i - 1];
|
|
|
|
}
|
|
|
|
self.print(message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-03-12 13:50:43 -05:00
|
|
|
function invoke(handlers, argv) {
|
|
|
|
var promises = [];
|
|
|
|
if (handlers) {
|
|
|
|
for (var i = 0; i < handlers.length; ++i) {
|
2016-06-12 09:45:57 -04:00
|
|
|
try {
|
|
|
|
promises.push(handlers[i].apply({}, argv));
|
|
|
|
} catch (error) {
|
|
|
|
handlers.splice(i, 1);
|
|
|
|
i--;
|
|
|
|
promises.push(new Promise(function(resolve, reject) { reject(error); }));
|
|
|
|
}
|
2016-03-12 13:50:43 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return Promise.all(promises);
|
|
|
|
}
|
|
|
|
|
2016-04-10 20:09:21 -04:00
|
|
|
function socket(request, response, client) {
|
|
|
|
var process;
|
|
|
|
|
|
|
|
var options = {};
|
|
|
|
var credentials = auth.query(request.headers);
|
|
|
|
if (credentials && credentials.session) {
|
|
|
|
options.userName = credentials.session.name;
|
|
|
|
}
|
|
|
|
options.credentials = credentials;
|
|
|
|
|
|
|
|
response.onMessage = function(event) {
|
|
|
|
if (event.opCode == 0x1 || event.opCode == 0x2) {
|
|
|
|
var message;
|
|
|
|
try {
|
|
|
|
message = JSON.parse(event.data);
|
|
|
|
} catch (error) {
|
|
|
|
print("ERROR", error, event.data, event.data.length, event.opCode);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (message.action == "hello") {
|
|
|
|
var packageOwner;
|
|
|
|
var packageName;
|
|
|
|
var match;
|
|
|
|
if (match = /^\/\~([^\/]+)\/([^\/]+)(.*)/.exec(message.path)) {
|
|
|
|
packageOwner = match[1];
|
|
|
|
packageName = match[2];
|
|
|
|
}
|
2016-04-10 20:28:42 -04:00
|
|
|
var sessionId = makeSessionId();
|
|
|
|
response.send(JSON.stringify({lines: [{action: "session", sessionId: sessionId, credentials: credentials}]}), 0x1);
|
2016-04-10 20:09:21 -04:00
|
|
|
|
2016-04-30 06:50:43 -04:00
|
|
|
options.terminalApi = message.terminalApi || [];
|
2016-04-10 20:28:42 -04:00
|
|
|
process = getSessionProcess(packageOwner, packageName, sessionId, options);
|
2016-04-10 20:09:21 -04:00
|
|
|
process.terminal.readOutput(function(message) {
|
|
|
|
response.send(JSON.stringify(message), 0x1);
|
|
|
|
});
|
|
|
|
|
|
|
|
var ping = function() {
|
|
|
|
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.
|
|
|
|
response.send("", 0x9);
|
|
|
|
process.lastPing = now;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (again) {
|
|
|
|
setTimeout(ping, process.timeout);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (process.timeout > 0) {
|
|
|
|
setTimeout(ping, process.timeout);
|
|
|
|
}
|
|
|
|
} else if (message.action == "command") {
|
|
|
|
var command = message.command;
|
|
|
|
var eventName = 'unknown';
|
|
|
|
if (typeof command == "string") {
|
|
|
|
if (process.terminal._echo) {
|
|
|
|
process.terminal.print("> " + command);
|
|
|
|
}
|
|
|
|
if (process.terminal._readLine) {
|
|
|
|
let promise = process.terminal._readLine;
|
|
|
|
process.terminal._readLine = null;
|
|
|
|
promise[0](command);
|
|
|
|
}
|
|
|
|
eventName = 'onInput';
|
|
|
|
} else if (command.event) {
|
|
|
|
eventName = command.event;
|
|
|
|
}
|
|
|
|
return invoke(process.eventHandlers[eventName], [command]).catch(function(error) {
|
|
|
|
process.terminal.print(error);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} else if (event.opCode == 0x8) {
|
|
|
|
// Close.
|
|
|
|
process.task.kill();
|
|
|
|
response.send(event.data, 0x8);
|
|
|
|
} else if (event.opCode == 0xa) {
|
|
|
|
// PONG
|
|
|
|
}
|
|
|
|
|
|
|
|
if (process) {
|
|
|
|
process.lastActive = Date.now();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-03-12 13:50:43 -05:00
|
|
|
function handler(request, response, packageOwner, packageName, uri) {
|
|
|
|
var found = false;
|
|
|
|
|
|
|
|
if (badName(packageOwner) || badName(packageName)) {
|
|
|
|
var data = "File not found";
|
|
|
|
response.writeHead(404, {"Content-Type": "text/plain; charset=utf-8", "Content-Length": data.length});
|
|
|
|
response.end(data);
|
|
|
|
found = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!found) {
|
|
|
|
for (var i in kStaticFiles) {
|
|
|
|
if (uri === kStaticFiles[i].uri) {
|
|
|
|
found = true;
|
2016-12-22 12:38:21 -05:00
|
|
|
var data = new TextDecoder("UTF-8").decode(File.readFile("core/" + kStaticFiles[i].path));
|
2016-03-12 13:50:43 -05:00
|
|
|
if (kStaticFiles[i].uri == "") {
|
2016-04-03 15:31:03 -04:00
|
|
|
if (gGlobalSettings && gGlobalSettings['google-signin-client_id']) {
|
|
|
|
data = data.replace("<!--HEAD-->", `
|
|
|
|
<script src="https://apis.google.com/js/platform.js" async defer></script>
|
|
|
|
<meta name="google-signin-client_id" content="${gGlobalSettings['google-signin-client_id']}">`);
|
|
|
|
}
|
2016-03-12 13:50:43 -05:00
|
|
|
data = data.replace("$(VIEW_SOURCE)", "/~" + packageOwner + "/" + packageName + "/view");
|
|
|
|
data = data.replace("$(EDIT_SOURCE)", "/~" + packageOwner + "/" + packageName + "/edit");
|
|
|
|
} else if (kStaticFiles[i].uri == "/edit") {
|
2016-12-22 12:38:21 -05:00
|
|
|
var source = new TextDecoder("UTF-8").decode(File.readFile("packages/" + packageOwner + "/" + packageName + "/" + packageName + ".js")) || "";
|
2016-03-12 13:50:43 -05:00
|
|
|
source = source.replace(/([&<>"])/g, function(x, item) {
|
|
|
|
return {'&': '&', '"': '"', '<': '<', '>': '>'}[item];
|
|
|
|
});
|
|
|
|
data = data.replace("$(SOURCE)", source);
|
|
|
|
}
|
2016-12-22 12:38:21 -05:00
|
|
|
var raw = new TextEncoder("UTF-8").encode(data);
|
|
|
|
response.writeHead(200, {"Content-Type": kStaticFiles[i].type, "Content-Length": raw.length});
|
|
|
|
response.end(raw);
|
2016-03-12 13:50:43 -05:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!found) {
|
|
|
|
var process;
|
|
|
|
if (uri === "/view") {
|
2017-01-03 18:23:50 -05:00
|
|
|
var data = new TextDecoder("UTF-8").decode(File.readFile("packages/" + packageOwner + "/" + packageName + "/" + packageName + ".js"));
|
2016-03-12 13:50:43 -05:00
|
|
|
response.writeHead(200, {"Content-Type": "text/javascript; charset=utf-8", "Content-Length": data.length});
|
|
|
|
response.end(data);
|
|
|
|
} else if (uri == "/save") {
|
|
|
|
var credentials = auth.query(request.headers);
|
|
|
|
var userName = credentials && credentials.session && credentials.session.name ? credentials.session.name : "guest";
|
|
|
|
if (badName(packageName)) {
|
|
|
|
response.writeHead(403, {"Content-Type": "text/plain; charset=utf-8"});
|
|
|
|
response.end("Invalid package name: " + packageName);
|
|
|
|
} else if (badName(userName)) {
|
|
|
|
response.writeHead(403, {"Content-Type": "text/plain; charset=utf-8"});
|
|
|
|
response.end("Invalid user name: " + userName);
|
|
|
|
} else {
|
|
|
|
File.makeDirectory("packages/" + userName);
|
|
|
|
File.makeDirectory("packages/" + userName + "/" + packageName);
|
|
|
|
if (!File.writeFile("packages/" + userName + "/" + packageName + "/" + packageName + ".js", request.body || "")) {
|
|
|
|
response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8"});
|
|
|
|
response.end("/~" + userName + "/" + packageName);
|
|
|
|
updateProcesses(userName, packageName);
|
|
|
|
} else {
|
|
|
|
response.writeHead(500, {"Content-Type": "text/plain; charset=utf-8"});
|
|
|
|
response.end("Problem saving: " + packageName);
|
|
|
|
}
|
|
|
|
}
|
2016-04-10 20:09:21 -04:00
|
|
|
} else if (uri === "/submit") {
|
|
|
|
var process = getServiceProcess(packageOwner, packageName, "submit");
|
|
|
|
process.lastActive = Date.now();
|
2016-03-16 21:07:54 -04:00
|
|
|
return process.ready.then(function() {
|
|
|
|
var payload = form.decodeForm(request.body, form.decodeForm(request.query));
|
|
|
|
return invoke(process.eventHandlers['onSubmit'], [payload]).then(function() {
|
2016-03-16 20:51:08 -04:00
|
|
|
response.writeHead(200, {
|
|
|
|
"Content-Type": "text/plain; charset=utf-8",
|
|
|
|
"Content-Length": "0",
|
|
|
|
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
|
|
"Pragma": "no-cache",
|
|
|
|
"Expires": "0",
|
|
|
|
});
|
2016-03-16 21:07:54 -04:00
|
|
|
return response.end("");
|
2016-03-16 20:51:08 -04:00
|
|
|
});
|
2016-03-16 21:07:54 -04:00
|
|
|
});
|
2016-04-10 20:09:21 -04:00
|
|
|
} else if (uri === "/atom") {
|
|
|
|
var process = getServiceProcess(packageOwner, packageName, "atom");
|
|
|
|
process.lastActive = Date.now();
|
|
|
|
return process.ready.then(function() {
|
|
|
|
var payload = form.decodeForm(request.body, form.decodeForm(request.query));
|
|
|
|
return invoke(process.eventHandlers['onAtom'], [payload]).then(function(content) {
|
|
|
|
var atomContent = content.join();
|
2016-03-12 13:50:43 -05:00
|
|
|
response.writeHead(200, {
|
2016-04-10 20:09:21 -04:00
|
|
|
"Content-Type": "application/atom+xml; charset=utf-8",
|
|
|
|
"Content-Length": atomContent.length.toString(),
|
2016-03-12 13:50:43 -05:00
|
|
|
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
|
|
"Pragma": "no-cache",
|
|
|
|
"Expires": "0",
|
|
|
|
});
|
2016-04-10 20:09:21 -04:00
|
|
|
return response.end(atomContent);
|
|
|
|
});
|
|
|
|
});
|
2016-03-12 13:50:43 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
exports.handler = handler;
|
2016-04-10 20:09:21 -04:00
|
|
|
exports.socket = socket;
|