diff --git a/js/common.js b/js/common.js index a12595d3..ea975e03 100644 --- a/js/common.js +++ b/js/common.js @@ -20,6 +20,7 @@ global.showdown = require('./showdown-1.9.1'); global.DOMPurify = require('./purify-1.0.11'); global.baseX = require('./base-x-3.0.5.1').baseX; require('./bootstrap-3.3.7'); +require('./legacy'); require('./privatebin'); // internal variables diff --git a/js/legacy.js b/js/legacy.js new file mode 100644 index 00000000..82a6bd12 --- /dev/null +++ b/js/legacy.js @@ -0,0 +1,256 @@ +/** + * PrivateBin + * + * a zero-knowledge paste bin + * + * @see {@link https://github.com/PrivateBin/PrivateBin} + * @copyright 2012 Sébastien SAUVAGE ({@link http://sebsauvage.net}) + * @license {@link https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License} + * @version 1.3 + * @name Legacy + * @namespace + * + * IMPORTANT NOTICE FOR DEVELOPERS: + * The logic in this file is intended to run in legacy browsers. Avoid any use of: + * - ES6 or newer in general + * - const/let, use the traditional var declarations instead + * - async/await or Promises, use traditional callbacks + * - shorthand function notation "() => output", use the full "function() {return output;}" style + * - IE doesn't support: + * - URL(), use the traditional window.location object + * - endsWith(), use indexof() + * - yes, this logic needs to support IE 5 or 6, to at least display the error message + */ + +// main application start, called when DOM is fully loaded +jQuery(document).ready(function() { + 'use strict'; + // run main controller + $.Legacy.Check.init(); +}); + +jQuery.Legacy = (function($) { + 'use strict'; + + /** + * compatibility check + * + * @name Check + * @class + */ + var Check = (function () { + var me = {}; + + /** + * Status of the initial check, true means it passed + * + * @private + * @prop {bool} + */ + var status = false; + + /** + * Initialization check did run + * + * @private + * @prop {bool} + */ + var init = false; + + /** + * blacklist of UserAgents (parts) known to belong to a bot + * + * @private + * @enum {Array} + * @readonly + */ + var badBotUA = [ + 'Bot', + 'bot' + ]; + + /** + * whitelist of top level domains to consider a secure context, + * regardless of protocol + * + * @private + * @enum {Array} + * @readonly + */ + var tld = [ + '.onion', + '.i2p' + ]; + + /** + * whitelist of hostnames to consider a secure context, + * regardless of protocol + * + * @private + * @enum {Array} + * @readonly + */ + // whitelists of TLDs & local hostnames + var hostname = [ + 'localhost', + '127.0.0.1', + '[::1]' + ]; + + /** + * check if the context is secure + * + * @private + * @name Check.isSecureContext + * @function + * @return {bool} + */ + function isSecureContext() + { + // use .isSecureContext if available + if (window.isSecureContext === true || window.isSecureContext === false) { + return window.isSecureContext; + } + + // HTTP is obviously insecure + if (window.location.protocol !== 'http:') { + return true; + } + + // filter out actually secure connections over HTTP + for (var i = 0; i < tld.length; i++) { + if ( + window.location.hostname.indexOf( + tld[i], + window.location.hostname.length - tld[i].length + ) !== -1 + ) { + return true; + } + } + + // whitelist localhost for development + for (var i = 0; i < hostname.length; i++) { + if (window.location.hostname === hostname[i]) { + return true; + } + } + + // totally INSECURE http protocol! + return false; + } + + /** + * checks whether this is a bot we dislike + * + * @private + * @name Check.isBadBot + * @function + * @return {bool} + */ + function isBadBot() { + // check whether a bot user agent part can be found in the current + // user agent + for (var i = 0; i < badBotUA.length; i++) { + if (navigator.userAgent.indexOf(badBotUA[i]) !== -1) { + return true; + } + } + return false; + } + + /** + * checks whether this is an unsupported browser, via feature detection + * + * @private + * @name Check.isOldBrowser + * @function + * @return {bool} + */ + function isOldBrowser() { + // webcrypto support + if (!( + 'crypto' in window && + 'getRandomValues' in window.crypto && + 'subtle' in window.crypto && + 'encrypt' in window.crypto.subtle && + 'decrypt' in window.crypto.subtle && + 'Uint8Array' in window && + 'Uint32Array' in window + )) { + return true; + } + + // not checking for async/await, ES6 or Promise support, as most + // browsers introduced these earlier then webassembly and webcrypto: + // https://github.com/PrivateBin/PrivateBin/pull/431#issuecomment-493129359 + + return false; + } + + /** + * returns if the check has concluded + * + * @name Check.getInit + * @function + * @return {bool} + */ + me.getInit = function() + { + return init; + } + + /** + * returns the current status of the check + * + * @name Check.getStatus + * @function + * @return {bool} + */ + me.getStatus = function() + { + return status; + } + + /** + * init on application start, returns an all-clear signal + * + * @name Check.init + * @function + */ + me.init = function() + { + // prevent bots from viewing a paste and potentially deleting data + // when burn-after-reading is set + if (isBadBot()) { + $.PrivateBin.Alert.showError('I love you too, bot…'); + init = true; + return; + } + + if (isOldBrowser()) { + // some browsers (Chrome based ones) would have webcrypto support if using HTTPS + if (!isSecureContext()) { + $.PrivateBin.Alert.showError(['Your browser may require an HTTPS connection to support the WebCrypto API. Try switching to HTTPS.', 'https' + window.location.href.slice(4)]); + } + $('#oldnotice').removeClass('hidden'); + init = true; + return; + } + + if (!isSecureContext()) { + $('#httpnotice').removeClass('hidden'); + } + init = true; + + // only if everything passed, we set the status to true + status = true; + } + + return me; + })(); + + return { + Check: Check + }; +})(jQuery); diff --git a/js/privatebin.js b/js/privatebin.js index cdfce541..1db85ee0 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -14,6 +14,7 @@ */ jQuery.fn.draghover = function() { + 'use strict'; return this.each(function() { let collection = $(), self = $(this); @@ -4709,156 +4710,6 @@ jQuery.PrivateBin = (function($, RawDeflate) { return me; })(); - - /** - * initial (security) check - * - * @name InitialCheck - * @class - */ - const InitialCheck = (function () { - const me = {}; - - /** - * blacklist of UserAgents (parts) known to belong to a bot - * - * @private - * @enum {Array} - * @readonly - */ - const badBotUA = [ - 'Bot', - 'bot' - ]; - - /** - * check if the connection is insecure - * - * @private - * @name InitialCheck.isInsecureConnection - * @function - * @return {bool} - */ - function isInsecureConnection() - { - // use .isSecureContext if available - if (window.isSecureContext === true || window.isSecureContext === false) { - return !window.isSecureContext; - } - - const url = new URL(window.location); - - // HTTP is obviously insecure - if (url.protocol !== 'http:') { - return false; - } - - // filter out actually secure connections over HTTP - for (const tld of ['.onion', '.i2p']) { - if (url.hostname.endsWith(tld)) { - return false; - } - } - - // whitelist localhost for development - for (const hostname of ['localhost', '127.0.0.1', '[::1]']) { - if (url.hostname === hostname) { - return false; - } - } - - // totally INSECURE http protocol! - return true; - } - - /** - * checks whether this is a bot we dislike - * - * @private - * @name InitialCheck.isBadBot - * @function - * @return {bool} - */ - function isBadBot() { - // check whether a bot user agent part can be found in the current - // user agent - for (const UAfragment of badBotUA) { - if (navigator.userAgent.indexOf(UAfragment) >= 0) { - return true; - } - } - return false; - } - - /** - * checks whether this is an unsupported browser, via feature detection - * - * @private - * @name InitialCheck.isOldBrowser - * @function - * @return {bool} - */ - function isOldBrowser() { - // webcrypto support - if (!( - 'crypto' in window && - 'getRandomValues' in window.crypto && - 'subtle' in window.crypto && - 'encrypt' in window.crypto.subtle && - 'decrypt' in window.crypto.subtle && - 'Uint8Array' in window && - 'Uint32Array' in window - )) { - return true; - } - - // not checking for async/await, ES6 or Promise support, as most - // browsers introduced these earlier then webassembly and webcrypto: - // https://github.com/PrivateBin/PrivateBin/pull/431#issuecomment-493129359 - - return false; - } - - /** - * init on application start, returns an all-clear signal - * - * @name InitialCheck.init - * @function - * @return {bool} - */ - me.init = function() - { - // prevent bots from viewing a paste and potentially deleting data - // when burn-after-reading is set - if (isBadBot()) { - Alert.showError('I love you too, bot…'); - return false; - } - - if (isOldBrowser()) { - // some browsers (Chrome based ones) would have webcrypto support if using HTTPS - if (isInsecureConnection()) { - Alert.showError(['Your browser may require an HTTPS connection to support the WebCrypto API. Try switching to HTTPS.', 'https' + window.location.href.slice(4)]); - } - $('#oldnotice').removeClass('hidden'); - return false; - } - - if (isInsecureConnection()) { - $('#httpnotice').removeClass('hidden'); - } - - z = zlib.catch(function () { - if ($('body').data('compression') !== 'none') { - Alert.showWarning('Your browser doesn\'t support WebAssembly, used for zlib compression. You can create uncompressed documents, but can\'t read compressed ones.'); - } - }); - return true; - } - - return me; - })(); - /** * (controller) main PrivateBin logic * @@ -5033,13 +4884,29 @@ jQuery.PrivateBin = (function($, RawDeflate) { DiscussionViewer.prepareNewDiscussion(); }; + /** + * try initializing zlib or display a warning if it fails, + * extracted from main init to allow unit testing + * + * @name Controller.initZ + * @function + */ + me.initZ = function() + { + z = zlib.catch(function () { + if ($('body').data('compression') !== 'none') { + Alert.showWarning('Your browser doesn\'t support WebAssembly, used for zlib compression. You can create uncompressed documents, but can\'t read compressed ones.'); + } + }); + } + /** * application start * * @name Controller.init * @function */ - me.init = async function() + me.init = function() { // first load translations I18n.loadTranslations(); @@ -5057,10 +4924,18 @@ jQuery.PrivateBin = (function($, RawDeflate) { Prompt.init(); TopNav.init(); UiHelper.init(); - if (!InitialCheck.init()) { + + // check for legacy browsers before going any further + if (!$.Legacy.Check.getInit()) { + // Legacy check didn't complete, wait and try again + setTimeout(init, 500); + return; + } + if (!$.Legacy.Check.getStatus()) { // something major is wrong, stop right away return; } + me.initZ(); // check whether existing paste needs to be shown try { @@ -5100,7 +4975,6 @@ jQuery.PrivateBin = (function($, RawDeflate) { ServerInteraction: ServerInteraction, PasteEncrypter: PasteEncrypter, PasteDecrypter: PasteDecrypter, - InitialCheck: InitialCheck, Controller: Controller }; })(jQuery, RawDeflate); diff --git a/js/test/InitialCheck.js b/js/test/Check.js similarity index 87% rename from js/test/InitialCheck.js rename to js/test/Check.js index 4e181f15..dd49236c 100644 --- a/js/test/InitialCheck.js +++ b/js/test/Check.js @@ -2,7 +2,7 @@ var common = require('../common'); /* global WebCrypto */ -describe('InitialCheck', function () { +describe('Check', function () { describe('init', function () { this.timeout(30000); before(function () { @@ -23,7 +23,8 @@ describe('InitialCheck', function () { '' ); $.PrivateBin.Alert.init(); - const result1 = !$.PrivateBin.InitialCheck.init(), + $.Legacy.Check.init(); + const result1 = $.Legacy.Check.getInit() && !$.Legacy.Check.getStatus(), result2 = !$('#errormessage').hasClass('hidden'); clean(); return result1 && result2; @@ -50,7 +51,8 @@ describe('InitialCheck', function () { '' ); $.PrivateBin.Alert.init(); - const result1 = !$.PrivateBin.InitialCheck.init(), + $.Legacy.Check.init(); + const result1 = $.Legacy.Check.getInit() && !$.Legacy.Check.getStatus(), result2 = isSecureContext === $('#errormessage').hasClass('hidden'), result3 = !$('#oldnotice').hasClass('hidden'); clean(); @@ -70,9 +72,10 @@ describe('InitialCheck', function () { ''+ '' ); - $.PrivateBin.Alert.init(); window.crypto = new WebCrypto(); - const result1 = $.PrivateBin.InitialCheck.init(), + $.PrivateBin.Alert.init(); + $.Legacy.Check.init(); + const result1 = $.Legacy.Check.getInit() && $.Legacy.Check.getStatus(), result2 = secureProtocol === $('#httpnotice').hasClass('hidden'); clean(); return result1 && result2; diff --git a/js/test/CryptTool.js b/js/test/CryptTool.js index b3aaaa59..627a242f 100644 --- a/js/test/CryptTool.js +++ b/js/test/CryptTool.js @@ -19,7 +19,7 @@ describe('CryptTool', function () { await new Promise(resolve => setTimeout(resolve, 300)); let clean = jsdom(); // ensure zlib is getting loaded - $.PrivateBin.InitialCheck.init(); + $.PrivateBin.Controller.initZ(); window.crypto = new WebCrypto(); message = message.trim(); let cipherMessage = await $.PrivateBin.CryptTool.cipher( @@ -182,7 +182,7 @@ describe('CryptTool', function () { clean = jsdom(); window.crypto = new WebCrypto(); // ensure zlib is getting loaded - $.PrivateBin.InitialCheck.init(); + $.PrivateBin.Controller.initZ(); let cipherMessage = await $.PrivateBin.CryptTool.cipher( 'foo', 'bar', message, [] ), @@ -227,7 +227,7 @@ conseq_or_bottom inv (interp (nth_iterate sBody n) (MemElem mem)) `; let clean = jsdom(); // ensure zlib is getting loaded - $.PrivateBin.InitialCheck.init(); + $.PrivateBin.Controller.initZ(); window.crypto = new WebCrypto(); let cipherMessage = await $.PrivateBin.CryptTool.cipher( key, password, message, [] diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index 338cf406..42fac244 100644 --- a/tpl/bootstrap.php +++ b/tpl/bootstrap.php @@ -71,7 +71,8 @@ if ($MARKDOWN): endif; ?> - + + diff --git a/tpl/page.php b/tpl/page.php index 567f4bbd..5b0abc8e 100644 --- a/tpl/page.php +++ b/tpl/page.php @@ -49,7 +49,8 @@ if ($MARKDOWN): endif; ?> - + +