diff --git a/omega-target-chromium-extension/background.coffee b/omega-target-chromium-extension/background.coffee index a76258c..52cb1cc 100644 --- a/omega-target-chromium-extension/background.coffee +++ b/omega-target-chromium-extension/background.coffee @@ -296,6 +296,7 @@ refreshActivePageIfEnabled = -> chrome.tabs.reload(tabs[0].id, {bypassCache: true}) chrome.runtime.onMessage.addListener (request, sender, respond) -> + return unless request and request.method options.ready.then -> target = options method = target[request.method] diff --git a/omega-target-chromium-extension/grunt/browserify.coffee b/omega-target-chromium-extension/grunt/browserify.coffee index b402df7..1381680 100644 --- a/omega-target-chromium-extension/grunt/browserify.coffee +++ b/omega-target-chromium-extension/grunt/browserify.coffee @@ -1,3 +1,4 @@ +path = require('path') module.exports = index: files: @@ -26,3 +27,17 @@ module.exports = browserifyOptions: extensions: '.coffee' standalone: 'OmegaTargetChromium' + omega_webext_proxy_script: + files: + 'build/js/omega_webext_proxy_script.min.js': + 'omega_webext_proxy_script.js' + options: + alias: + 'omega-pac': 'omega-pac/omega_pac.min.js' + plugin: + if process.env.BUILD == 'release' + [['minifyify', {map: false}]] + else + [] + browserifyOptions: + noParse: [require.resolve('omega-pac/omega_pac.min.js')] diff --git a/omega-target-chromium-extension/grunt/watch.coffee b/omega-target-chromium-extension/grunt/watch.coffee index cafeb37..2d18a99 100644 --- a/omega-target-chromium-extension/grunt/watch.coffee +++ b/omega-target-chromium-extension/grunt/watch.coffee @@ -23,6 +23,9 @@ module.exports = src: files: ['src/**/*.coffee'] tasks: ['coffeelint:src', 'browserify', 'copy:target_self'] + browserify_omega_webext_proxy_script: + files: ['omega_webext_proxy_script.js'] + tasks: ['browserify:omega_webext_proxy_script'] coffee: files: ['src/**/*.coffee', '*.coffee'] tasks: ['coffeelint:src', 'coffee', 'copy:target_self'] diff --git a/omega-target-chromium-extension/omega_webext_proxy_script.js b/omega-target-chromium-extension/omega_webext_proxy_script.js new file mode 100644 index 0000000..823fc32 --- /dev/null +++ b/omega-target-chromium-extension/omega_webext_proxy_script.js @@ -0,0 +1,113 @@ +FindProxyForURL = (function () { + var OmegaPac = require('omega-pac'); + var options = {}; + var state = {}; + var activeProfile = null; + var fallbackResult = 'DIRECT'; + var pacCache = {}; + + init(); + + return FindProxyForURL; + + function FindProxyForURL(url, host, details) { + if (!activeProfile) { + warn('Warning: Proxy script not initialized on handling: ' + url); + return fallbackResult; + } + // Moz: Neither path or query is included url regardless of scheme for now. + // This is even more strict than Chromium restricting HTTPS URLs. + // Therefore, it leads to different behavior than the icon and badge. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1337001 + var request = OmegaPac.Conditions.requestFromUrl(url); + var profile = activeProfile; + var matchResult, next; + while (profile) { + matchResult = OmegaPac.Profiles.match(profile, request) + if (!matchResult) { + if (profile.profileType === 'DirectProfile') { + return 'DIRECT'; + } else if (profile.pacScript) { + return runPacProfile(profile.pacScript); + } else { + warn('Warning: Unsupported profile: ' + profile.profileType); + return fallbackResult; + } + } + + if (Array.isArray(matchResult)) { + next = matchResult[0]; + // TODO: Maybe also return user/pass if Mozilla supports it or it ends + // up standardized in WebExtensions in the future. + // MOZ: Mozilla has a bug tracked for user/pass in PAC return value. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1319641 + if (next.charCodeAt(0) !== 43) { + // MOZ: HTTPS proxies are supported under the prefix PROXY. + // https://dxr.mozilla.org/mozilla-central/source/toolkit/components/extensions/ProxyScriptContext.jsm#180 + return next.replace(/HTTPS /g, 'PROXY '); + } + } else if (matchResult.profileName) { + next = OmegaPac.Profiles.nameAsKey(matchResult.profileName) + } else { + return fallbackResult; + } + profile = OmegaPac.Profiles.byKey(next, options) + } + warn('Warning: Cannot find profile: ' + next); + return fallbackResult; + } + + function runPacProfile(profile) { + var cached = pacCache[profile.name]; + if (!cached || cached.revision !== profile.revision) { + // https://github.com/FelisCatus/SwitchyOmega/issues/390 + var body = ';\n' + profile.pacScript + '\n\n/* End of PAC */;' + body += 'return FindProxyForURL'; + var func = new Function(body).call(this); + + if (typeof func !== 'function') { + warn('Warning: Cannot compile pacScript: ' + profile.name); + func = function() { return fallbackResult; }; + } + cached = {func: func, revision: profile.revision} + pacCache[cacheKey] = cached; + } + try { + // Moz: Most scripts probably won't run without global PAC functions. + // Example: dnsDomainIs, shExpMatch, isInNet. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1353510 + return cached.func.call(this); + } catch (ex) { + warn('Warning: Error occured in pacScript: ' + profile.name, ex); + return fallbackResult; + } + } + + function warn(message, error) { + // We don't have console here and alert is not implemented. + // Throwing and messaging seems to be the only ways to communicate. + // MOZ: alert(): https://bugzilla.mozilla.org/show_bug.cgi?id=1353510 + browser.runtime.sendMessage({ + event: 'proxyScriptLog', + message: message, + error: error, + level: 'warn', + }); + } + + function init() { + browser.runtime.sendMessage({event: 'proxyScriptLoaded'}); + browser.runtime.onMessage.addListener(function(message) { + if (message.event === 'proxyScriptStateChanged') { + state = message.state; + options = message.options; + if (!state.currentProfileName) { + activeProfile = state.tempProfile; + } else { + activeProfile = OmegaPac.Profiles.byName(state.currentProfileName, + options); + } + } + }); + } +})(); diff --git a/omega-target-chromium-extension/overlay/manifest.json b/omega-target-chromium-extension/overlay/manifest.json index 22c9b7a..db2fe75 100644 --- a/omega-target-chromium-extension/overlay/manifest.json +++ b/omega-target-chromium-extension/overlay/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "__MSG_manifest_app_name__", - "version": "2.4.5", + "version": "2.4.7", "description": "__MSG_manifest_app_description__", "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkhwZJT76btQ04EEMOFtZPLESD1TmSVjbLjs0OyesD9Ht8YllFPfJ3qmtbSQGVuvmxH1GK/jUO2QcEWb8bHuOjoRlq20fi5j5Aq90O8FKET+y5D8PxCyi3WmnquiEwaE5cNmaCsw/G2JlO+bZOtdQ/QKOvMxBAegABYimEGfSvCMVUEvpymys0gBhLoch72zPAiJUBkf0z8BtjYTueMRcRXkrSeRPLygUDQnZ1TkQWMYYBp/zqpD5ggxytAklEMQzR9Hn0lqu5s7iuUAgihbysPn/8Wh00Zj5FySpK//KcpG3JS7UWxC28oSt8z5ZR3YimnX+HX3P36V0mC1pgM4o7wIDAQAB", "icons": { @@ -43,11 +43,19 @@ "ftp://*/*", "" ], + "content_security_policy": + "script-src 'self' 'unsafe-eval'; object-src 'self';", "commands": { "_execute_browser_action": { "suggested_key": { "default": "Alt+Shift+O" } } + }, + "applications": { + "gecko": { + "id": "switchyomega@example.com", + "strict_min_version": "55.0a1" + } } } diff --git a/omega-target-chromium-extension/package.json b/omega-target-chromium-extension/package.json index 47fa274..18a7ce7 100644 --- a/omega-target-chromium-extension/package.json +++ b/omega-target-chromium-extension/package.json @@ -24,12 +24,13 @@ "heap": "^0.2.6", "omega-target": "../omega-target", "omega-web": "../omega-web", + "omega-pac": "../omega-pac", "xhr": "^1.16.0" }, "browser": { "omega-target": "./omega_target_shim.js" }, "scripts": { - "dev": "npm link omega-target && npm link omega-web" + "dev": "npm link omega-target && npm link omega-web && npm link omega-pac" } } diff --git a/omega-target-chromium-extension/src/options.coffee b/omega-target-chromium-extension/src/options.coffee index 72d0334..b329885 100644 --- a/omega-target-chromium-extension/src/options.coffee +++ b/omega-target-chromium-extension/src/options.coffee @@ -128,6 +128,14 @@ class ChromeOptions extends OmegaTarget.Options proxySettings.onChange.addListener @_proxyChangeListener @_proxyChangeWatchers.push(callback) applyProfileProxy: (profile, meta) -> + if chrome?.proxy?.settings? + return @applyProfileProxySettings(profile, meta) + else if browser?.proxy?.registerProxyScript? + return @applyProfileProxyScript(profile, meta) + else + ex = new Error('Your browser does not support proxy settings!') + return Promise.reject ex + applyProfileProxySettings: (profile, meta) -> meta ?= profile if profile.profileType == 'SystemProfile' # Clear proxy settings, returning proxy control to Chromium. @@ -171,6 +179,71 @@ class ChromeOptions extends OmegaTarget.Options proxySettings.get {}, @_proxyChangeListener return + _proxyScriptUrl: 'js/omega_webext_proxy_script.min.js' + _proxyScriptDisabled: false + applyProfileProxyScript: (profile, state) -> + state = state ? {} + state.currentProfileName = profile.name + if profile.name == '' + state.tempProfile = @_tempProfile + if profile.profileType == 'SystemProfile' + # MOZ: SystemProfile cannot be done now due to lack of "PASS" support. + # https://bugzilla.mozilla.org/show_bug.cgi?id=1319634 + # In the mean time, let's just set an invalid script to unregister it. + browser.proxy.registerProxyScript('js/omega_invalid_proxy_script.min.js') + @_proxyScriptDisabled = true + else + @_proxyScriptState = state + @_initWebextProxyScript().then => @_proxyScriptStateChanged() + # Proxy authentication is not covered in WebExtensions standard now. + # MOZ: Mozilla has a bug tracked to implemented it in PAC return value. + # https://bugzilla.mozilla.org/show_bug.cgi?id=1319641 + return Promise.resolve() + + _proxyScriptInitialized: false + _proxyScriptState: {} + _initWebextProxyScript: -> + if not @_proxyScriptInitialized + browser.proxy.onProxyError.addListener (err) => + if err and err.message.indexOf('Invalid Proxy Rule: DIRECT') >= 0 + # MOZ: DIRECT cannot be correctly parsed due to a bug. Even though it + # throws, it actually falls back to direct connection so it works. + # https://bugzilla.mozilla.org/show_bug.cgi?id=1355198 + return + @log.error(err) + browser.runtime.onMessage.addListener (message) => + return unless message.event == 'proxyScriptLog' + if message.level == 'error' + @log.error(message) + else if message.level == 'warn' + @log.warn(message) + else + @log.log(message) + + if not @_proxyScriptInitialized or @_proxyScriptDisabled + promise = new Promise (resolve) -> + onMessage = (message) -> + return unless message.event == 'proxyScriptLoaded' + resolve() + browser.runtime.onMessage.removeListener onMessage + return + browser.runtime.onMessage.addListener onMessage + browser.proxy.registerProxyScript(@_proxyScriptUrl) + @_proxyScriptDisabled = false + else + promise = Promise.resolve() + @_proxyScriptInitialized = true + return promise + + _proxyScriptStateChanged: -> + browser.runtime.sendMessage({ + event: 'proxyScriptStateChanged' + state: @_proxyScriptState + options: @_options + }, { + toProxyScript: true + }) + _quickSwitchInit: false _quickSwitchContextMenuCreated: false _quickSwitchCanEnable: false