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
12 KiB

  1. /*
  2. globals require console
  3. */
  4. var Express = require('express');
  5. var Http = require('http');
  6. var Fs = require('fs');
  7. var Package = require('./package.json');
  8. var Path = require("path");
  9. var nThen = require("nthen");
  10. var Util = require("./lib/common-util");
  11. var Default = require("./lib/defaults");
  12. var config = require("./lib/load-config");
  13. var app = Express();
  14. // mode can be FRESH (default), DEV, or PACKAGE
  15. var FRESH_KEY = '';
  16. var FRESH_MODE = true;
  17. var DEV_MODE = false;
  18. if (process.env.PACKAGE) {
  19. // `PACKAGE=1 node server` uses the version string from package.json as the cache string
  20. console.log("PACKAGE MODE ENABLED");
  21. FRESH_MODE = false;
  22. DEV_MODE = false;
  23. } else if (process.env.DEV) {
  24. // `DEV=1 node server` will use a random cache string on every page reload
  25. console.log("DEV MODE ENABLED");
  26. FRESH_MODE = false;
  27. DEV_MODE = true;
  28. } else {
  29. // `FRESH=1 node server` will set a random cache string when the server is launched
  30. // and use it for the process lifetime or until it is reset from the admin panel
  31. console.log("FRESH MODE ENABLED");
  32. FRESH_KEY = +new Date();
  33. }
  34. (function () {
  35. // you absolutely must provide an 'httpUnsafeOrigin'
  36. if (typeof(config.httpUnsafeOrigin) !== 'string') {
  37. throw new Error("No 'httpUnsafeOrigin' provided");
  38. }
  39. config.httpUnsafeOrigin = config.httpUnsafeOrigin.trim();
  40. if (typeof(config.httpSafeOrigin) === 'string') {
  41. config.httpSafeOrigin = config.httpSafeOrigin.trim().replace(/\/$/, '');
  42. }
  43. // fall back to listening on a local address
  44. // if httpAddress is not a string
  45. if (typeof(config.httpAddress) !== 'string') {
  46. config.httpAddress = '127.0.0.1';
  47. }
  48. // listen on port 3000 if a valid port number was not provided
  49. if (typeof(config.httpPort) !== 'number' || config.httpPort > 65535) {
  50. config.httpPort = 3000;
  51. }
  52. if (typeof(config.httpSafeOrigin) !== 'string') {
  53. if (typeof(config.httpSafePort) !== 'number') {
  54. config.httpSafePort = config.httpPort + 1;
  55. }
  56. if (DEV_MODE) { return; }
  57. console.log(`
  58. m m mm mmmmm mm m mmmmm mm m mmm m
  59. # # # ## # "# #"m # # #"m # m" " #
  60. " #"# # # # #mmmm" # #m # # # #m # # mm #
  61. ## ##" #mm# # "m # # # # # # # # #
  62. # # # # # " # ## mm#mm # ## "mmm" #
  63. `);
  64. console.log("\nNo 'httpSafeOrigin' provided.");
  65. console.log("Your configuration probably isn't taking advantage of all of CryptPad's security features!");
  66. console.log("This is acceptable for development, otherwise your users may be at risk.\n");
  67. console.log("Serving sandboxed content via port %s.\nThis is probably not what you want for a production instance!\n", config.httpSafePort);
  68. }
  69. }());
  70. var configCache = {};
  71. config.flushCache = function () {
  72. configCache = {};
  73. FRESH_KEY = +new Date();
  74. if (!(DEV_MODE || FRESH_MODE)) { FRESH_MODE = true; }
  75. if (!config.log) { return; }
  76. config.log.info("UPDATING_FRESH_KEY", FRESH_KEY);
  77. };
  78. const clone = (x) => (JSON.parse(JSON.stringify(x)));
  79. var setHeaders = (function () {
  80. // load the default http headers unless the admin has provided their own via the config file
  81. var headers;
  82. var custom = config.httpHeaders;
  83. // if the admin provided valid http headers then use them
  84. if (custom && typeof(custom) === 'object' && !Array.isArray(custom)) {
  85. headers = clone(custom);
  86. } else {
  87. // otherwise use the default
  88. headers = Default.httpHeaders();
  89. }
  90. // next define the base Content Security Policy (CSP) headers
  91. if (typeof(config.contentSecurity) === 'string') {
  92. headers['Content-Security-Policy'] = config.contentSecurity;
  93. if (!/;$/.test(headers['Content-Security-Policy'])) { headers['Content-Security-Policy'] += ';' }
  94. if (headers['Content-Security-Policy'].indexOf('frame-ancestors') === -1) {
  95. // backward compat for those who do not merge the new version of the config
  96. // when updating. This prevents endless spinner if someone clicks donate.
  97. // It also fixes the cross-domain iframe.
  98. headers['Content-Security-Policy'] += "frame-ancestors *;";
  99. }
  100. } else {
  101. // use the default CSP headers constructed with your domain
  102. headers['Content-Security-Policy'] = Default.contentSecurity(config.httpUnsafeOrigin);
  103. }
  104. const padHeaders = clone(headers);
  105. if (typeof(config.padContentSecurity) === 'string') {
  106. padHeaders['Content-Security-Policy'] = config.padContentSecurity;
  107. } else {
  108. padHeaders['Content-Security-Policy'] = Default.padContentSecurity(config.httpUnsafeOrigin);
  109. }
  110. if (Object.keys(headers).length) {
  111. return function (req, res) {
  112. const h = [
  113. /^\/pad\/inner\.html.*/,
  114. /^\/common\/onlyoffice\/.*\/index\.html.*/,
  115. /^\/(sheet|ooslide|oodoc)\/inner\.html.*/,
  116. ].some((regex) => {
  117. return regex.test(req.url);
  118. }) ? padHeaders : headers;
  119. for (let header in h) { res.setHeader(header, h[header]); }
  120. };
  121. }
  122. return function () {};
  123. }());
  124. (function () {
  125. if (!config.logFeedback) { return; }
  126. const logFeedback = function (url) {
  127. url.replace(/\?(.*?)=/, function (all, fb) {
  128. config.log.feedback(fb, '');
  129. });
  130. };
  131. app.head(/^\/common\/feedback\.html/, function (req, res, next) {
  132. logFeedback(req.url);
  133. next();
  134. });
  135. }());
  136. app.use(function (req, res, next) {
  137. if (req.method === 'OPTIONS' && /\/blob\//.test(req.url)) {
  138. res.setHeader('Access-Control-Allow-Origin', '*');
  139. res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
  140. 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');
  141. res.setHeader('Access-Control-Max-Age', 1728000);
  142. res.setHeader('Content-Type', 'application/octet-stream; charset=utf-8');
  143. res.setHeader('Content-Length', 0);
  144. res.statusCode = 204;
  145. return void res.end();
  146. }
  147. setHeaders(req, res);
  148. if (/[\?\&]ver=[^\/]+$/.test(req.url)) { res.setHeader("Cache-Control", "max-age=31536000"); }
  149. next();
  150. });
  151. app.use(Express.static(__dirname + '/www'));
  152. // FIXME I think this is a regression caused by a recent PR
  153. // correct this hack without breaking the contributor's intended behaviour.
  154. var mainPages = config.mainPages || Default.mainPages();
  155. var mainPagePattern = new RegExp('^\/(' + mainPages.join('|') + ').html$');
  156. app.get(mainPagePattern, Express.static(__dirname + '/customize'));
  157. app.get(mainPagePattern, Express.static(__dirname + '/customize.dist'));
  158. app.use("/blob", Express.static(Path.join(__dirname, (config.blobPath || './blob')), {
  159. maxAge: DEV_MODE? "0d": "365d"
  160. }));
  161. app.use("/datastore", Express.static(Path.join(__dirname, (config.filePath || './datastore')), {
  162. maxAge: "0d"
  163. }));
  164. app.use("/block", Express.static(Path.join(__dirname, (config.blockPath || '/block')), {
  165. maxAge: "0d",
  166. }));
  167. app.use("/customize", Express.static(__dirname + '/customize'));
  168. app.use("/customize", Express.static(__dirname + '/customize.dist'));
  169. app.use("/customize.dist", Express.static(__dirname + '/customize.dist'));
  170. app.use(/^\/[^\/]*$/, Express.static('customize'));
  171. app.use(/^\/[^\/]*$/, Express.static('customize.dist'));
  172. var admins = [];
  173. try {
  174. admins = (config.adminKeys || []).map(function (k) {
  175. k = k.replace(/\/+$/, '');
  176. var s = k.split('/');
  177. return s[s.length-1].replace(/-/g, '/');
  178. });
  179. } catch (e) { console.error("Can't parse admin keys"); }
  180. var serveConfig = (function () {
  181. // if dev mode: never cache
  182. var cacheString = function () {
  183. return (FRESH_KEY? '-' + FRESH_KEY: '') + (DEV_MODE? '-' + (+new Date()): '');
  184. };
  185. var template = function (host) {
  186. return [
  187. 'define(function(){',
  188. 'var obj = ' + JSON.stringify({
  189. requireConf: {
  190. waitSeconds: 600,
  191. urlArgs: 'ver=' + Package.version + cacheString(),
  192. },
  193. removeDonateButton: (config.removeDonateButton === true),
  194. allowSubscriptions: (config.allowSubscriptions === true),
  195. websocketPath: config.externalWebsocketURL,
  196. httpUnsafeOrigin: config.httpUnsafeOrigin,
  197. adminEmail: config.adminEmail,
  198. adminKeys: admins,
  199. inactiveTime: config.inactiveTime,
  200. supportMailbox: config.supportMailboxPublicKey,
  201. maxUploadSize: config.maxUploadSize,
  202. premiumUploadSize: config.premiumUploadSize,
  203. }, null, '\t'),
  204. 'obj.httpSafeOrigin = ' + (function () {
  205. if (config.httpSafeOrigin) { return '"' + config.httpSafeOrigin + '"'; }
  206. if (config.httpSafePort) {
  207. return "(function () { return window.location.origin.replace(/\:[0-9]+$/, ':" +
  208. config.httpSafePort + "'); }())";
  209. }
  210. return 'window.location.origin';
  211. }()),
  212. 'return obj',
  213. '});'
  214. ].join(';\n')
  215. };
  216. var cleanUp = {};
  217. return function (req, res) {
  218. var host = req.headers.host.replace(/\:[0-9]+/, '');
  219. res.setHeader('Content-Type', 'text/javascript');
  220. // don't cache anything if you're in dev mode
  221. if (DEV_MODE) {
  222. return void res.send(template(host));
  223. }
  224. // generate a lookup key for the cache
  225. var cacheKey = host + ':' + cacheString();
  226. // if there's nothing cached for that key...
  227. if (!configCache[cacheKey]) {
  228. // generate the response and cache it in memory
  229. configCache[cacheKey] = template(host);
  230. // and create a function to conditionally evict cache entries
  231. // which have not been accessed in the last 20 seconds
  232. cleanUp[cacheKey] = Util.throttle(function () {
  233. delete cleanUp[cacheKey];
  234. delete configCache[cacheKey];
  235. }, 20000);
  236. }
  237. // successive calls to this function
  238. cleanUp[cacheKey]();
  239. return void res.send(configCache[cacheKey]);
  240. };
  241. }());
  242. app.get('/api/config', serveConfig);
  243. var four04_path = Path.resolve(__dirname + '/customize.dist/404.html');
  244. var custom_four04_path = Path.resolve(__dirname + '/customize/404.html');
  245. var send404 = function (res, path) {
  246. if (!path && path !== four04_path) { path = four04_path; }
  247. Fs.exists(path, function (exists) {
  248. res.setHeader('Content-Type', 'text/html; charset=utf-8');
  249. if (exists) { return Fs.createReadStream(path).pipe(res); }
  250. send404(res);
  251. });
  252. };
  253. app.use(function (req, res, next) {
  254. res.status(404);
  255. send404(res, custom_four04_path);
  256. });
  257. var httpServer = Http.createServer(app);
  258. nThen(function (w) {
  259. Fs.exists(__dirname + "/customize", w(function (e) {
  260. if (e) { return; }
  261. console.log("Cryptpad is customizable, see customize.dist/readme.md for details");
  262. }));
  263. }).nThen(function (w) {
  264. httpServer.listen(config.httpPort,config.httpAddress,function(){
  265. var host = config.httpAddress;
  266. var hostName = !host.indexOf(':') ? '[' + host + ']' : host;
  267. var port = config.httpPort;
  268. var ps = port === 80? '': ':' + port;
  269. console.log('[%s] server available http://%s%s', new Date().toISOString(), hostName, ps);
  270. });
  271. if (config.httpSafePort) {
  272. Http.createServer(app).listen(config.httpSafePort, config.httpAddress, w());
  273. }
  274. }).nThen(function () {
  275. var wsConfig = { server: httpServer };
  276. // Initialize logging then start the API server
  277. require("./lib/log").create(config, function (_log) {
  278. config.log = _log;
  279. config.httpServer = httpServer;
  280. if (config.externalWebsocketURL) { return; }
  281. require("./lib/api").create(config);
  282. });
  283. });