diff --git a/config/config.example.js b/config/config.example.js index 25a4c97a6..273c196d2 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -1,67 +1,110 @@ -/* - globals module -*/ -var _domain = 'http://localhost:3000/'; - -// You can `kill -USR2` the node process and it will write out a heap dump. -// If your system doesn't support dumping, comment this out and install with -// `npm install --production` -// See: https://strongloop.github.io/strongloop.com/strongblog/how-to-heap-snapshots/ +/* globals module */ -// to enable this feature, uncomment the line below: -// require('heapdump'); +/* DISCLAIMER: -// we prepend a space because every usage expects it -// requiring admins to preserve it is unnecessarily confusing -var domain = ' ' + _domain; + There are two recommended methods of running a CryptPad instance: -// Content-Security-Policy -var baseCSP = [ - "default-src 'none'", - "style-src 'unsafe-inline' 'self' " + domain, - "font-src 'self' data:" + domain, + 1. Using a standalone nodejs server without HTTPS (suitable for local development) + 2. Using NGINX to serve static assets and to handle HTTPS for API server's websocket traffic - /* child-src is used to restrict iframes to a set of allowed domains. - * connect-src is used to restrict what domains can connect to the websocket. - * - * it is recommended that you configure these fields to match the - * domain which will serve your CryptPad instance. - */ - "child-src blob: *", - // IE/Edge - "frame-src blob: *", + We do not officially recommend or support Apache, Docker, Kubernetes, Traefik, or any other configuration. + Support requests for such setups should be directed to their authors. - /* this allows connections over secure or insecure websockets - if you are deploying to production, you'll probably want to remove - the ws://* directive, and change '*' to your domain - */ - "connect-src 'self' ws: wss: blob:" + domain, + If you're having difficulty difficulty configuring your instance + we suggest that you join the project's IRC/Matrix channel. - // data: is used by codemirror - "img-src 'self' data: blob:" + domain, - "media-src * blob:", + If you don't have any difficulty configuring your instance and you'd like to + support us for the work that went into making it pain-free we are quite happy + to accept donations via our opencollective page: https://opencollective.com/cryptpad - // for accounts.cryptpad.fr authentication and cross-domain iframe sandbox - "frame-ancestors *", - "" -]; +*/ +module.exports = { +/* CryptPad is designed to serve its content over two domains. + * Account passwords and cryptographic content is handled on the 'main' domain, + * while the user interface is loaded on a 'sandbox' domain + * which can only access information which the main domain willingly shares. + * + * In the event of an XSS vulnerability in the UI (that's bad) + * this system prevents attackers from gaining access to your account (that's good). + * + * Most problems with new instances are related to this system blocking access + * because of incorrectly configured sandboxes. If you only see a white screen + * when you try to load CryptPad, this is probably the cause. + * + * PLEASE READ THE FOLLOWING COMMENTS CAREFULLY. + * + */ + +/* httpUnsafeOrigin is the URL that clients will enter to load your instance. + * Any other URL that somehow points to your instance is supposed to be blocked. + * The default provided below assumes you are loading CryptPad from a server + * which is running on the same machine, using port 3000. + * + * In a production instance this should be available ONLY over HTTPS + * using the default port for HTTPS (443) ie. https://cryptpad.fr + * In such a case this should be handled by NGINX, as documented in + * cryptpad/docs/example.nginx.conf (see the $main_domain variable) + * + */ + httpUnsafeOrigin: 'http://localhost:3000/', + +/* httpSafeOrigin is the URL that is used for the 'sandbox' described above. + * If you're testing or developing with CryptPad on your local machine then + * it is appropriate to leave this blank. The default behaviour is to serve + * the main domain over port 3000 and to serve the content over port 3001. + * + * This is not appropriate in a production environment where invasive networks + * may filter traffic going over abnormal ports. + * To correctly configure your production instance you must provide a URL + * with a different domain (a subdomain is sufficient). + * It will be used to load the UI in our 'sandbox' system. + * + * This value corresponds to the $sandbox_domain variable + * in the example nginx file. + * + * CUSTOMIZE AND UNCOMMENT THIS FOR PRODUCTION INSTALLATIONS. + */ + // httpSafeOrigin: "https://some-other-domain.xyz", +/* httpAddress specifies the address on which the nodejs server + * should be accessible. By default it will listen on 127.0.0.1 + * (IPv4 localhost on most systems). If you want it to listen on + * all addresses, including IPv6, set this to '::'. + * + */ + //httpAddress: '::', + +/* httpPort specifies on which port the nodejs server should listen. + * By default it will serve content over port 3000, which is suitable + * for both local development and for use with the provided nginx example, + * which will proxy websocket traffic to your node server. + * + */ + //httpPort: 3000, + +/* httpSafePort allows you to specify an alternative port from which + * the node process should serve sandboxed assets. The default value is + * that of your httpPort + 1. You probably don't need to change this. + * + */ + //httpSafePort: 3001, -module.exports = { /* ===================== * Admin * ===================== */ /* - * CryptPad now contains an administration panel. Its access is restricted to specific + * CryptPad contains an administration panel. Its access is restricted to specific * users using the following list. * To give access to the admin panel to a user account, just add their user id, * which can be found on the settings page for registered users. * Entries should be strings separated by a comma. */ +/* adminKeys: [ //"https://my.awesome.website/user/#/1/cryptpad-user1/YZgXQxKR0Rcb6r6CmxHPdAGLVludrAF2lEnkbx1vVOo=", ], +*/ /* CryptPad's administration panel includes a "support" tab * wherein administrators with a secret key can view messages @@ -76,159 +119,55 @@ module.exports = { */ // supportMailboxPublicKey: "", - /* ===================== - * Infra setup - * ===================== */ - - // the address you want to bind to, :: means all ipv4 and ipv6 addresses - // this may not work on all operating systems - httpAddress: '::', - - // the port on which your httpd will listen - httpPort: 3000, - - // This is for allowing the cross-domain iframe to function when developing - httpSafePort: 3001, - - // This is for deployment in production, CryptPad uses a separate origin (domain) to host the - // cross-domain iframe. It can simply host the same content as CryptPad. - // httpSafeOrigin: "https://some-other-domain.xyz", - - httpUnsafeOrigin: domain, - - /* Your CryptPad server will share this value with clients - * via its /api/config endpoint. - * - * If you want to host your API and asset servers on different hosts - * specify a URL for your API server websocket endpoint, like so: - * wss://api.yourdomain.com/cryptpad_websocket - * - * Otherwise, leave this commented and your clients will use the default - * websocket (wss://yourdomain.com/cryptpad_websocket) - */ - //externalWebsocketURL: 'wss://api.yourdomain.com/cryptpad_websocket - - /* CryptPad can be configured to send customized HTTP Headers - * These settings may vary widely depending on your needs - * Examples are provided below - */ - httpHeaders: { - "X-XSS-Protection": "1; mode=block", - "X-Content-Type-Options": "nosniff", - "Access-Control-Allow-Origin": "*" - }, - - contentSecurity: baseCSP.join('; ') + - "script-src 'self'" + domain, - - // CKEditor and OnlyOffice require significantly more lax content security policy in order to function. - padContentSecurity: baseCSP.join('; ') + - "script-src 'self' 'unsafe-eval' 'unsafe-inline'" + domain, - - /* Main pages - * add exceptions to the router so that we can access /privacy.html - * and other odd pages - */ - mainPages: [ - 'index', - 'privacy', - 'terms', - 'about', - 'contact', - 'what-is-cryptpad', - 'features', - 'faq', - 'maintenance' - ], - - /* ===================== - * Subscriptions - * ===================== */ - - /* Limits, Donations, Subscriptions and Contact + /* We're very proud that CryptPad is available to the public as free software! + * We do, however, still need to pay our bills as we develop the platform. * - * By default, CryptPad limits every registered user to 50MB of storage. It also shows a - * subscribe button which allows them to upgrade to a paid account. We handle payment, - * and keep 50% of the proceeds to fund ongoing development. + * By default CryptPad will prompt users to consider donating to + * our OpenCollective campaign. We publish the state of our finances periodically + * so you can decide for yourself whether our expenses are reasonable. * - * You can: - * A: leave things as they are - * B: disable accounts but display a donate button - * C: hide any reference to paid accounts or donation - * - * If you chose A then there's nothing to do. - * If you chose B, set 'allowSubscriptions' to false. - * If you chose C, set 'removeDonateButton' to true + * You can disable any solicitations for donations by setting 'removeDonateButton' to true, + * but we'd appreciate it if you didn't! */ - allowSubscriptions: true, - removeDonateButton: false, + //removeDonateButton: false, - /* - * By default, CryptPad also contacts our accounts server once a day to check for changes in - * the people who have accounts. This check-in will also send the version of your CryptPad - * instance and your email so we can reach you if we are aware of a serious problem. We will - * never sell it or send you marketing mail. If you want to block this check-in and remain - * completely invisible, set this and allowSubscriptions both to false. + /* CryptPad will display a point of contact for your instance on its contact page + * (/contact.html) if you provide it below. */ adminEmail: 'i.did.not.read.my.config@cryptpad.fr', - /* Sales coming from your server will be identified by your domain - * - * If you are using CryptPad in a business context, please consider taking a support contract - * by contacting sales@cryptpad.fr - */ - myDomain: _domain, - /* - * If you are using CryptPad internally and you want to increase the per-user storage limit, - * change the following value. + * By default, CryptPad contacts one of our servers once a day. + * This check-in will also send some very basic information about your instance including its + * version and the adminEmail so we can reach you if we are aware of a serious problem. + * We will never sell it or send you marketing mail. * - * Please note: This limit is what makes people subscribe and what pays for CryptPad - * development. Running a public instance that provides a "better deal" than cryptpad.fr - * is effectively using the project against itself. + * If you want to block this check-in and remain set 'blockDailyCheck' to true. */ - defaultStorageLimit: 50 * 1024 * 1024, + //blockDailyCheck: false, /* - * CryptPad allows administrators to give custom limits to their friends. - * add an entry for each friend, identified by their user id, - * which can be found on the settings page. Include a 'limit' (number of bytes), - * a 'plan' (string), and a 'note' (string). + * By default users get 50MB of storage by registering on an instance. + * You can set this value to whatever you want. * - * hint: 1GB is 1024 * 1024 * 1024 bytes + * hint: 50MB is 50 * 1024 * 1024 */ - customLimits: { - /* - "https://my.awesome.website/user/#/1/cryptpad-user1/YZgXQxKR0Rcb6r6CmxHPdAGLVludrAF2lEnkbx1vVOo=": { - limit: 20 * 1024 * 1024 * 1024, - plan: 'insider', - note: 'storage space donated by my.awesome.website' - }, - "https://my.awesome.website/user/#/1/cryptpad-user2/GdflkgdlkjeworijfkldfsdflkjeEAsdlEnkbx1vVOo=": { - limit: 10 * 1024 * 1024 * 1024, - plan: 'insider', - note: 'storage space donated by my.awesome.website' - } - */ - }, + //defaultStorageLimit: 50 * 1024 * 1024, + /* ===================== * STORAGE * ===================== */ - /* By default the CryptPad server will run scheduled tasks every five minutes - * If you want to run scheduled tasks in a separate process (like a crontab) - * you can disable this behaviour by setting the following value to true - */ - disableIntegratedTasks: false, - /* Pads that are not 'pinned' by any registered user can be set to expire * after a configurable number of days of inactivity (default 90 days). * The value can be changed or set to false to remove expiration. * Expired pads can then be removed using a cron job calling the - * `delete-inactive.js` script with node + * `evict-inactive.js` script with node + * + * defaults to 90 days if nothing is provided */ - inactiveTime: 90, // days + //inactiveTime: 90, // days /* CryptPad archives some data instead of deleting it outright. * This archived data still takes up space and so you'll probably still want to @@ -241,31 +180,46 @@ module.exports = { * deletion. Set this value to the number of days you'd like to retain * archived data before it's removed permanently. * + * defaults to 15 days if nothing is provided */ - archiveRetentionTime: 15, + //archiveRetentionTime: 15, /* Max Upload Size (bytes) * this sets the maximum size of any one file uploaded to the server. * anything larger than this size will be rejected + * defaults to 20MB if no value is provided */ - maxUploadSize: 20 * 1024 * 1024, - - // XXX - premiumUploadSize: 100 * 1024 * 1024, - - /* ===================== - * HARDWARE RELATED - * ===================== */ + //maxUploadSize: 20 * 1024 * 1024, - /* CryptPad's file storage adaptor closes unused files after a configurable - * number of milliseconds (default 30000 (30 seconds)) + /* + * CryptPad allows administrators to give custom limits to their friends. + * add an entry for each friend, identified by their user id, + * which can be found on the settings page. Include a 'limit' (number of bytes), + * a 'plan' (string), and a 'note' (string). + * + * hint: 1GB is 1024 * 1024 * 1024 bytes */ - channelExpirationMs: 30000, +/* + customLimits: { + "https://my.awesome.website/user/#/1/cryptpad-user1/YZgXQxKR0Rcb6r6CmxHPdAGLVludrAF2lEnkbx1vVOo=": { + limit: 20 * 1024 * 1024 * 1024, + plan: 'insider', + note: 'storage space donated by my.awesome.website' + }, + "https://my.awesome.website/user/#/1/cryptpad-user2/GdflkgdlkjeworijfkldfsdflkjeEAsdlEnkbx1vVOo=": { + limit: 10 * 1024 * 1024 * 1024, + plan: 'insider', + note: 'storage space donated by my.awesome.website' + } + }, +*/ - /* CryptPad's file storage adaptor is limited by the number of open files. - * When the adaptor reaches openFileLimit, it will clean up older files + /* Users with premium accounts (those with a plan included in their customLimit) + * can benefit from an increased upload size limit. By default they are restricted to the same + * upload size as any other registered user. + * */ - openFileLimit: 2048, + //premiumUploadSize: 100 * 1024 * 1024, /* ===================== * DATABASE VOLUMES diff --git a/lib/commands/quota.js b/lib/commands/quota.js index 74c4eca44..72d04cc68 100644 --- a/lib/commands/quota.js +++ b/lib/commands/quota.js @@ -38,6 +38,7 @@ Quota.updateCachedLimits = function (Env, cb) { if (Env.adminEmail === false) { Quota.applyCustomLimits(Env); if (Env.allowSubscriptions === false) { return; } + if (Env.blockDailyCheck === true) { return; } throw new Error("allowSubscriptions must be false if adminEmail is false"); } diff --git a/lib/defaults.js b/lib/defaults.js new file mode 100644 index 000000000..6d8ec5e04 --- /dev/null +++ b/lib/defaults.js @@ -0,0 +1,86 @@ +var Default = module.exports; + +Default.commonCSP = function (domain) { + domain = ' ' + domain; + // Content-Security-Policy + + return [ + "default-src 'none'", + "style-src 'unsafe-inline' 'self' " + domain, + "font-src 'self' data:" + domain, + + /* child-src is used to restrict iframes to a set of allowed domains. + * connect-src is used to restrict what domains can connect to the websocket. + * + * it is recommended that you configure these fields to match the + * domain which will serve your CryptPad instance. + */ + "child-src blob: *", + // IE/Edge + "frame-src blob: *", + + /* this allows connections over secure or insecure websockets + if you are deploying to production, you'll probably want to remove + the ws://* directive, and change '*' to your domain + */ + "connect-src 'self' ws: wss: blob:" + domain, + + // data: is used by codemirror + "img-src 'self' data: blob:" + domain, + "media-src * blob:", + + // for accounts.cryptpad.fr authentication and cross-domain iframe sandbox + "frame-ancestors *", + "" + ]; +}; + +Default.contentSecurity = function (domain) { + return (Default.commonCSP(domain).join('; ') + "script-src 'self' " + domain).replace(/\s+/g, ' '); +}; + +Default.padContentSecurity = function (domain) { + return (Default.commonCSP(domain).join('; ') + "script-src 'self' 'unsafe-eval' 'unsafe-inline' " + domain).replace(/\s+/g, ' '); +}; + +Default.httpHeaders = function () { + return { + "X-XSS-Protection": "1; mode=block", + "X-Content-Type-Options": "nosniff", + "Access-Control-Allow-Origin": "*" + }; +}; + +Default.mainPages = function () { + return [ + 'index', + 'privacy', + 'terms', + 'about', + 'contact', + 'what-is-cryptpad', + 'features', + 'faq', + 'maintenance' + ]; +}; + +/* By default the CryptPad server will run scheduled tasks every five minutes + * If you want to run scheduled tasks in a separate process (like a crontab) + * you can disable this behaviour by setting the following value to true + */ + //disableIntegratedTasks: false, + + /* CryptPad's file storage adaptor closes unused files after a configurable + * number of milliseconds (default 30000 (30 seconds)) + */ +// channelExpirationMs: 30000, + + /* CryptPad's file storage adaptor is limited by the number of open files. + * When the adaptor reaches openFileLimit, it will clean up older files + */ + //openFileLimit: 2048, + + + + diff --git a/lib/historyKeeper.js b/lib/historyKeeper.js index ed67602bd..3a27b228a 100644 --- a/lib/historyKeeper.js +++ b/lib/historyKeeper.js @@ -65,10 +65,13 @@ module.exports.create = function (config, cb) { WARN: WARN, flushCache: config.flushCache, adminEmail: config.adminEmail, - allowSubscriptions: config.allowSubscriptions, - myDomain: config.myDomain, + allowSubscriptions: config.allowSubscriptions === true, + blockDailyCheck: config.blockDailyCheck === true, + + myDomain: config.httpUnsafeOrigin, + // XXX not included in the config... mySubdomain: config.mySubdomain, - customLimits: config.customLimits, + customLimits: config.customLimits || {}, // FIXME this attribute isn't in the default conf // but it is referenced in Quota domain: config.domain diff --git a/lib/load-config.js b/lib/load-config.js index 0756c2df4..0b861b826 100644 --- a/lib/load-config.js +++ b/lib/load-config.js @@ -1,7 +1,7 @@ /* jslint node: true */ "use strict"; var config; -var configPath = process.env.CRYPTPAD_CONFIG || "../config/config"; +var configPath = process.env.CRYPTPAD_CONFIG || "../config/config.js"; try { config = require(configPath); if (config.adminEmail === 'i.did.not.read.my.config@cryptpad.fr') { @@ -18,5 +18,23 @@ try { } config = require("../config/config.example"); } + +var isPositiveNumber = function (n) { + return (!isNaN(n) && n >= 0); +}; + +if (!isPositiveNumber(config.inactiveTime)) { + config.inactiveTime = 90; +} +if (!isPositiveNumber(config.archiveRetentionTime)) { + config.archiveRetentionTime = 90; +} +if (!isPositiveNumber(config.maxUploadSize)) { + config.maxUploadSize = 20 * 1024 * 1024; +} +if (!isPositiveNumber(config.defaultStorageLimit)) { + config.defaultStorageLimit = 50 * 1024 * 1024; +} + module.exports = config; diff --git a/scripts/check-accounts.js b/scripts/check-accounts.js index 4d0067d43..9e75da0c6 100644 --- a/scripts/check-accounts.js +++ b/scripts/check-accounts.js @@ -4,7 +4,7 @@ var Config = require("../lib/load-config"); var Package = require("../package.json"); var body = JSON.stringify({ - domain: Config.myDomain, + domain: Config.myDomain || Config.httpUnsafeOrigin, subdomain: Config.mySubdomain || null, adminEmail: Config.adminEmail, version: Package.version, diff --git a/server.js b/server.js index ddf9fc8b0..d7f663d4e 100644 --- a/server.js +++ b/server.js @@ -8,6 +8,7 @@ var Package = require('./package.json'); var Path = require("path"); var nThen = require("nthen"); var Util = require("./lib/common-util"); +var Default = require("./lib/defaults"); var config = require("./lib/load-config"); @@ -35,6 +36,47 @@ if (process.env.PACKAGE) { FRESH_KEY = +new Date(); } +(function () { + // you absolutely must provide an 'httpUnsafeOrigin' + if (typeof(config.httpUnsafeOrigin) !== 'string') { + throw new Error("No 'httpUnsafeOrigin' provided"); + } + + config.httpUnsafeOrigin = config.httpUnsafeOrigin.trim(); + + // fall back to listening on a local address + // if httpAddress is not a string + if (typeof(config.httpAddress) !== 'string') { + config.httpAddress = '127.0.0.1'; + } + + // listen on port 3000 if a valid port number was not provided + if (typeof(config.httpPort) !== 'number' || config.httpPort > 65535) { + config.httpPort = 3000; + } + + if (typeof(httpSafeOrigin) !== 'string') { + if (typeof(config.httpSafePort) !== 'number') { + config.httpSafePort = config.httpPort + 1; + } + + if (DEV_MODE) { return; } + console.log(` + m m mm mmmmm mm m mmmmm mm m mmm m + # # # ## # "# #"m # # #"m # m" " # + " #"# # # # #mmmm" # #m # # # #m # # mm # + ## ##" #mm# # "m # # # # # # # # # + # # # # # " # ## mm#mm # ## "mmm" # +`); + + console.log("\nNo 'httpSafeOrigin' provided."); + console.log("Your configuration probably isn't taking advantage of all of CryptPad's security features!"); + console.log("This is acceptable for development, otherwise your users may be at risk.\n"); + + console.log("Serving sandboxed content via port %s.\nThis is probably not what you want for a production instance!\n", config.httpSafePort); + } +}()); + var configCache = {}; config.flushCache = function () { configCache = {}; @@ -47,11 +89,21 @@ config.flushCache = function () { const clone = (x) => (JSON.parse(JSON.stringify(x))); var setHeaders = (function () { - if (typeof(config.httpHeaders) !== 'object') { return function () {}; } + // load the default http headers unless the admin has provided their own via the config file + var headers; + + var custom = config.httpHeaders; + // if the admin provided valid http headers then use them + if (custom && typeof(custom) === 'object' && !Array.isArray(custom)) { + headers = clone(custom); + } else { + // otherwise use the default + headers = Default.httpHeaders(); + } - const headers = clone(config.httpHeaders); - if (config.contentSecurity) { - headers['Content-Security-Policy'] = clone(config.contentSecurity); + // next define the base Content Security Policy (CSP) headers + if (typeof(config.contentSecurity) === 'string') { + headers['Content-Security-Policy'] = config.contentSecurity; if (!/;$/.test(headers['Content-Security-Policy'])) { headers['Content-Security-Policy'] += ';' } if (headers['Content-Security-Policy'].indexOf('frame-ancestors') === -1) { // backward compat for those who do not merge the new version of the config @@ -59,10 +111,16 @@ var setHeaders = (function () { // It also fixes the cross-domain iframe. headers['Content-Security-Policy'] += "frame-ancestors *;"; } + } else { + // use the default CSP headers constructed with your domain + headers['Content-Security-Policy'] = Default.contentSecurity(config.httpUnsafeOrigin); } + const padHeaders = clone(headers); - if (config.padContentSecurity) { - padHeaders['Content-Security-Policy'] = clone(config.padContentSecurity); + if (typeof(config.padContentSecurity) === 'string') { + padHeaders['Content-Security-Policy'] = config.padContentSecurity; + } else { + padHeaders['Content-Security-Policy'] = Default.padContentSecurity(config.httpUnsafeOrigin); } if (Object.keys(headers).length) { return function (req, res) { @@ -116,7 +174,7 @@ app.use(Express.static(__dirname + '/www')); // FIXME I think this is a regression caused by a recent PR // correct this hack without breaking the contributor's intended behaviour. -var mainPages = config.mainPages || ['index', 'privacy', 'terms', 'about', 'contact']; +var mainPages = config.mainPages || Default.mainPages(); var mainPagePattern = new RegExp('^\/(' + mainPages.join('|') + ').html$'); app.get(mainPagePattern, Express.static(__dirname + '/customize')); app.get(mainPagePattern, Express.static(__dirname + '/customize.dist')); @@ -163,7 +221,7 @@ var serveConfig = (function () { removeDonateButton: (config.removeDonateButton === true), allowSubscriptions: (config.allowSubscriptions === true), websocketPath: config.externalWebsocketURL, - httpUnsafeOrigin: config.httpUnsafeOrigin.replace(/^\s*/, ''), + httpUnsafeOrigin: config.httpUnsafeOrigin, adminEmail: config.adminEmail, adminKeys: admins, inactiveTime: config.inactiveTime,