You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

330 lines
11 KiB

1 year ago
2 years ago
  1. /*
  2. globals require console
  3. */
  4. var Express = require('express');
  5. var Http = require('http');
  6. var Https = require('https');
  7. var Fs = require('fs');
  8. var WebSocketServer = require('ws').Server;
  9. var NetfluxSrv = require('./node_modules/chainpad-server/NetfluxWebsocketSrv');
  10. var Package = require('./package.json');
  11. var Path = require("path");
  12. var nThen = require("nthen");
  13. var config = require("./lib/load-config");
  14. // This is stuff which will become available to replify
  15. const debuggableStore = new WeakMap();
  16. const debuggable = function (name, x) {
  17. if (name in debuggableStore) {
  18. try { throw new Error(); } catch (e) {
  19. console.error('cannot add ' + name + ' more than once [' + e.stack + ']');
  20. }
  21. } else {
  22. debuggableStore[name] = x;
  23. }
  24. return x;
  25. };
  26. debuggable('global', global);
  27. debuggable('config', config);
  28. // support multiple storage back ends
  29. var Storage = require(config.storage||'./storage/file');
  30. var app = debuggable('app', Express());
  31. var httpsOpts;
  32. // mode can be FRESH (default), DEV, or PACKAGE
  33. var FRESH_KEY = '';
  34. var FRESH_MODE = true;
  35. var DEV_MODE = false;
  36. if (process.env.PACKAGE) {
  37. // `PACKAGE=1 node server` uses the version string from package.json as the cache string
  38. console.log("PACKAGE MODE ENABLED");
  39. FRESH_MODE = false;
  40. DEV_MODE = false;
  41. } else if (process.env.DEV) {
  42. // `DEV=1 node server` will use a random cache string on every page reload
  43. console.log("DEV MODE ENABLED");
  44. FRESH_MODE = false;
  45. DEV_MODE = true;
  46. } else {
  47. // `FRESH=1 node server` will set a random cache string when the server is launched
  48. // and use it for the process lifetime or until it is reset from the admin panel
  49. console.log("FRESH MODE ENABLED");
  50. FRESH_KEY = +new Date();
  51. }
  52. config.flushCache = function () {
  53. FRESH_KEY = +new Date();
  54. if (!(DEV_MODE || FRESH_MODE)) { FRESH_MODE = true; }
  55. if (!config.log) { return; }
  56. config.log.info("UPDATING_FRESH_KEY", FRESH_KEY);
  57. };
  58. const clone = (x) => (JSON.parse(JSON.stringify(x)));
  59. var setHeaders = (function () {
  60. if (typeof(config.httpHeaders) !== 'object') { return function () {}; }
  61. const headers = clone(config.httpHeaders);
  62. if (config.contentSecurity) {
  63. headers['Content-Security-Policy'] = clone(config.contentSecurity);
  64. if (!/;$/.test(headers['Content-Security-Policy'])) { headers['Content-Security-Policy'] += ';' }
  65. if (headers['Content-Security-Policy'].indexOf('frame-ancestors') === -1) {
  66. // backward compat for those who do not merge the new version of the config
  67. // when updating. This prevents endless spinner if someone clicks donate.
  68. // It also fixes the cross-domain iframe.
  69. headers['Content-Security-Policy'] += "frame-ancestors *;";
  70. }
  71. }
  72. const padHeaders = clone(headers);
  73. if (config.padContentSecurity) {
  74. padHeaders['Content-Security-Policy'] = clone(config.padContentSecurity);
  75. }
  76. if (Object.keys(headers).length) {
  77. return function (req, res) {
  78. const h = [
  79. /^\/pad(2)?\/inner\.html.*/,
  80. /^\/sheet\/inner\.html.*/,
  81. /^\/common\/onlyoffice\/.*\/index\.html.*/
  82. ].some((regex) => {
  83. return regex.test(req.url)
  84. }) ? padHeaders : headers;
  85. for (let header in h) { res.setHeader(header, h[header]); }
  86. };
  87. }
  88. return function () {};
  89. }());
  90. (function () {
  91. if (!config.logFeedback) { return; }
  92. const logFeedback = function (url) {
  93. url.replace(/\?(.*?)=/, function (all, fb) {
  94. config.log.feedback(fb, '');
  95. });
  96. };
  97. app.head(/^\/common\/feedback\.html/, function (req, res, next) {
  98. logFeedback(req.url);
  99. next();
  100. });
  101. }());
  102. app.use(function (req, res, next) {
  103. if (req.method === 'OPTIONS' && /\/blob\//.test(req.url)) {
  104. res.setHeader('Access-Control-Allow-Origin', '*');
  105. res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
  106. res.setHeader('Access-Control-Allow-Headers', 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range');
  107. res.setHeader('Access-Control-Max-Age', 1728000);
  108. res.setHeader('Content-Type', 'application/octet-stream; charset=utf-8');
  109. res.setHeader('Content-Length', 0);
  110. res.statusCode = 204;
  111. return void res.end();
  112. }
  113. setHeaders(req, res);
  114. if (/[\?\&]ver=[^\/]+$/.test(req.url)) { res.setHeader("Cache-Control", "max-age=31536000"); }
  115. next();
  116. });
  117. app.use(Express.static(__dirname + '/www'));
  118. Fs.exists(__dirname + "/customize", function (e) {
  119. if (e) { return; }
  120. console.log("Cryptpad is customizable, see customize.dist/readme.md for details");
  121. });
  122. // FIXME I think this is a regression caused by a recent PR
  123. // correct this hack without breaking the contributor's intended behaviour.
  124. var mainPages = config.mainPages || ['index', 'privacy', 'terms', 'about', 'contact'];
  125. var mainPagePattern = new RegExp('^\/(' + mainPages.join('|') + ').html$');
  126. app.get(mainPagePattern, Express.static(__dirname + '/customize'));
  127. app.get(mainPagePattern, Express.static(__dirname + '/customize.dist'));
  128. app.use("/blob", Express.static(Path.join(__dirname, (config.blobPath || './blob')), {
  129. maxAge: DEV_MODE? "0d": "365d"
  130. }));
  131. app.use("/datastore", Express.static(Path.join(__dirname, (config.filePath || './datastore')), {
  132. maxAge: "0d"
  133. }));
  134. app.use("/block", Express.static(Path.join(__dirname, (config.blockPath || '/block')), {
  135. maxAge: "0d",
  136. }));
  137. app.use("/customize", Express.static(__dirname + '/customize'));
  138. app.use("/customize", Express.static(__dirname + '/customize.dist'));
  139. app.use("/customize.dist", Express.static(__dirname + '/customize.dist'));
  140. app.use(/^\/[^\/]*$/, Express.static('customize'));
  141. app.use(/^\/[^\/]*$/, Express.static('customize.dist'));
  142. if (config.privKeyAndCertFiles) {
  143. var privKeyAndCerts = '';
  144. config.privKeyAndCertFiles.forEach(function (file) {
  145. privKeyAndCerts = privKeyAndCerts + Fs.readFileSync(file);
  146. });
  147. var array = privKeyAndCerts.split('\n-----BEGIN ');
  148. for (var i = 1; i < array.length; i++) { array[i] = '-----BEGIN ' + array[i]; }
  149. var privKey;
  150. for (var i = 0; i < array.length; i++) {
  151. if (array[i].indexOf('PRIVATE KEY-----\n') !== -1) {
  152. privKey = array[i];
  153. array.splice(i, 1);
  154. break;
  155. }
  156. }
  157. if (!privKey) { throw new Error("cannot find private key"); }
  158. httpsOpts = {
  159. cert: array.shift(),
  160. key: privKey,
  161. ca: array
  162. };
  163. }
  164. var admins = [];
  165. try {
  166. admins = (config.adminKeys || []).map(function (k) {
  167. k = k.replace(/\/+$/, '');
  168. var s = k.split('/');
  169. return s[s.length-1].replace(/-/g, '/');
  170. });
  171. } catch (e) { console.error("Can't parse admin keys"); }
  172. // TODO, cache this /api/config responses instead of re-computing it each time
  173. app.get('/api/config', function(req, res){
  174. // TODO precompute any data that isn't dynamic to save some CPU time
  175. var host = req.headers.host.replace(/\:[0-9]+/, '');
  176. res.setHeader('Content-Type', 'text/javascript');
  177. res.send('define(function(){\n' + [
  178. 'var obj = ' + JSON.stringify({
  179. requireConf: {
  180. waitSeconds: 600,
  181. urlArgs: 'ver=' + Package.version + (FRESH_KEY? '-' + FRESH_KEY: '') + (DEV_MODE? '-' + (+new Date()): ''),
  182. },
  183. removeDonateButton: (config.removeDonateButton === true),
  184. allowSubscriptions: (config.allowSubscriptions === true),
  185. websocketPath: config.useExternalWebsocket ? undefined : config.websocketPath,
  186. httpUnsafeOrigin: config.httpUnsafeOrigin.replace(/^\s*/, ''),
  187. adminEmail: config.adminEmail,
  188. adminKeys: admins,
  189. inactiveTime: config.inactiveTime,
  190. supportMailbox: config.supportMailboxPublicKey
  191. }, null, '\t'),
  192. 'obj.httpSafeOrigin = ' + (function () {
  193. if (config.httpSafeOrigin) { return '"' + config.httpSafeOrigin + '"'; }
  194. if (config.httpSafePort) {
  195. return "(function () { return window.location.origin.replace(/\:[0-9]+$/, ':" +
  196. config.httpSafePort + "'); }())";
  197. }
  198. return 'window.location.origin';
  199. }()),
  200. 'return obj',
  201. '});'
  202. ].join(';\n'));
  203. });
  204. var four04_path = Path.resolve(__dirname + '/customize.dist/404.html');
  205. var custom_four04_path = Path.resolve(__dirname + '/customize/404.html');
  206. var send404 = function (res, path) {
  207. if (!path && path !== four04_path) { path = four04_path; }
  208. Fs.exists(path, function (exists) {
  209. res.setHeader('Content-Type', 'text/html; charset=utf-8');
  210. if (exists) { return Fs.createReadStream(path).pipe(res); }
  211. send404(res);
  212. });
  213. };
  214. app.use(function (req, res, next) {
  215. res.status(404);
  216. send404(res, custom_four04_path);
  217. });
  218. var httpServer = httpsOpts ? Https.createServer(httpsOpts, app) : Http.createServer(app);
  219. httpServer.listen(config.httpPort,config.httpAddress,function(){
  220. var host = config.httpAddress;
  221. var hostName = !host.indexOf(':') ? '[' + host + ']' : host;
  222. var port = config.httpPort;
  223. var ps = port === 80? '': ':' + port;
  224. console.log('[%s] server available http://%s%s', new Date().toISOString(), hostName, ps);
  225. });
  226. if (config.httpSafePort) {
  227. Http.createServer(app).listen(config.httpSafePort, config.httpAddress);
  228. }
  229. var wsConfig = { server: httpServer };
  230. var rpc;
  231. var historyKeeper;
  232. var log;
  233. // Initialize logging, the the store, then tasks, then rpc, then history keeper and then start the server
  234. var nt = nThen(function (w) {
  235. // set up logger
  236. var Logger = require("./lib/log");
  237. //console.log("Loading logging module");
  238. Logger.create(config, w(function (_log) {
  239. log = config.log = _log;
  240. }));
  241. }).nThen(function (w) {
  242. if (config.useExternalWebsocket) { return; }
  243. Storage.create(config, w(function (_store) {
  244. config.store = _store;
  245. }));
  246. }).nThen(function (w) {
  247. var Tasks = require("./storage/tasks");
  248. Tasks.create(config, w(function (e, tasks) {
  249. if (e) {
  250. throw e;
  251. }
  252. config.tasks = tasks;
  253. if (config.disableIntegratedTasks) { return; }
  254. setInterval(function () {
  255. tasks.runAll(function (err) {
  256. if (err) {
  257. // either TASK_CONCURRENCY or an error with tasks.list
  258. // in either case it is already logged.
  259. }
  260. });
  261. }, 1000 * 60 * 5); // run every five minutes
  262. }));
  263. }).nThen(function (w) {
  264. config.rpc = typeof(config.rpc) === 'undefined'? './rpc.js' : config.rpc;
  265. if (typeof(config.rpc) !== 'string') { return; }
  266. // load pin store...
  267. var Rpc = require(config.rpc);
  268. Rpc.create(config, debuggable, w(function (e, _rpc) {
  269. if (e) {
  270. w.abort();
  271. throw e;
  272. }
  273. rpc = _rpc;
  274. }));
  275. }).nThen(function () {
  276. if (config.useExternalWebsocket) { return; }
  277. var HK = require('./historyKeeper.js');
  278. var hkConfig = {
  279. tasks: config.tasks,
  280. rpc: rpc,
  281. store: config.store,
  282. log: log,
  283. retainData: Boolean(config.retainData),
  284. };
  285. historyKeeper = HK.create(hkConfig);
  286. }).nThen(function () {
  287. if (config.useExternalWebsocket) { return; }
  288. var wsSrv = new WebSocketServer(wsConfig);
  289. NetfluxSrv.run(wsSrv, config, historyKeeper);
  290. });
  291. if (config.debugReplName) {
  292. require('replify')({ name: config.debugReplName, app: debuggableStore });
  293. }