Browse Source

Implement proxy script for WebExtensions. #102.

FelisCatus 8 years ago
parent
commit
f421810959

+ 1 - 0
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]

+ 15 - 0
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')]

+ 3 - 0
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']

+ 113 - 0
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);
+        }
+      }
+    });
+  }
+})();

+ 9 - 1
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://*/*",
     "<all_urls>"
   ],
+  "content_security_policy":
+    "script-src 'self' 'unsafe-eval'; object-src 'self';",
   "commands": {
     "_execute_browser_action": {
       "suggested_key": {
         "default": "Alt+Shift+O"
       }
     }
+  },
+  "applications": {
+    "gecko": {
+      "id": "[email protected]",
+      "strict_min_version": "55.0a1"
+    }
   }
 }

+ 2 - 1
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"
   }
 }

+ 73 - 0
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