diff --git a/customize.dist/src/less2/pages/page-checkup.less b/customize.dist/src/less2/pages/page-checkup.less index 9119a4b72..751db910d 100644 --- a/customize.dist/src/less2/pages/page-checkup.less +++ b/customize.dist/src/less2/pages/page-checkup.less @@ -58,6 +58,13 @@ html, body { border: 1px solid red; background-color: @cp_alerts-danger-bg; color: @cp_alerts-danger-text; + code { + word-break: keep-all; + font-style: italic; + } + a { + color: @cryptpad_color_link; + } } iframe { diff --git a/www/assert/assertions.js b/www/assert/assertions.js index a1f6944fb..8e2351cf7 100644 --- a/www/assert/assertions.js +++ b/www/assert/assertions.js @@ -3,8 +3,9 @@ define([], function () { var failMessages = []; var passed = 0; var ASSERTS = []; - + var MESSAGES = []; var assert = function (test, msg) { + MESSAGES.push(msg || false); ASSERTS.push(function (cb, i) { test(function (result) { if (result === true) { @@ -17,7 +18,7 @@ define([], function () { output: result, }); } - }); + }, msg); }); }; diff --git a/www/checkup/main.js b/www/checkup/main.js index 7afa06abf..de160e139 100644 --- a/www/checkup/main.js +++ b/www/checkup/main.js @@ -19,38 +19,100 @@ define([ ], function ($, ApiConfig, Assertions, h, Messages, DomReady, nThen, SFCommonO, Login, Hash, Util, Pinpad, NetConfig) { - var assert = Assertions(); - + var Assert = Assertions(); var trimSlashes = function (s) { if (typeof(s) !== 'string') { return s; } return s.replace(/\/+$/, ''); }; - var _alert = function (content) { - return h('span.advisory-text', content); + var assert = function (f, msg) { + Assert(f, msg || h('span.advisory-text')); + }; + + var CONFIG_PATH = function () { + return h('code', 'cryptpad/config/config.js'); + }; + var API_CONFIG_LINK = function () { + return h('a', { + href: '/api/config', + target: '_blank', + }, '/api/config'); + }; + + var RESTART_WARNING = function () { + return h('span', [ + 'Changes to ', + CONFIG_PATH(), + ' will require a server restart in order for ', + API_CONFIG_LINK(), + ' to be updated.', + ]); }; var trimmedSafe = trimSlashes(ApiConfig.httpSafeOrigin); var trimmedUnsafe = trimSlashes(ApiConfig.httpUnsafeOrigin); - assert(function (cb) { + assert(function (cb, msg) { + msg.appendChild(h('span', [ + "CryptPad's sandbox requires that both ", + h('code', 'httpUnsafeOrigin'), + ' and ', + h('code', 'httpSafeOrigin'), + " be configured in ", + CONFIG_PATH(), + '. ', + RESTART_WARNING(), + ])); + //console.error(trimmedSafe, trimmedUnsafe); cb(Boolean(trimmedSafe && trimmedUnsafe)); - }, _alert("Sandbox configuration: ensure that both httpUnsafeOrigin and httpSafeOrigin are defined")); + }); + + assert(function (cb, msg) { + msg.appendChild(h('span', [ + h('code', 'httpUnsafeOrigin'), + ' and ', + h('code', 'httpSafeOrigin'), + ' are equivalent. ', + "In order for CryptPad's security features to be as effective as intended they must be different. ", + "See ", + CONFIG_PATH(), + '. ', + RESTART_WARNING(), + ])); - assert(function (cb) { return void cb(trimmedSafe !== trimmedUnsafe); - }, _alert('Sandbox configuration: httpUnsafeOrigin !== httpSafeOrigin')); + }); + + assert(function (cb, msg) { + msg.appendChild(h('span', [ + h('code', 'httpUnsafeOrigin'), + ' and ', + h('code', 'httpSafeOrigin'), + ' must not contain trailing slashes. This can be configured in ', + CONFIG_PATH(), + '. ', + RESTART_WARNING(), + ])); + cb(trimmedSafe === ApiConfig.httpSafeOrigin && trimmedUnsafe === ApiConfig.httpUnsafeOrigin); + }); - assert(function (cb) { - cb(trimmedSafe === ApiConfig.httpSafeOrigin); - }, "httpSafeOrigin must not have a trailing slash"); + assert(function (cb, msg) { + msg.appendChild(h("span", [ + "It appears that you are trying to load this page via an origin other than its main domain (", + h('code', ApiConfig.httpUnsafeOrigin), - assert(function (cb) { + "). See the ", + h('code', 'httpUnsafeOrigin'), + " option in ", + CONFIG_PATH(), + " which is exposed via ", + API_CONFIG_LINK(), + '.', + ])); var origin = window.location.origin; return void cb(ApiConfig.httpUnsafeOrigin === origin); - }, _alert('Sandbox configuration: loading via httpUnsafeOrigin')); - + }); var checkAvailability = function (url, cb) { $.ajax({ @@ -62,12 +124,38 @@ define([ }); }; - assert(function (cb) { + assert(function (cb, msg) { + msg.appendChild(h('span', [ + "The main domain (configured via ", + h('code', 'httpUnsafeOrigin'), + ' as ', + ApiConfig.httpUnsafeOrigin, + ' in ', + CONFIG_PATH(), + ' and exposed via ', + API_CONFIG_LINK(), + ') could not be reached.', + ])); + checkAvailability(trimmedUnsafe, cb); - }, _alert("Main domain is not available")); + }); // Try loading an iframe on the safe domain - assert(function (cb) { + assert(function (cb, msg) { + msg.appendChild(h('span', [ + "Your browser was not able to load an iframe using the origin specified as ", + h('code', "httpSafeOrigin"), + " (", + ApiConfig.httpSafeOrigin, + ") in ", + CONFIG_PATH(), + ". This can be caused by an invalid ", + h('code', 'httpUnsafeDomain'), + ', invalid CSP configuration in your reverse proxy, invalid SSL certificates, and many other factors. ', + 'More information about your particular error may be found in your browser console. ', + RESTART_WARNING(), + ])); + var to; nThen(function (waitFor) { DomReady.onReady(waitFor()); @@ -82,16 +170,22 @@ define([ clearTimeout(to); cb(true); }); - }, _alert("Sandbox domain is not available")); + }); // Test Websocket var evWSError = Util.mkEvent(true); - assert(function (cb) { + assert(function (_cb, msg) { + var cb = Util.once(Util.both(_cb, function (err) { + if (typeof(err) === 'string') { + msg.innerText = err; + } + })); + var ws = new WebSocket(NetConfig.getWebsocketURL()); var to = setTimeout(function () { console.error('Websocket TIMEOUT'); evWSError.fire(); - cb('TIMEOUT (5 seconds)'); + cb('Could not connect to the websocket server within 5 seconds.'); }, 5000); ws.onopen = function () { clearTimeout(to); @@ -99,14 +193,25 @@ define([ }; ws.onerror = function (err) { clearTimeout(to); - console.error('Websocket error', err); + console.error('[Websocket error]', err); evWSError.fire(); - cb('WebSocket error: check your console'); + cb('Unable to connect to the websocket server. More information may be available in your browser console ([Websocket error]).'); }; - }, _alert("Websocket is not available")); + }); // Test login block - assert(function (cb) { + assert(function (cb, msg) { + msg.appendChild(h('span', [ + "Unable to create, retrieve, or remove encrypted credentials from the server. ", + "This is most commonly caused by a mismatch between the value of the ", + h('code', 'blockPath'), + ' value configured in ', + CONFIG_PATH(), + " and the corresponding settings in your reverse proxy's configuration file,", + " but it can also be explained by a websocket error. ", + RESTART_WARNING(), + ])); + var bytes = new Uint8Array(Login.requiredBytes); var opt = Login.allocateBytes(bytes); @@ -132,7 +237,7 @@ define([ // If WebSockets aren't working, don't wait forever here evWSError.reg(function () { waitFor.abort(); - cb("No WebSocket (test number 6)"); + cb("No WebSocket available"); }); // Create proxy Login.loadUserObject(opt, waitFor(function (err, rt) { @@ -200,28 +305,78 @@ define([ cb(true); }); - }, _alert("Login block is not working (write/read/remove)")); + }); + + var sheetURL = '/common/onlyoffice/v4/web-apps/apps/spreadsheeteditor/main/index.html'; - assert(function (cb) { - var url = '/common/onlyoffice/v4/web-apps/apps/spreadsheeteditor/main/index.html'; + assert(function (cb, msg) { + msg.innerText = "Missing HTTP headers required for .xlsx export from sheets. "; + var url = sheetURL; var expect = { 'cross-origin-resource-policy': 'cross-origin', 'cross-origin-embedder-policy': 'require-corp', + //'cross-origin-opener-policy': 'same-origin', // FIXME this is in our nginx config but not server.js }; $.ajax(url, { complete: function (xhr) { cb(!Object.keys(expect).some(function (k) { var response = xhr.getResponseHeader(k); - console.log(k, response); - return response !== expect[k]; + if (response !== expect[k]) { + msg.appendChild(h('span', [ + 'A value of ', + h('code', expect[k]), + ' was expected for the ', + h('code', k), + ' HTTP header, but instead a value of "', + h('code', response), + '" was received.', + ])); + return true; // returning true indicates that a value is incorrect + } })); }, }); - }, _alert("Missing HTTP headers required for XLSX export")); + }); + + assert(function (cb, msg) { + msg.appendChild(h('span', [ + "The spreadsheet editor's code was not served with the required Content-Security Policy headers. ", + "This is most often caused by incorrectly configured sandbox parameters (", + h('code', 'httpUnsafeOrigin'), + ' and ', + h('code', 'httpSafeOrigin'), + ' in ', + CONFIG_PATH, + "), or settings in your reverse proxy's configuration which don't match your application server's config. ", + RESTART_WARNING(), + ])); + + $.ajax(sheetURL, { + complete: function (xhr) { + var csp = xhr.getResponseHeader('Content-Security-Policy'); + if (!/unsafe\-eval/.test(csp)) { + // OnlyOffice requires unsafe-eval + return cb(false); + } + if (!/unsafe\-inline/.test(csp)) { + // OnlyOffice also requires unsafe-inline + return cb(false); + } + //console.error('CSP', csp); + cb(true); + }, + }); + }); + + assert(function (cb, msg) { + msg.appendChild(h('span', [ + h('code', '/api/broadcast'), + " could not be loaded. This can be caused by an outdated application server or an incorrectly configured reverse proxy. ", + "Even if the most recent code has been downloaded it's possible the application server has not been restarted. ", + "Your browser console may provide more details as to why this resource could not be loaded. ", + ])); - assert(function (cb) { - cb(true); $.ajax('/api/broadcast', { dataType: 'text', complete: function (xhr) { @@ -229,7 +384,7 @@ define([ cb(xhr.status === 200); }, }); - }, _alert("/api/broadcast is not available")); + }); var row = function (cells) { return h('tr', cells.map(function (cell) { @@ -249,7 +404,7 @@ define([ var completed = 0; var $progress = $('#cp-progress'); - assert.run(function (state) { + Assert.run(function (state) { var errors = state.errors; var failed = errors.length;