diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d333700b..e667c55b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,38 @@ +# 4.6.0 + +## Goals + +* work on polls/surveys +* stabilize and implement tests + +## Update notes + +* checkup/server/config + * test for anti-FLoC header + * add anti-FloC header to server so the default dev server passes all tests + * update NGINX example to avoid duplicated headers + * simplify dev server headers + * adjust table borders for dark/light mode + * say what headers are wrong +* rename exported object in `application_config_internal.js` to avoid copypasta errors +* `AppConfig.disableAnonymousPadCreation = false;` + * note that it's only enforced client-side +* update lodash devDependency + +## Features + +* generate `supportMailbox` keys via the admin panel + * document this +* markdown preview + * code blocks are full width + +## Bug fixes + +* fix opening links from temporary shared folders on iphone or other contexts that do not support shared workers +* show "features" instead of "pricing" in static pages' footer when premium subscriptions are not available +* use preferred 12/24h time format in date picker +* add error handling to admin panel decree RPCs + # 4.5.0 ## Goals diff --git a/config/config.example.js b/config/config.example.js index f53f3bb5b..d05b985af 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -122,19 +122,6 @@ module.exports = { ], */ - /* CryptPad's administration panel includes a "support" tab - * wherein administrators with a secret key can view messages - * sent from users via the encrypted forms on the /support/ page - * - * To enable this functionality: - * run `node ./scripts/generate-admin-keys.js` - * save the public key in your config in the value below - * add the private key via the admin panel - * and back it up in a secure manner - * - */ - // supportMailboxPublicKey: "", - /* 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. * @@ -147,11 +134,6 @@ module.exports = { */ //removeDonateButton: 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', - /* * 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 diff --git a/customize.dist/pages.js b/customize.dist/pages.js index e3ae2b83e..aaf7c60b3 100644 --- a/customize.dist/pages.js +++ b/customize.dist/pages.js @@ -43,6 +43,17 @@ define([ return Pages.externalLink(el, Pages.localizeDocsLink(href)); }; + var accounts = Pages.accounts = { + donateURL: AppConfig.donateURL || "https://opencollective.com/cryptpad/", + upgradeURL: AppConfig.upgradeURL + }; + + Pages.areSubscriptionsAllowed = function () { + try { + return ApiConfig.allowSubscriptions && accounts.upgradeURL && !ApiConfig.restrictRegistration; + } catch (err) { return void console.error(err); } + }; + var languageSelector = function () { var options = []; var languages = Msg._languages; @@ -94,7 +105,7 @@ define([ var imprintUrl = AppConfig.imprint && (typeof(AppConfig.imprint) === "boolean" ? '/imprint.html' : AppConfig.imprint); - Pages.versionString = "v4.5.0"; + Pages.versionString = "v4.6.0"; // used for the about menu @@ -133,7 +144,7 @@ define([ footerCol('footer_product', [ footLink('/what-is-cryptpad.html', 'topbar_whatIsCryptpad'), Pages.docsLink, - footLink('/features.html', 'pricing'), + footLink('/features.html', Pages.areSubscriptionsAllowed()? 'pricing': 'features'), // Messages.pricing, Messages.features Pages.githubLink, footLink('https://opencollective.com/cryptpad/contribute/', 'footer_donate'), ]), diff --git a/customize.dist/pages/features.js b/customize.dist/pages/features.js index ee82d5d67..1dfd2417f 100644 --- a/customize.dist/pages/features.js +++ b/customize.dist/pages/features.js @@ -8,10 +8,7 @@ define([ '/api/config', '/common/common-ui-elements.js', ], function ($, h, Msg, AppConfig, LocalStore, Pages, Config, UIElements) { - var accounts = { - donateURL: AppConfig.donateURL || "https://opencollective.com/cryptpad/", - upgradeURL: AppConfig.upgradeURL - }; + var accounts = Pages.accounts; return function () { Msg.features_f_apps_note = AppConfig.availablePadTypes.map(function (app) { @@ -145,10 +142,11 @@ define([ ]), ]), ]); - var availableFeatures = - (Config.allowSubscriptions && accounts.upgradeURL && !Config.restrictRegistration) ? - [anonymousFeatures, registeredFeatures, premiumFeatures] : - [anonymousFeatures, registeredFeatures]; + var availableFeatures = [ + anonymousFeatures, + registeredFeatures, + Pages.areSubscriptionsAllowed() ? premiumFeatures: undefined, + ]; return h('div#cp-main', [ Pages.infopageTopbar(), diff --git a/customize.dist/src/less2/include/markdown.less b/customize.dist/src/less2/include/markdown.less index 717b49990..3943f128d 100644 --- a/customize.dist/src/less2/include/markdown.less +++ b/customize.dist/src/less2/include/markdown.less @@ -194,7 +194,7 @@ pre > code { display: block; position: relative; - width: 90%; + width: 100%; margin: auto; padding: 5px; overflow-x: auto; diff --git a/docs/example.nginx.conf b/docs/example.nginx.conf index 78e4f30ce..a51a1ecaa 100644 --- a/docs/example.nginx.conf +++ b/docs/example.nginx.conf @@ -167,6 +167,13 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # These settings prevent both NGINX and the API server + # from setting the same headers and creating duplicates + proxy_hide_header Cross-Origin-Resource-Policy; + add_header Cross-Origin-Resource-Policy cross-origin; + proxy_hide_header Cross-Origin-Embedder-Policy; + add_header Cross-Origin-Embedder-Policy require-corp; } # encrypted blobs are immutable and are thus cached for a year diff --git a/lib/commands/admin-rpc.js b/lib/commands/admin-rpc.js index 32338b772..9ab86bdf2 100644 --- a/lib/commands/admin-rpc.js +++ b/lib/commands/admin-rpc.js @@ -348,6 +348,9 @@ var commands = { CLEAR_CACHED_CHANNEL_INDEX: clearChannelIndex, GET_CACHED_CHANNEL_INDEX: getChannelIndex, + // TODO implement admin historyTrim + // TODO implement kick from channel + // TODO implement force-disconnect user(s)? CLEAR_CACHED_CHANNEL_METADATA: clearChannelMetadata, GET_CACHED_CHANNEL_METADATA: getChannelMetadata, diff --git a/lib/defaults.js b/lib/defaults.js index 3d5e74576..635e155be 100644 --- a/lib/defaults.js +++ b/lib/defaults.js @@ -48,6 +48,7 @@ Default.httpHeaders = function () { "X-XSS-Protection": "1; mode=block", "X-Content-Type-Options": "nosniff", "Access-Control-Allow-Origin": "*", + "Permissions-policy":"interest-cohort=()" }; }; Default.mainPages = function () { diff --git a/lib/hk-util.js b/lib/hk-util.js index 7c244c174..5a96970d0 100644 --- a/lib/hk-util.js +++ b/lib/hk-util.js @@ -701,6 +701,7 @@ const handleGetHistory = function (Env, Server, seq, userId, parsed) { } if (msgCount === 0 && !metadata_cache[channelName] && Server.channelContainsUser(channelName, userId)) { + // TODO this might be a good place to reject channel creation by anonymous users handleFirstMessage(Env, channelName, metadata); Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, JSON.stringify(metadata)]); } diff --git a/package-lock.json b/package-lock.json index 75b03cfe1..b1d794abe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cryptpad", - "version": "4.5.0", + "version": "4.6.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 3314ffd17..8526afc8a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cryptpad", "description": "realtime collaborative visual editor with zero knowlege server", - "version": "4.5.0", + "version": "4.6.0", "license": "AGPL-3.0+", "repository": { "type": "git", diff --git a/server.js b/server.js index 3a71f83b8..1824cf59c 100644 --- a/server.js +++ b/server.js @@ -108,28 +108,21 @@ var setHeaders = (function () { // apply a bunch of cross-origin headers for XLSX export in FF and printing elsewhere applyHeaderMap(res, { "Cross-Origin-Opener-Policy": /^\/sheet\//.test(req.url)? 'same-origin': '', - "Cross-Origin-Embedder-Policy": 'require-corp', }); if (Env.NO_SANDBOX) { // handles correct configuration for local development // https://stackoverflow.com/questions/11531121/add-duplicate-http-response-headers-in-nodejs applyHeaderMap(res, { "Cross-Origin-Resource-Policy": 'cross-origin', + "Cross-Origin-Embedder-Policy": 'require-corp', }); } - // Don't set CSP headers on /api/config because they aren't necessary and they cause problems + // Don't set CSP headers on /api/ endpoints + // because they aren't necessary and they cause problems // when duplicated by NGINX in production environments - if (/^\/api\/(broadcast|config)/.test(req.url)) { - /* - if (Env.NO_SANDBOX) { - applyHeaderMap(res, { - "Cross-Origin-Resource-Policy": 'cross-origin', - }); - } - */ - return; - } + if (/^\/api\/(broadcast|config)/.test(req.url)) { return; } + applyHeaderMap(res, { "Cross-Origin-Resource-Policy": 'cross-origin', }); diff --git a/www/admin/inner.js b/www/admin/inner.js index ffc9a561c..3956d7e50 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -16,6 +16,7 @@ define([ '/support/ui.js', '/lib/datepicker/flatpickr.js', + '/bower_components/tweetnacl/nacl-fast.min.js', 'css!/lib/datepicker/flatpickr.min.css', 'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', @@ -44,6 +45,7 @@ define([ 'instanceStatus': {} }; + var Nacl = window.nacl; var common; var sFrameChan; @@ -54,6 +56,7 @@ define([ 'cp-admin-archive', 'cp-admin-unarchive', 'cp-admin-registration', + 'cp-admin-email' ], 'quota': [ // Msg.admin_cat_quota 'cp-admin-defaultlimit', @@ -71,7 +74,8 @@ define([ ], 'support': [ // Msg.admin_cat_support 'cp-admin-support-list', - 'cp-admin-support-init' + 'cp-admin-support-init', + 'cp-admin-support-priv', ], 'broadcast': [ // Msg.admin_cat_broadcast 'cp-admin-maintenance', @@ -267,8 +271,11 @@ define([ sFrameChan.query('Q_ADMIN_RPC', { cmd: 'ADMIN_DECREE', data: ['RESTRICT_REGISTRATION', [val]] - }, function (e) { - if (e) { UI.warn(Messages.error); console.error(e); } + }, function (e, response) { + if (e || response.error) { + UI.warn(Messages.error); + console.error(e, response); + } APP.updateStatus(function () { spinner.done(); state = APP.instanceStatus.restrictRegistration; @@ -282,6 +289,45 @@ define([ return $div; }; + create['email'] = function () { + var key = 'email'; + var $div = makeBlock(key, true); // Msg.admin_emailHint, Msg.admin_emailTitle, Msg.admin_emailButton + var $button = $div.find('button'); + + var input = h('input', { + type: 'email', + value: ApiConfig.adminEmail || '' + }); + var $input = $(input); + var innerDiv = h('div.cp-admin-setlimit-form', input); + var spinner = UI.makeSpinner($(innerDiv)); + + $button.click(function () { + if (!$input.val()) { return; } + spinner.spin(); + $button.attr('disabled', 'disabled'); + sFrameChan.query('Q_ADMIN_RPC', { + cmd: 'ADMIN_DECREE', + data: ['SET_ADMIN_EMAIL', [$input.val()]] + }, function (e, response) { + $button.removeAttr('disabled'); + if (e || response.error) { + UI.warn(Messages.error); + $input.val(''); + console.error(e, response); + spinner.hide(); + return; + } + spinner.done(); + UI.log(Messages.saved); + }); + }); + + $button.before(innerDiv); + + return $div; + }; + var getPrettySize = UIElements.prettySize; create['defaultlimit'] = function () { @@ -316,8 +362,11 @@ define([ sFrameChan.query('Q_ADMIN_RPC', { cmd: 'ADMIN_DECREE', data: ['UPDATE_DEFAULT_STORAGE', data] - }, function (e) { - if (e) { UI.warn(Messages.error); return void console.error(e); } + }, function (e, response) { + if (e || response.error) { + UI.warn(Messages.error); + return void console.error(e, response); + } var limit = getPrettySize(l); $div.find('.cp-admin-defaultlimit-value').text(Messages._getKey('admin_limit', [limit])); }); @@ -448,8 +497,12 @@ define([ sFrameChan.query('Q_ADMIN_RPC', { cmd: 'ADMIN_DECREE', data: ['RM_QUOTA', data] - }, function (e) { - if (e) { UI.warn(Messages.error); console.error(e); } + }, function (e, response) { + if (e || response.error) { + UI.warn(Messages.error); + console.error(e, response); + return; + } APP.refreshLimits(); $key.val(''); }); @@ -462,8 +515,12 @@ define([ sFrameChan.query('Q_ADMIN_RPC', { cmd: 'ADMIN_DECREE', data: ['SET_QUOTA', data] - }, function (e) { - if (e) { UI.warn(Messages.error); console.error(e); } + }, function (e, response) { + if (e || response.error) { + UI.warn(Messages.error); + console.error(e, response); + return; + } APP.refreshLimits(); $key.val(''); }); @@ -637,8 +694,13 @@ define([ }; var supportKey = ApiConfig.supportMailbox; + var checkAdminKey = function (priv) { + if (!supportKey) { return; } + return Hash.checkBoxKeyPair(priv, supportKey); + }; + create['support-list'] = function () { - if (!supportKey || !APP.privateKey) { return; } + if (!supportKey || !APP.privateKey || !checkAdminKey(APP.privateKey)) { return; } var $container = makeBlock('support-list'); // Msg.admin_supportListHint, .admin_supportListTitle var $div = $(h('div.cp-support-container')).appendTo($container); @@ -898,16 +960,63 @@ define([ return $container; }; + create['support-priv'] = function () { + if (!supportKey || !APP.privateKey || !checkAdminKey(APP.privateKey)) { return; } - var checkAdminKey = function (priv) { - if (!supportKey) { return; } - return Hash.checkBoxKeyPair(priv, supportKey); + var $div = makeBlock('support-priv', true); // Msg.admin_supportPrivHint, .admin_supportPrivTitle, .admin_supportPrivButton + var $button = $div.find('button').click(function () { + $button.remove(); + var $selectable = $(UI.dialog.selectable(APP.privateKey)).css({ 'max-width': '28em' }); + $div.append($selectable); + }); + return $div; }; - create['support-init'] = function () { var $div = makeBlock('support-init'); // Msg.admin_supportInitHint, .admin_supportInitTitle if (!supportKey) { - $div.append(h('p', Messages.admin_supportInitHelp)); + (function () { + $div.append(h('p', Messages.admin_supportInitHelp)); + var button = h('button.btn.btn-primary', Messages.admin_supportInitGenerate); + var $button = $(button).appendTo($div); + $div.append($button); + var spinner = UI.makeSpinner($div); + $button.click(function () { + spinner.spin(); + $button.attr('disabled', 'disabled'); + var keyPair = Nacl.box.keyPair(); + var pub = Nacl.util.encodeBase64(keyPair.publicKey); + var priv = Nacl.util.encodeBase64(keyPair.secretKey); + // Store the private key first. It won't be used until the decree is accepted. + sFrameChan.query("Q_ADMIN_MAILBOX", priv, function (err, obj) { + if (err || (obj && obj.error)) { + console.error(err || obj.error); + UI.warn(Messages.error); + spinner.hide(); + return; + } + // Then send the decree + sFrameChan.query('Q_ADMIN_RPC', { + cmd: 'ADMIN_DECREE', + data: ['SET_SUPPORT_MAILBOX', [pub]] + }, function (e, response) { + $button.removeAttr('disabled'); + if (e || response.error) { + UI.warn(Messages.error); + console.error(e, response); + spinner.hide(); + return; + } + spinner.done(); + UI.log(Messages.saved); + supportKey = pub; + APP.privateKey = priv; + $('.cp-admin-support-init').hide(); + APP.$rightside.append(create['support-list']()); + APP.$rightside.append(create['support-priv']()); + }); + }); + }); + })(); return $div; } if (!APP.privateKey || !checkAdminKey(APP.privateKey)) { @@ -937,6 +1046,7 @@ define([ APP.privateKey = key; $('.cp-admin-support-init').hide(); APP.$rightside.append(create['support-list']()); + APP.$rightside.append(create['support-priv']()); }); }); return $div; @@ -1030,9 +1140,10 @@ define([ sFrameChan.query('Q_ADMIN_RPC', { cmd: 'ADMIN_DECREE', data: ['SET_LAST_BROADCAST_HASH', [lastHash]] - }, function (e) { - if (e) { - console.error(e); + }, function (e, response) { + if (e || response.error) { + UI.warn(Messages.error); + console.error(e, response); return; } console.log('lastBroadcastHash updated'); @@ -1299,19 +1410,23 @@ define([ var $start = $(start); var $end = $(end); var is24h = false; + var dateFormat = "Y-m-d H:i"; try { is24h = !new Intl.DateTimeFormat(navigator.language, { hour: 'numeric' }).format(0).match(/AM/); } catch (e) {} + if (!is24h) { dateFormat = "Y-m-d h:i K"; } var endPickr = Flatpickr(end, { enableTime: true, time_24hr: is24h, + dateFormat: dateFormat, minDate: new Date() }); Flatpickr(start, { enableTime: true, time_24hr: is24h, minDate: new Date(), + dateFormat: dateFormat, onChange: function () { endPickr.set('minDate', new Date($start.val())); } @@ -1336,9 +1451,10 @@ define([ sFrameChan.query('Q_ADMIN_RPC', { cmd: 'ADMIN_DECREE', data: ['SET_MAINTENANCE', [data]] - }, function (e) { - if (e) { - UI.warn(Messages.error); console.error(e); + }, function (e, response) { + if (e || response.error) { + UI.warn(Messages.error); + console.error(e, response); $button.prop('disabled', ''); return; } @@ -1430,10 +1546,11 @@ define([ sFrameChan.query('Q_ADMIN_RPC', { cmd: 'ADMIN_DECREE', data: ['SET_SURVEY_URL', [data]] - }, function (e) { - if (e) { + }, function (e, response) { + if (e || response.error) { $button.prop('disabled', ''); - UI.warn(Messages.error); console.error(e); + UI.warn(Messages.error); + console.error(e, response); return; } // Maintenance applied, send notification @@ -1529,11 +1646,12 @@ define([ sFrameChan.query('Q_ADMIN_RPC', { cmd: 'GET_WORKER_PROFILES', }, function (e, data) { - if (e) { return void console.error(e); } + if (e || data.error) { + UI.warn(Messages.error); + return void console.error(e, data); + } //console.info(data); $div.find("table").remove(); - - process(data); $div.append(table); }); diff --git a/www/checkup/app-checkup.less b/www/checkup/app-checkup.less index 40e6a1add..a475d5983 100644 --- a/www/checkup/app-checkup.less +++ b/www/checkup/app-checkup.less @@ -20,7 +20,7 @@ html, body { } .pending { - border: 1px solid white; + border: 1px solid @cryptpad_text_col; .fa { margin-right: 20px; } @@ -45,7 +45,7 @@ html, body { table { td { padding: 5px; - border: 1px solid white; + border: 1px solid @cryptpad_text_col; } } diff --git a/www/checkup/main.js b/www/checkup/main.js index aeb8a4239..2beee1229 100644 --- a/www/checkup/main.js +++ b/www/checkup/main.js @@ -50,6 +50,10 @@ define([ ]); }; + var cacheBuster = function (url) { + return url + '?test=' + (+new Date()); + }; + var trimmedSafe = trimSlashes(ApiConfig.httpSafeOrigin); var trimmedUnsafe = trimSlashes(ApiConfig.httpUnsafeOrigin); @@ -117,7 +121,7 @@ define([ var checkAvailability = function (url, cb) { $.ajax({ - url: url, + url: cacheBuster(url), data: {}, complete: function (xhr) { cb(xhr.status === 200); @@ -169,10 +173,13 @@ define([ }).nThen(function () { // Iframe is loaded clearTimeout(to); + console.log("removing sandbox iframe"); + $('iframe#sbox-iframe').remove(); cb(true); }); }); + var shared_websocket; // Test Websocket var evWSError = Util.mkEvent(true); assert(function (_cb, msg) { @@ -185,6 +192,7 @@ define([ })); var ws = new WebSocket(NetConfig.getWebsocketURL()); + shared_websocket = ws; var to = setTimeout(function () { console.error('Websocket TIMEOUT'); evWSError.fire(); @@ -203,6 +211,7 @@ define([ }); // Test login block + var shared_realtime; assert(function (_cb, msg) { var websocketErr = "No WebSocket available"; var cb = Util.once(Util.both(_cb, function (status) { @@ -237,7 +246,7 @@ define([ var blockUrl = Login.Block.getBlockUrl(opt.blockKeys); var blockRequest = Login.Block.serialize("{}", opt.blockKeys); var removeRequest = Login.Block.remove(opt.blockKeys); - console.log('Test block URL:', blockUrl); + console.warn('Testing block URL (%s). One 404 is normal.', blockUrl); var userHash = '/2/drive/edit/000000000000000000000000'; var secret = Hash.getSecrets('drive', userHash); @@ -264,7 +273,7 @@ define([ console.error("Can't create new channel. This may also be a websocket issue."); return void cb(false); } - RT = rt; + shared_realtime = RT = rt; var proxy = rt.proxy; proxy.edPublic = opt.edPublic; proxy.edPrivate = opt.edPrivate; @@ -330,14 +339,13 @@ define([ }).nThen(function () { cb(true); }); - }); var sheetURL = '/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 url = cacheBuster(sheetURL); var expect = { 'cross-origin-resource-policy': 'cross-origin', 'cross-origin-embedder-policy': 'require-corp', @@ -366,37 +374,12 @@ define([ }); assert(function (cb, msg) { - msg = msg; - return void cb(true); - /* - 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, { + msg.innerText = "Missing HTTP header required to disable Google's Floc."; + $.ajax('/?'+ (+new Date()), { complete: function (xhr) { - var csp = xhr.getResponseHeader('Content-Security-Policy'); - if (!/unsafe\-eval/.test(csp)) { - // OnlyOffice requires unsafe-eval - console.error('CSP', csp); - return cb("expected 'unsafe-eval'"); - } - if (!/unsafe\-inline/.test(csp)) { - // OnlyOffice also requires unsafe-inline - console.error('CSP', csp); - return cb("expected 'unsafe-inline'"); - } - cb(true); + cb(xhr.getResponseHeader('permissions-policy') === 'interest-cohort=()'); }, - }); */ + }); }); assert(function (cb, msg) { @@ -407,17 +390,20 @@ define([ "Your browser console may provide more details as to why this resource could not be loaded. ", ])); - $.ajax('/api/broadcast', { + $.ajax(cacheBuster('/api/broadcast'), { dataType: 'text', complete: function (xhr) { - console.log(xhr); cb(xhr.status === 200); }, }); }); - var checkAPIHeaders = function (url, cb) { - $.ajax(url, { + var code = function (content) { + return h('code', content); + }; + + var checkAPIHeaders = function (url, msg, cb) { + $.ajax(cacheBuster(url), { dataType: 'text', complete: function (xhr) { var allHeaders = xhr.getAllResponseHeaders(); @@ -436,15 +422,31 @@ define([ var expect = { 'cross-origin-resource-policy': 'cross-origin', + 'cross-origin-embedder-policy': 'require-corp', }; - var incorrect = Object.keys(expect).some(function (k) { + var incorrect = false; + + Object.keys(expect).forEach(function (k) { var response = xhr.getResponseHeader(k); - if (response !== expect[k]) { - return true; + var expected = expect[k]; + if (response !== expected) { + incorrect = true; + msg.appendChild(h('p', [ + 'The ', + code(k), + ' header for ', + code(url), + " is '", + code(response), + "' instead of '", + code(expected), + "' as expected.", + ])); + } }); - if (duplicated || incorrect) { console.error(allHeaders); } + if (duplicated || incorrect) { console.debug(allHeaders); } cb(!duplicated && !incorrect); }, }); @@ -455,13 +457,13 @@ define([ assert(function (cb, msg) { var url = '/api/config'; msg.innerText = url + INCORRECT_HEADER_TEXT; - checkAPIHeaders(url, cb); + checkAPIHeaders(url, msg, cb); }); assert(function (cb, msg) { var url = '/api/broadcast'; msg.innerText = url + INCORRECT_HEADER_TEXT; - checkAPIHeaders(url, cb); + checkAPIHeaders(url, msg, cb); }); var setWarningClass = function (msg) { @@ -479,8 +481,9 @@ define([ 'This instance does not provide a valid ', h('code', 'adminEmail'), ' which can make it difficult to contact its adminstrator to report vulnerabilities or abusive content.', - ' This can be configured in ', CONFIG_PATH(), '. ', - RESTART_WARNING(), + " This can be configured on your instance's admin panel. Use the provided ", + code("Flush cache'"), + " button for this change to take effect for all users.", ])); cb(email); }); @@ -517,6 +520,156 @@ define([ cb(false); }); + var response = Util.response(function (err) { + console.error('SANDBOX_ERROR', err); + }); + + var sandboxIframe = h('iframe', { + class: 'sandbox-test', + src: cacheBuster(trimmedSafe + '/checkup/sandbox/index.html'), + }); + document.body.appendChild(sandboxIframe); + + var sandboxIframeReady = Util.mkEvent(true); + setTimeout(function () { + sandboxIframeReady.fire("TIMEOUT"); + }, 10 * 1000); + + var postMessage = function (content, cb) { + try { + var txid = Util.uid(); + content.txid = txid; + response.expect(txid, cb, 15000); + sandboxIframe.contentWindow.postMessage(JSON.stringify(content), '*'); + } catch (err) { + console.error(err); + } + }; + + var deferredPostMessage = function (content, _cb) { + var cb = Util.once(Util.mkAsync(_cb)); + nThen(function (w) { + sandboxIframeReady.reg(w(function (err) { + if (!err) { return; } + w.abort(); + cb(err); + })); + }).nThen(function () { + postMessage(content, cb); + }); + }; + + window.addEventListener('message', function (event) { + try { + var msg = JSON.parse(event.data); + if (msg.command === 'READY') { return void sandboxIframeReady.fire(); } + if (msg.q === "READY") { return; } // ignore messages from the usual sandboxed iframe + var txid = msg.txid; + if (!txid) { return console.log("no handler for ", txid); } + response.handle(txid, msg.content); + } catch (err) { + console.error(event); + console.error(err); + } + }); + + var parseCSP = function (CSP) { + //console.error(CSP); + var CSP_headers = {}; + CSP.split(";") + .forEach(function (rule) { + rule = (rule || "").trim(); + if (!rule) { return; } + var parts = rule.split(/\s/); + var first = parts[0]; + var rest = rule.slice(first.length + 1); + CSP_headers[first] = rest; + //console.error(rule.trim()); + //console.info("[%s] '%s'", first, rest); + }); + return CSP_headers; + }; + + var hasUnsafeEval = function (CSP_headers) { + return /unsafe\-eval/.test(CSP_headers['script-src']); + }; + + var hasUnsafeInline = function (CSP_headers) { + return /unsafe\-inline/.test(CSP_headers['script-src']); + }; + + var hasOnlyOfficeHeaders = function (CSP_headers) { + if (!hasUnsafeEval(CSP_headers)) { + console.error("NO_UNSAFE_EVAL"); + console.log(CSP_headers); + return false; + } + if (!hasUnsafeInline(CSP_headers)) { + console.error("NO_UNSAFE_INLINE"); + return void false; + } + return true; + }; + + var CSP_WARNING = function (url) { + return h('span', [ + code(url), + ' does not have the required ', + code("'content-security-policy'"), + ' headers set. This is most often related to incorrectly configured sandbox domains or reverse proxies.', + ]); + }; + + assert(function (_cb, msg) { + var url = '/sheet/inner.html'; + var cb = Util.once(Util.mkAsync(_cb)); + msg.appendChild(CSP_WARNING(url)); + deferredPostMessage({ + command: 'GET_HEADER', + content: { + url: url, + header: 'content-security-policy', + }, + }, function (content) { + var CSP_headers = parseCSP(content); + cb(hasOnlyOfficeHeaders(CSP_headers)); + }); + }); + + assert(function (cb, msg) { + var url = '/common/onlyoffice/v4/web-apps/apps/spreadsheeteditor/main/index.html'; + msg.appendChild(CSP_WARNING(url)); + deferredPostMessage({ + command: 'GET_HEADER', + content: { + url: url, + header: 'content-security-policy', + }, + }, function (content) { + var CSP_headers = parseCSP(content); + cb(hasOnlyOfficeHeaders(CSP_headers)); + }); + }); + + assert(function (cb, msg) { + var url = '/sheet/inner.html'; + msg.appendChild(h('span', [ + code(url), + ' does not have the required ', + code("'cross-origin-opener-policy'"), + ' headers set.', + ])); + deferredPostMessage({ + command: 'GET_HEADER', + content: { + url: url, + header: 'cross-origin-opener-policy', + }, + }, function (content) { + cb(content === 'same-origin'); + }); + }); + if (false) { assert(function (cb, msg) { msg.innerText = 'fake test to simulate failure'; @@ -583,6 +736,14 @@ define([ $progress.remove(); $('body').prepend(report); + try { + console.log('closing shared websocket'); + shared_websocket.close(); + } catch (err) { console.error(err); } + try { + console.log('closing shared realtime'); + shared_realtime.network.disconnect(); + } catch (err) { console.error(err); } }, function (i, total) { console.log('test '+ i +' completed'); completed++; diff --git a/www/checkup/sandbox/index.html b/www/checkup/sandbox/index.html new file mode 100644 index 000000000..04a9502d3 --- /dev/null +++ b/www/checkup/sandbox/index.html @@ -0,0 +1,11 @@ + + +
+ + + + + + +