1025 lines
26 KiB
JavaScript
1025 lines
26 KiB
JavaScript
/**
|
|
* @copyright Copyright (C) 2018, struktur AG
|
|
*
|
|
* @author Joachim Bauch <mail@joachim-bauch.de>
|
|
*
|
|
* @license AGPL-3.0
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
"use strict";
|
|
|
|
var process = require('process');
|
|
|
|
var config;
|
|
try {
|
|
config = require('./config');
|
|
} catch (e) {
|
|
if (e.code !== 'MODULE_NOT_FOUND') {
|
|
throw e;
|
|
}
|
|
|
|
console.log('Could not load "config.js":', e.code);
|
|
console.log('Please make sure to copy "config.js.in" to "config.js" and adjust to your environment.');
|
|
process.exit(1);
|
|
}
|
|
|
|
var port = config.port || 8080;
|
|
var secret = config.secret || '';
|
|
|
|
var child_process = require('child_process');
|
|
var fs = require('fs');
|
|
var path = require('path');
|
|
var https = require('https');
|
|
var http = require('http');
|
|
var jwt = require('jsonwebtoken');
|
|
var os = require('os');
|
|
var socketio = require('socket.io');
|
|
var querystring = require('querystring');
|
|
var url = require('url');
|
|
|
|
if (config.use_auto_ecdh_curve) {
|
|
// See https://github.com/nodejs/node/issues/21513#issuecomment-399790415
|
|
require("tls").DEFAULT_ECDH_CURVE = "auto";
|
|
}
|
|
|
|
if (config.allow_invalid_certificates) {
|
|
console.log('WARNING: Invalid certificates are allowed!');
|
|
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0;
|
|
}
|
|
|
|
var CMD_PDFTK = config.cmd_pdftk || 'pdftk';
|
|
console.log('Using', CMD_PDFTK, 'to run as pdftk');
|
|
var CMD_SVG2PDF = config.cmd_svg2pdf || 'svg2pdf';
|
|
console.log('Using', CMD_SVG2PDF, 'to run as svg2pdf');
|
|
|
|
var PERMISSION_CREATE = 4;
|
|
var PERMISSION_READ = 1;
|
|
var PERMISSION_UPDATE = 2;
|
|
var PERMISSION_DELETE = 8;
|
|
var PERMISSION_SHARE = 16;
|
|
var PERMISSION_ALL = 31;
|
|
|
|
function isEmpty(obj) {
|
|
return Object.keys(obj).length === 0 && obj.constructor === Object;
|
|
}
|
|
|
|
function parse_url(s) {
|
|
if (url.hasOwnProperty('URL')) {
|
|
return new url.URL(s);
|
|
} else {
|
|
return url.parse(s);
|
|
}
|
|
}
|
|
|
|
function performOcsRequest(token, request_url, body, method) {
|
|
return new Promise(function(resolve, reject) {
|
|
var u = parse_url(request_url);
|
|
var headers = {
|
|
"Accept": "application/json",
|
|
"Authorization": "Bearer " + token,
|
|
"OCS-APIRequest": "true"
|
|
};
|
|
var options = {
|
|
"hostname": u.hostname,
|
|
"port": u.port,
|
|
"path": u.pathname,
|
|
"headers": headers
|
|
};
|
|
if (body) {
|
|
options.method = method ? method : "POST";
|
|
if (typeof(body) !== "string") {
|
|
body = querystring.stringify(body);
|
|
}
|
|
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
// "Transfer-Encoding: chunked" doesn't seem to work.
|
|
headers["Content-Length"] = body.length;
|
|
} else {
|
|
options.method = method ? method : "GET";
|
|
}
|
|
|
|
var req = https.request(options, function(response) {
|
|
var data = [];
|
|
response.on('data', function(chunk) {
|
|
data.push(chunk);
|
|
});
|
|
|
|
response.on('end', function() {
|
|
if (data.length) {
|
|
data = Buffer.concat(data).toString("utf-8");
|
|
}
|
|
|
|
var ct = response.headers['content-type'];
|
|
if (ct && ct.indexOf("application/json") === 0) {
|
|
data = JSON.parse(data);
|
|
}
|
|
|
|
if (response.statusCode !== 200) {
|
|
return reject({
|
|
"code": response.statusCode,
|
|
"data": data,
|
|
"headers": response.headers
|
|
});
|
|
}
|
|
|
|
if (!data || typeof(data) === "string" || !data.hasOwnProperty("ocs") ||
|
|
!data.ocs.hasOwnProperty("meta") || !data.ocs.hasOwnProperty("data")) {
|
|
return reject({
|
|
"code": response.statusCode,
|
|
"data": data,
|
|
"headers": response.headers,
|
|
"msgid": "invalid_response"
|
|
});
|
|
}
|
|
|
|
var meta = data.ocs.meta;
|
|
if (meta.status !== "ok" || meta.statuscode !== 200) {
|
|
return reject({
|
|
"code": meta.statusCode,
|
|
"data": data,
|
|
"headers": response.headers
|
|
});
|
|
}
|
|
|
|
resolve(data.ocs.data);
|
|
});
|
|
});
|
|
|
|
req.on("error", function(error) {
|
|
reject({
|
|
"error": error
|
|
});
|
|
});
|
|
if (body) {
|
|
req.write(body);
|
|
}
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
var Room = function(io, id, base_url) {
|
|
this.io = io;
|
|
this.room_io = io.to(id);
|
|
this.id = id;
|
|
this.base_url = base_url;
|
|
this.ocs_url = base_url + '/ocs/v2.php/apps/pdfdraw';
|
|
this.users = {};
|
|
this.items = [];
|
|
this.currentPage = null;
|
|
this.loadItems();
|
|
};
|
|
|
|
Room.prototype.isEmpty = function() {
|
|
return isEmpty(this.users);
|
|
};
|
|
|
|
Room.prototype.createToken = function() {
|
|
var token = jwt.sign({
|
|
iss: 'backend',
|
|
exp: Math.floor(Date.now() / 1000) + 300, // 5 minutes
|
|
file: this.id,
|
|
}, secret, {'algorithm': 'HS256'});
|
|
return token;
|
|
};
|
|
|
|
Room.prototype.loadItems = function() {
|
|
var request_url = this.ocs_url + "/api/v1/item/" + this.id;
|
|
var token = this.createToken();
|
|
performOcsRequest(token, request_url).then(function(items) {
|
|
console.log("Received items", items);
|
|
for (var i = 0; i < items.length; i++) {
|
|
var item = items[i];
|
|
this._addItem(item.name, item.page, item.data);
|
|
}
|
|
this.sendItems();
|
|
}.bind(this), function(error) {
|
|
console.log("Error receiving items", error);
|
|
}.bind(this));
|
|
};
|
|
|
|
Room.prototype._storeItem = function(page, name, data) {
|
|
var request_url = this.ocs_url + "/api/v1/item/" + this.id + "/" + page + "/" + encodeURIComponent(name);
|
|
var body = {
|
|
"data": data
|
|
};
|
|
var token = this.createToken();
|
|
performOcsRequest(token, request_url, body).then(function() {
|
|
console.log("Saved item", name);
|
|
}.bind(this), function(error) {
|
|
console.log("Error saving item", name, error);
|
|
}.bind(this));
|
|
};
|
|
|
|
Room.prototype._removeItem = function(page, name) {
|
|
var request_url = this.ocs_url + "/api/v1/item/" + this.id + "/" + page + "/" + encodeURIComponent(name);
|
|
var token = this.createToken();
|
|
performOcsRequest(token, request_url, null, "DELETE").then(function() {
|
|
console.log("Removed item", name);
|
|
}.bind(this), function(error) {
|
|
console.log("Error removing item", name, error);
|
|
}.bind(this));
|
|
};
|
|
|
|
Room.prototype.send = function(socket, type, message) {
|
|
socket.to(this.id).emit(type, message);
|
|
};
|
|
|
|
Room.prototype.sendMessage = function(socket, message) {
|
|
this.send(socket, 'message', message);
|
|
};
|
|
|
|
Room.prototype._addItem = function(name, page, data) {
|
|
var items;
|
|
if (!this.items.hasOwnProperty(page)) {
|
|
items = this.items[page] = {};
|
|
} else {
|
|
items = this.items[page];
|
|
}
|
|
items[name] = data;
|
|
};
|
|
|
|
Room.prototype.addItem = function(socket, name, page, data) {
|
|
if (!this.userMayModify(socket)) {
|
|
return false;
|
|
}
|
|
|
|
this._addItem(name, page, data);
|
|
this._storeItem(page, name, data);
|
|
return true;
|
|
};
|
|
|
|
Room.prototype.removeItem = function(socket, name, page) {
|
|
if (!this.userMayModify(socket)) {
|
|
return false;
|
|
}
|
|
|
|
var items;
|
|
if (!this.items.hasOwnProperty(page)) {
|
|
return true;
|
|
}
|
|
|
|
items = this.items[page];
|
|
delete items[name];
|
|
this._removeItem(page, name);
|
|
if (isEmpty(items)) {
|
|
delete this.items[page];
|
|
}
|
|
return true;
|
|
};
|
|
|
|
Room.prototype.sendItem = function(socket, name, page, data) {
|
|
var message = {
|
|
'type': 'item',
|
|
'item': {
|
|
'page': page,
|
|
'name': name,
|
|
'data': data
|
|
}
|
|
};
|
|
socket.emit("message", message);
|
|
};
|
|
|
|
Room.prototype.sendItems = function(socket) {
|
|
for (var page in this.items) {
|
|
if (!this.items.hasOwnProperty(page)) {
|
|
continue;
|
|
}
|
|
|
|
var items = this.items[page];
|
|
for (var name in items) {
|
|
if (!items.hasOwnProperty(name)) {
|
|
continue;
|
|
}
|
|
|
|
if (!socket) {
|
|
for (var userid in this.users) {
|
|
if (!this.users.hasOwnProperty(userid)) {
|
|
continue;
|
|
}
|
|
this.sendItem(this.io.to(userid), name, page, items[name]);
|
|
}
|
|
} else {
|
|
this.sendItem(socket, name, page, items[name]);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
Room.prototype.handleControl = function(socket, message) {
|
|
if (!this.userMayModify(socket)) {
|
|
return false;
|
|
}
|
|
|
|
var control = message.control;
|
|
if (!control || !control.type) {
|
|
console.log('ignore invalid control message', message);
|
|
return false;
|
|
}
|
|
|
|
switch (control.type) {
|
|
case 'page':
|
|
var page = control.page;
|
|
console.log('room switched page', this.id, page);
|
|
this.currentPage = page;
|
|
break;
|
|
default:
|
|
console.log('ignore unsupported control message', message);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
Room.prototype.addUser = function(socket, data) {
|
|
data.userid = socket.id;
|
|
console.log('user joined room', this.id, data);
|
|
this.send(socket, 'user.joined', [data]);
|
|
this.users[socket.id] = data;
|
|
var all_users = [];
|
|
for (var userid in this.users) {
|
|
if (!this.users.hasOwnProperty(userid)) {
|
|
continue;
|
|
}
|
|
all_users.push(this.users[userid]);
|
|
}
|
|
socket.emit('user.joined', all_users);
|
|
this.sendItems(socket);
|
|
if (this.currentPage) {
|
|
socket.emit("message", {
|
|
'type': 'control',
|
|
'control': {
|
|
'type': 'page',
|
|
'page': this.currentPage
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
Room.prototype.removeUser = function(socket) {
|
|
if (!this.users.hasOwnProperty(socket.id)) {
|
|
return;
|
|
}
|
|
|
|
var data = this.users[socket.id];
|
|
console.log('user left room', this.id, data);
|
|
delete this.users[socket.id];
|
|
this.send(socket, 'user.left', data);
|
|
};
|
|
|
|
Room.prototype.userMayModify = function(socket) {
|
|
if (!this.users.hasOwnProperty(socket.id)) {
|
|
return false;
|
|
}
|
|
|
|
var userdata = this.users[socket.id];
|
|
if (userdata.permissions == null) {
|
|
// Old-style user with an old token.
|
|
return false;
|
|
}
|
|
|
|
return (userdata.permissions & PERMISSION_UPDATE) === PERMISSION_UPDATE;
|
|
};
|
|
|
|
Room.prototype.downloadFile = function(token) {
|
|
return new Promise(function(resolve, reject) {
|
|
var u = parse_url(this.base_url + '/apps/pdfdraw/download/' + this.id);
|
|
var options = {
|
|
"hostname": u.hostname,
|
|
"port": u.port,
|
|
"method": "GET",
|
|
"path": u.pathname,
|
|
"headers": {
|
|
"Authorization": "Bearer " + token,
|
|
}
|
|
};
|
|
|
|
var req = https.request(options, function(response) {
|
|
var data = [];
|
|
response.on('data', function(chunk) {
|
|
data.push(chunk);
|
|
});
|
|
|
|
response.on('end', function() {
|
|
if (response.statusCode !== 200) {
|
|
return reject({
|
|
"code": response.statusCode,
|
|
"data": data,
|
|
"headers": response.headers
|
|
});
|
|
}
|
|
|
|
resolve(Buffer.concat(data));
|
|
});
|
|
});
|
|
|
|
req.on("error", function(error) {
|
|
reject({
|
|
"error": error
|
|
});
|
|
});
|
|
req.end();
|
|
}.bind(this));
|
|
};
|
|
|
|
var rooms = {};
|
|
|
|
function svg2pdf(svg_filename) {
|
|
return new Promise(function(resolve, reject) {
|
|
var cmd = CMD_SVG2PDF;
|
|
console.log("Converting", svg_filename, "to pdf ...");
|
|
var options = {};
|
|
child_process.execFile(cmd, [svg_filename], options, function(error, stdout, stderr) {
|
|
if (error) {
|
|
console.log("Converting", svg_filename, "returned an error", error);
|
|
if (stderr) {
|
|
console.log(CMD_SVG2PDF, "returned an error", error.message, stderr.toString());
|
|
} else {
|
|
console.log(CMD_SVG2PDF, "returned an error", error.message);
|
|
}
|
|
reject(error);
|
|
return;
|
|
}
|
|
|
|
var pdf_filename = svg_filename.substr(0, svg_filename.length - 4) + ".pdf";
|
|
if (!fs.existsSync(pdf_filename)) {
|
|
reject(new Error("Converting " + svg_filename + " didn't produce a file"));
|
|
return;
|
|
}
|
|
|
|
console.log("Finished converting", svg_filename);
|
|
resolve(pdf_filename);
|
|
});
|
|
});
|
|
}
|
|
|
|
function spawn(cmd, args, options) {
|
|
return new Promise((resolve, reject) => {
|
|
let stdin;
|
|
if (options && options.input) {
|
|
stdin = options.input;
|
|
delete options.input;
|
|
}
|
|
|
|
const proc = child_process.spawn(cmd, args, options);
|
|
if (stdin) {
|
|
proc.stdin.write(stdin, (error) => {
|
|
proc.stdin.end();
|
|
});
|
|
}
|
|
|
|
let data;
|
|
let errorData;
|
|
|
|
proc.on("message", console.log);
|
|
|
|
proc.stdout.on("data", chunk => {
|
|
if (!data) {
|
|
data = chunk;
|
|
} else {
|
|
data = Buffer.concat([data, chunk]);
|
|
}
|
|
});
|
|
|
|
proc.stderr.on("data", chunk => {
|
|
if (!errorData) {
|
|
errorData = chunk;
|
|
} else {
|
|
errorData = Buffer.concat([errorData, chunk]);
|
|
}
|
|
});
|
|
|
|
proc.on("close", function(code) {
|
|
resolve({
|
|
"status": code,
|
|
"stdout": data,
|
|
"stderr": errorData,
|
|
});
|
|
});
|
|
|
|
proc.on("error", function(err) {
|
|
reject(err);
|
|
});
|
|
});
|
|
}
|
|
|
|
async function combine_pdfs(pages, filename) {
|
|
if (!pages.length) {
|
|
return false;
|
|
}
|
|
|
|
var cmd = CMD_PDFTK;
|
|
console.log("Combining pages ...");
|
|
var args = pages.splice(0);
|
|
args.push("cat");
|
|
args.push("output");
|
|
args.push(filename);
|
|
var status = await spawn(cmd, args);
|
|
console.log("Done", status.status);
|
|
if (status.status !== 0) {
|
|
if (status.stderr) {
|
|
console.log(CMD_PDFTK, "returned an error when combining", status.stderr.toString());
|
|
} else {
|
|
console.log(CMD_PDFTK, "returned an error when combining", status);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// TODO(jojo): Check if file has been created and is non-empty.
|
|
return true;
|
|
}
|
|
|
|
async function overlay_pdf(source_pdf, overlay_filename) {
|
|
if (!overlay_filename) {
|
|
return source_pdf;
|
|
}
|
|
|
|
var cmd = CMD_PDFTK;
|
|
console.log("Merging combined with overlay ...");
|
|
var args = ["-", "multistamp", overlay_filename, "output", "-"];
|
|
var status = await spawn(cmd, args, {
|
|
"input": source_pdf,
|
|
});
|
|
console.log("Done", status.status);
|
|
if (status.status !== 0) {
|
|
if (status.stderr) {
|
|
console.log(CMD_PDFTK, "returned an error when merging", status.stderr.toString());
|
|
} else {
|
|
console.log(CMD_PDFTK, "returned an error when merging", status);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
var merged = status.stdout;
|
|
if (!merged.length) {
|
|
console.log(CMD_PDFTK, "didn't return a PDF when merging");
|
|
return null;
|
|
}
|
|
return merged;
|
|
}
|
|
|
|
async function add_text_annotations(source_pdf, text, tempdir) {
|
|
if (!text || !text.length) {
|
|
return source_pdf;
|
|
}
|
|
|
|
var cmd = __dirname + "/pdfannotate";
|
|
console.log("Adding " + text.length + " text annotations ...");
|
|
|
|
var text_filename = tempdir + "/text-annotations.json";
|
|
fs.writeFileSync(text_filename, JSON.stringify(text));
|
|
|
|
var args = ["--text", text_filename, "-", "-"];
|
|
var status = await spawn(cmd, args, {
|
|
"input": source_pdf,
|
|
});
|
|
console.log("Done", status.status);
|
|
if (status.status !== 0) {
|
|
if (status.stderr) {
|
|
console.log("Adding text annotations returned an error", status.stderr.toString());
|
|
} else {
|
|
console.log("Adding text annotations returned an error", status);
|
|
}
|
|
return source_pdf;
|
|
}
|
|
|
|
var annotated = status.stdout;
|
|
if (!annotated.length) {
|
|
console.log("Adding text annotations didn't return a PDF");
|
|
return source_pdf;
|
|
}
|
|
|
|
return annotated;
|
|
}
|
|
|
|
// mktemp code taken from https://github.com/sasaplus1/mktemp/ (MIT license)
|
|
/**
|
|
* random table string and table length.
|
|
*/
|
|
var TABLE = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
var TABLE_LEN = TABLE.length;
|
|
|
|
/**
|
|
* generate random string from template.
|
|
*
|
|
* replace for placeholder "X" in template.
|
|
* return template if not has placeholder.
|
|
*
|
|
* @param {String} template template string.
|
|
* @throws {TypeError} if template is not a String.
|
|
* @return {String} replaced string.
|
|
*/
|
|
function generate_randomstring(template) {
|
|
var match, i, len, result;
|
|
|
|
if (typeof template !== 'string') {
|
|
throw new TypeError('template must be a String: ' + template);
|
|
}
|
|
|
|
match = template.match(/(X+)[^X]*$/);
|
|
|
|
// return template if not has placeholder
|
|
if (match === null) {
|
|
return template;
|
|
}
|
|
|
|
// generate random string
|
|
for (result = '', i = 0, len = match[1].length; i < len; ++i) {
|
|
result += TABLE[Math.floor(Math.random() * TABLE_LEN)];
|
|
}
|
|
|
|
// concat template and random string
|
|
return template.slice(0, match.index) + result +
|
|
template.slice(match.index + result.length);
|
|
}
|
|
|
|
/**
|
|
* sync version createDir.
|
|
*
|
|
* @param {String} template template string for dirname.
|
|
* @return {String} created dirname.
|
|
*/
|
|
function createDirSync(template) {
|
|
var isExist, dirname;
|
|
|
|
// FIXME: infinite loop
|
|
do {
|
|
isExist = false;
|
|
dirname = generate_randomstring(template);
|
|
try {
|
|
fs.mkdirSync(dirname, 448 /*=0700*/);
|
|
} catch (e) {
|
|
if (e.code === 'EEXIST') {
|
|
isExist = true;
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
} while (isExist);
|
|
|
|
return dirname;
|
|
}
|
|
|
|
// From https://stackoverflow.com/a/32197381
|
|
var deleteFolderRecursive = function(path) {
|
|
if (fs.existsSync(path)) {
|
|
fs.readdirSync(path).forEach(function(file, index) {
|
|
var curPath = path + "/" + file;
|
|
if (fs.lstatSync(curPath).isDirectory()) { // recurse
|
|
deleteFolderRecursive(curPath);
|
|
} else { // delete file
|
|
fs.unlinkSync(curPath);
|
|
}
|
|
});
|
|
fs.rmdirSync(path);
|
|
}
|
|
};
|
|
|
|
var runSvg2PdfCombiner = function(tempdir, data) {
|
|
return new Promise(function(resolve, reject) {
|
|
var promises = [];
|
|
var svg_cache = {};
|
|
for (var i = 0; i < data.svg.length; i++) {
|
|
var svg = data.svg[i];
|
|
if (svg_cache.hasOwnProperty(svg)) {
|
|
promises.push(svg_cache[svg]);
|
|
continue;
|
|
}
|
|
|
|
var svg_filename = tempdir + "/page-" + i + ".svg";
|
|
fs.writeFileSync(svg_filename, svg);
|
|
var promise = svg2pdf(svg_filename);
|
|
svg_cache[svg] = promise;
|
|
promises.push(promise);
|
|
}
|
|
|
|
Promise.all(promises)
|
|
.then(function(pdfs) {
|
|
var combined_filename = tempdir + "/combined.pdf";
|
|
combine_pdfs(pdfs, combined_filename)
|
|
.then((status) => {
|
|
if (!status) {
|
|
reject();
|
|
return;
|
|
}
|
|
resolve(combined_filename);
|
|
});
|
|
})
|
|
.catch(function(error) {
|
|
reject(error);
|
|
});
|
|
});
|
|
};
|
|
|
|
function pad(n) {
|
|
return n < 10 ? '0' + n : n;
|
|
}
|
|
|
|
function formatTimestamp(d) {
|
|
return '' +
|
|
d.getFullYear() +
|
|
pad(d.getMonth() + 1) +
|
|
pad(d.getDate()) + '-' +
|
|
pad(d.getHours()) +
|
|
pad(d.getMinutes()) +
|
|
pad(d.getSeconds());
|
|
}
|
|
|
|
function getDownloadFilename(filename, now) {
|
|
var ext = path.extname(filename);
|
|
filename = path.basename(filename, ext);
|
|
filename = filename.replace('"', '');
|
|
filename += '-Annotated-';
|
|
filename += formatTimestamp(now);
|
|
filename += ext || '.pdf';
|
|
return filename;
|
|
}
|
|
|
|
var server = http.createServer(function(request, response) {
|
|
if (request.method === "OPTIONS") {
|
|
// Pre-flight request
|
|
response.writeHead(200, {
|
|
"Access-Control-Allow-Origin": "*",
|
|
"Access-Control-Allow-Methods": "POST",
|
|
"Access-Control-Allow-Headers": "Content-Type",
|
|
"Access-Control-Expose-Headers": "Content-Disposition"
|
|
});
|
|
response.end();
|
|
return;
|
|
}
|
|
if (request.method !== "POST") {
|
|
response.writeHead(400, {
|
|
"Content-Type": "text/plain"
|
|
});
|
|
response.end('Bad Request');
|
|
return;
|
|
}
|
|
|
|
var u = url.parse(request.url);
|
|
if (u.pathname.indexOf("/download/") !== 0) {
|
|
response.writeHead(404, {
|
|
"Content-Type": "text/plain"
|
|
});
|
|
response.end('Not Found');
|
|
return;
|
|
}
|
|
|
|
var room_id = u.pathname.substr(10);
|
|
if (!rooms.hasOwnProperty(room_id)) {
|
|
console.log("Unknown room", room_id);
|
|
response.writeHead(404, {
|
|
"Content-Type": "text/plain"
|
|
});
|
|
response.end('Not Found');
|
|
return;
|
|
}
|
|
|
|
var room = rooms[room_id];
|
|
|
|
var body = null;
|
|
request.on("data", function(chunk) {
|
|
if (body === null) {
|
|
body = chunk;
|
|
} else {
|
|
body += chunk;
|
|
}
|
|
});
|
|
|
|
request.on('end', () => {
|
|
if (!body || !body.length) {
|
|
console.log("Client sent an empty request");
|
|
response.writeHead(400, {
|
|
"Access-Control-Allow-Origin": "*",
|
|
"Access-Control-Allow-Methods": "POST",
|
|
"Access-Control-Allow-Headers": "Content-Type, Content-Disposition",
|
|
"Access-Control-Expose-Headers": "Content-Disposition",
|
|
"Content-Type": "text/plain"
|
|
});
|
|
response.end('Bad Request');
|
|
return;
|
|
}
|
|
|
|
var data = JSON.parse(body.toString("utf-8"));
|
|
var token = data.token;
|
|
var decoded;
|
|
try {
|
|
decoded = jwt.verify(token, secret, {"algorithms": ['HS256']});
|
|
} catch (err) {
|
|
console.log("Client sent an invalid token", token);
|
|
response.writeHead(401, {
|
|
"Access-Control-Allow-Origin": "*",
|
|
"Access-Control-Allow-Methods": "POST",
|
|
"Access-Control-Allow-Headers": "Content-Type, Content-Disposition",
|
|
"Access-Control-Expose-Headers": "Content-Disposition",
|
|
"Content-Type": "text/plain"
|
|
});
|
|
response.end('Unauthorized');
|
|
return;
|
|
}
|
|
|
|
if (room_id !== decoded.file) {
|
|
console.log("Client sent a token for a different file", decoded);
|
|
response.writeHead(401, {
|
|
"Access-Control-Allow-Origin": "*",
|
|
"Access-Control-Allow-Methods": "POST",
|
|
"Access-Control-Allow-Headers": "Content-Type, Content-Disposition",
|
|
"Access-Control-Expose-Headers": "Content-Disposition",
|
|
"Content-Type": "text/plain"
|
|
});
|
|
response.end('Unauthorized');
|
|
return;
|
|
}
|
|
|
|
var downloader = room.downloadFile(token);
|
|
|
|
var tempdir = createDirSync(os.tmpdir() + "/svg2pdf-XXXXXX");
|
|
var combiner = runSvg2PdfCombiner(tempdir, data);
|
|
|
|
Promise.all([downloader, combiner]).then(function(results) {
|
|
var source_pdf = results[0];
|
|
var combined_filename = results[1];
|
|
if (!source_pdf.length) {
|
|
deleteFolderRecursive(tempdir);
|
|
console.log("Empty file received", room.id, token);
|
|
response.writeHead(500, {
|
|
"Access-Control-Allow-Origin": "*",
|
|
"Access-Control-Allow-Methods": "POST",
|
|
"Access-Control-Allow-Headers": "Content-Type, Content-Disposition",
|
|
"Access-Control-Expose-Headers": "Content-Disposition",
|
|
"Content-Type": "text/plain"
|
|
});
|
|
response.end('Internal Server Error');
|
|
return;
|
|
}
|
|
|
|
overlay_pdf(source_pdf, combined_filename)
|
|
.then((merged) => {
|
|
add_text_annotations(merged, data.text, tempdir)
|
|
.then((merged) => {
|
|
deleteFolderRecursive(tempdir);
|
|
if (!merged) {
|
|
response.writeHead(500, {
|
|
"Access-Control-Allow-Origin": "*",
|
|
"Access-Control-Allow-Methods": "POST",
|
|
"Access-Control-Allow-Headers": "Content-Type, Content-Disposition",
|
|
"Access-Control-Expose-Headers": "Content-Disposition",
|
|
"Content-Type": "text/plain"
|
|
});
|
|
response.end('Internal Server Error');
|
|
return;
|
|
}
|
|
|
|
var now = new Date();
|
|
var filename = getDownloadFilename(decoded.filename || room_id, now);
|
|
response.writeHead(200, {
|
|
"Access-Control-Allow-Origin": "*",
|
|
"Access-Control-Allow-Methods": "POST",
|
|
"Access-Control-Allow-Headers": "Content-Type, Content-Disposition",
|
|
"Access-Control-Expose-Headers": "Content-Disposition",
|
|
"Content-Type": "application/pdf",
|
|
"Content-Disposition": "attachment; filename=\"" + filename + "\"",
|
|
"Content-Length": merged.length
|
|
});
|
|
response.end(merged);
|
|
});
|
|
});
|
|
})
|
|
.catch(function(error) {
|
|
deleteFolderRecursive(tempdir);
|
|
if (error) {
|
|
console.log("Error", error);
|
|
}
|
|
response.writeHead(500, {
|
|
"Access-Control-Allow-Origin": "*",
|
|
"Access-Control-Allow-Methods": "POST",
|
|
"Access-Control-Allow-Headers": "Content-Type, Content-Disposition",
|
|
"Access-Control-Expose-Headers": "Content-Disposition",
|
|
"Content-Type": "text/plain"
|
|
});
|
|
response.end('Internal Server Error');
|
|
});
|
|
});
|
|
});
|
|
|
|
var io = socketio(server, {
|
|
'cors': {
|
|
'origin': '*:*',
|
|
}
|
|
});
|
|
|
|
io.on('connection', function(socket) {
|
|
var query = socket.handshake.query;
|
|
console.log('a user connected', query);
|
|
|
|
var token = query.token;
|
|
var decoded;
|
|
try {
|
|
decoded = jwt.verify(token, secret, {"algorithms": ['HS256']});
|
|
} catch(err) {
|
|
console.log("Invalid token received", err);
|
|
socket.disconnect(true);
|
|
return;
|
|
}
|
|
|
|
var baseurl = decoded.iss;
|
|
if (!baseurl) {
|
|
console.log('token did not contain a issuer', decoded);
|
|
socket.disconnect(true);
|
|
return;
|
|
}
|
|
|
|
var room_id = decoded.file;
|
|
if (!room_id) {
|
|
console.log("token did not contain a file id", decoded);
|
|
socket.disconnect(true);
|
|
return;
|
|
}
|
|
|
|
console.log("Token", decoded);
|
|
|
|
var room;
|
|
|
|
socket.on('disconnecting', function(reason) {
|
|
if (!room) {
|
|
return;
|
|
}
|
|
room.removeUser(socket);
|
|
if (room.isEmpty()) {
|
|
console.log("deleting empty room", room.id);
|
|
delete rooms[room.id];
|
|
}
|
|
});
|
|
|
|
socket.on('disconnect', function(reason) {
|
|
console.log('a user disconnected');
|
|
});
|
|
|
|
if (!rooms.hasOwnProperty(room_id)) {
|
|
room = rooms[room_id] = new Room(io, room_id, baseurl);
|
|
console.log("created room", room_id);
|
|
} else {
|
|
room = rooms[room_id];
|
|
console.log('joined room', room_id);
|
|
}
|
|
|
|
socket.on('message', function(message) {
|
|
message.userid = socket.id;
|
|
var page, name;
|
|
if (message.type === "item") {
|
|
page = message.item.page;
|
|
name = message.item.name;
|
|
var data = message.item.data;
|
|
if (!page || !name || !data) {
|
|
// Ignore invalid entries.
|
|
return;
|
|
}
|
|
try {
|
|
JSON.parse(data);
|
|
} catch (e) {
|
|
// Ignore items with invalid data.
|
|
return;
|
|
}
|
|
if (!room.addItem(socket, name, page, data)) {
|
|
return;
|
|
}
|
|
} else if (message.type === "delete") {
|
|
page = message.delete.page;
|
|
name = message.delete.name;
|
|
if (!page || !name) {
|
|
// Ignore invalid entries.
|
|
return;
|
|
}
|
|
if (!room.removeItem(socket, name, page)) {
|
|
return;
|
|
}
|
|
} else if (message.type === "control") {
|
|
if (!room.handleControl(socket, message)) {
|
|
return;
|
|
}
|
|
}
|
|
room.sendMessage(socket, message);
|
|
});
|
|
|
|
socket.join(room_id);
|
|
|
|
var displayname = decoded.displayname || null;
|
|
var permissions = decoded.permissions || null;
|
|
room.addUser(socket, {
|
|
'displayname': displayname,
|
|
'permissions': permissions
|
|
});
|
|
});
|
|
|
|
server.listen(port, function() {
|
|
console.log('listening on *:' + port);
|
|
});
|