Просмотр исходного кода

Support browser.proxy.onRequest. Fix #1456.

This commit also refactors other implementations and moves them to
dedicated files, using feature detection to select one on runtime.
FelisCatus 7 лет назад
Родитель
Сommit
465c98f78a

+ 5 - 2
omega-target-chromium-extension/src/coffee/background.coffee

@@ -179,7 +179,10 @@ if chrome?.storage?.sync or browser?.storage?.sync
     sync.enabled = false
   sync.transformValue = OmegaTargetCurrent.Options.transformValueForSync
 
-options = new OmegaTargetCurrent.Options(null, storage, state, Log, sync)
+proxyImpl = OmegaTargetCurrent.proxy.getProxyImpl(Log)
+state.set({proxyImplFeatures: proxyImpl.features})
+options = new OmegaTargetCurrent.Options(null, storage, state, Log, sync,
+  proxyImpl)
 options.externalApi = new OmegaTargetCurrent.ExternalApi(options)
 options.externalApi.listen()
 
@@ -218,7 +221,7 @@ options._inspect = new OmegaTargetCurrent.Inspect (url, tab) ->
 options.setProxyNotControllable(null)
 timeout = null
 
-options.watchProxyChange (details) ->
+proxyImpl.watchProxyChange (details) ->
   return if options.externalApi.disabled
   return unless details
   notControllableBefore = options.proxyNotControllable()

+ 1 - 0
omega-target-chromium-extension/src/module/index.coffee

@@ -7,6 +7,7 @@ module.exports =
   WebRequestMonitor: require('./web_request_monitor')
   Inspect: require('./inspect')
   Url: require('url')
+  proxy: require('./proxy')
 
 for name, value of require('omega-target')
   module.exports[name] ?= value

+ 0 - 199
omega-target-chromium-extension/src/module/options.coffee

@@ -2,9 +2,7 @@ OmegaTarget = require('omega-target')
 OmegaPac = OmegaTarget.OmegaPac
 Promise = OmegaTarget.Promise
 querystring = require('querystring')
-chromeApiPromisify = require('./chrome_api').chromeApiPromisify
 parseExternalProfile = require('./parse_external_profile')
-ProxyAuth = require('./proxy_auth')
 WebRequestMonitor = require('./web_request_monitor')
 ChromePort = require('./chrome_port')
 fetchUrl = require('./fetch_url')
@@ -73,202 +71,6 @@ class ChromeOptions extends OmegaTarget.Options
       chrome.browserAction.setBadgeText?(text: '')
     return
 
-  _formatBypassItem: (condition) ->
-    str = OmegaPac.Conditions.str(condition)
-    i = str.indexOf(' ')
-    return str.substr(i + 1)
-  _fixedProfileConfig: (profile) ->
-    config = {}
-    config['mode'] = 'fixed_servers'
-    rules = {}
-    protocols = ['proxyForHttp', 'proxyForHttps', 'proxyForFtp']
-    protocolProxySet = false
-    for protocol in protocols when profile[protocol]?
-      rules[protocol] = profile[protocol]
-      protocolProxySet = true
-
-    if profile.fallbackProxy
-      if profile.fallbackProxy.scheme == 'http'
-        # Chromium does not allow HTTP proxies in 'fallbackProxy'.
-        if not protocolProxySet
-          # Use 'singleProxy' if no proxy is configured for other protocols.
-          rules['singleProxy'] = profile.fallbackProxy
-        else
-          # Try to set the proxies of all possible protocols.
-          for protocol in protocols
-            rules[protocol] ?= JSON.parse(JSON.stringify(profile.fallbackProxy))
-      else
-        rules['fallbackProxy'] = profile.fallbackProxy
-    else if not protocolProxySet
-      config['mode'] = 'direct'
-
-    if config['mode'] != 'direct'
-      rules['bypassList'] = bypassList = []
-      for condition in profile.bypassList
-        bypassList.push(@_formatBypassItem(condition))
-      config['rules'] = rules
-    return config
-
-  _proxyChangeWatchers: null
-  _proxyChangeListener: null
-  watchProxyChange: (callback) ->
-    @_proxyChangeWatchers = []
-    if not @_proxyChangeListener?
-      @_proxyChangeListener = (details) =>
-        for watcher in @_proxyChangeWatchers
-          watcher(details)
-      if chrome?.proxy?.settings?.onChange?
-        chrome.proxy.settings.onChange.addListener @_proxyChangeListener
-    @_proxyChangeWatchers.push(callback)
-  applyProfileProxy: (profile, meta) ->
-    if browser?.proxy?.register? or browser?.proxy?.registerProxyScript?
-      return @applyProfileProxyScript(profile, meta)
-    else if chrome?.proxy?.settings?
-      return @applyProfileProxySettings(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.
-      return chromeApiPromisify(chrome.proxy.settings, 'clear')({}).then =>
-        chrome.proxy.settings.get {}, @_proxyChangeListener
-        return
-    config = {}
-    if profile.profileType == 'DirectProfile'
-      config['mode'] = 'direct'
-    else if profile.profileType == 'PacProfile'
-      config['mode'] = 'pac_script'
-      
-      config['pacScript'] =
-        if !profile.pacScript || OmegaPac.Profiles.isFileUrl(profile.pacUrl)
-          url: profile.pacUrl
-          mandatory: true
-        else
-          data: OmegaPac.PacGenerator.ascii(profile.pacScript)
-          mandatory: true
-    else if profile.profileType == 'FixedProfile'
-      config = @_fixedProfileConfig(profile)
-    else
-      config['mode'] = 'pac_script'
-      config['pacScript'] =
-        data: null
-        mandatory: true
-      setPacScript = @pacForProfile(profile).then (script) ->
-        profileName = OmegaPac.PacGenerator.ascii(JSON.stringify(meta.name))
-        profileName = profileName.replace(/\*/g, '\\u002a')
-        profileName = profileName.replace(/\\/g, '\\u002f')
-        prefix = "/*OmegaProfile*#{profileName}*#{meta.revision}*/"
-        config['pacScript'].data = prefix + script
-        return
-    setPacScript ?= Promise.resolve()
-    setPacScript.then(=>
-      @_proxyAuth ?= new ProxyAuth(this)
-      @_proxyAuth.listen()
-      @_proxyAuth.setProxies(@_watchingProfiles)
-      chromeApiPromisify(chrome.proxy.settings, 'set')({value: config})
-    ).then =>
-      chrome.proxy.settings.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 unregister the script.
-      if browser.proxy.unregister?
-        browser.proxy.unregister()
-      else
-        # Some older browers may not ship with .unregister API.
-        # In that case, let's just set an invalid script to unregister it.
-        browser.proxy.registerProxyScript('js/omega_invalid_proxy_script.js')
-      @_proxyScriptDisabled = true
-    else
-      @_proxyScriptState = state
-      Promise.all([
-        browser.runtime.getBrowserInfo(),
-        @_initWebextProxyScript(),
-      ]).then ([info]) =>
-        if info.vendor == 'Mozilla' and info.buildID < '20170918220054'
-          # MOZ: Legacy proxy support expects PAC-like string return type.
-          # TODO(catus): Remove support for string return type.
-          @log.error(
-            'Your browser is outdated! SOCKS5 DNS/Auth unsupported! ' +
-            "Please update your browser ASAP! (Current Build #{info.buildID})")
-          @_proxyScriptState.useLegacyStringReturn = true
-        @_proxyScriptStateChanged()
-    @_proxyAuth ?= new ProxyAuth(this)
-    @_proxyAuth.listen()
-    @_proxyAuth.setProxies(@_watchingProfiles)
-    return Promise.resolve()
-
-  _proxyScriptInitialized: false
-  _proxyScriptState: {}
-  _initWebextProxyScript: ->
-    if not @_proxyScriptInitialized
-      browser.proxy.onProxyError.addListener (err) =>
-        if err?.message?
-          if err.message.indexOf('Invalid Proxy Rule: DIRECT') >= 0
-            # DIRECT cannot be parsed in Mozilla earlier 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
-          if err.message.indexOf('Return type must be a string') >= 0
-            # MOZ: Legacy proxy support expects PAC-like string return type.
-            # TODO(catus): Remove support for string return type.
-            #
-            @log.error(
-              'Your browser is outdated! SOCKS5 DNS/Auth unsupported! ' +
-              'Please update your browser ASAP!')
-            @_proxyScriptState.useLegacyStringReturn = true
-            @_proxyScriptStateChanged()
-            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.error(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
-      # The API has been renamed to .register but for some old browsers' sake:
-      if browser.proxy.register?
-        browser.proxy.register(@_proxyScriptUrl)
-      else
-        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
   _quickSwitchHandlerReady: false
   _quickSwitchCanEnable: false
@@ -478,4 +280,3 @@ class ChromeOptions extends OmegaTarget.Options
       }
 
 module.exports = ChromeOptions
-

+ 10 - 0
omega-target-chromium-extension/src/module/proxy/index.coffee

@@ -0,0 +1,10 @@
+ListenerProxyImpl = require('./proxy_impl_listener')
+SettingsProxyImpl = require('./proxy_impl_settings')
+ScriptProxyImpl = require('./proxy_impl_script')
+
+exports.proxyImpls = [ListenerProxyImpl, ScriptProxyImpl, SettingsProxyImpl]
+exports.getProxyImpl = (log) ->
+  for Impl in exports.proxyImpls
+    if Impl.isSupported()
+      return new Impl(log)
+  throw new Error('Your browser does not support proxy settings!')

+ 6 - 15
omega-target-chromium-extension/src/module/proxy_auth.coffee → omega-target-chromium-extension/src/module/proxy/proxy_auth.coffee

@@ -3,18 +3,18 @@ OmegaPac = OmegaTarget.OmegaPac
 Promise = OmegaTarget.Promise
 
 module.exports = class ProxyAuth
-  constructor: (options) ->
+  constructor: (log) ->
     @_requests = {}
-    @options = options
+    @log = log
 
   listening: false
   listen: ->
     return if @listening
     if not chrome.webRequest
-      @options.log.error('Proxy auth disabled! No webRequest permission.')
+      @log.error('Proxy auth disabled! No webRequest permission.')
       return
     if not chrome.webRequest.onAuthRequired
-      @options.log.error('Proxy auth disabled! onAuthRequired not available.')
+      @log.error('Proxy auth disabled! onAuthRequired not available.')
       return
     chrome.webRequest.onAuthRequired.addListener(
       @authHandler.bind(this)
@@ -35,9 +35,7 @@ module.exports = class ProxyAuth
   setProxies: (profiles) ->
     @_proxies = {}
     @_fallbacks = []
-    processProfile = (profile) =>
-      profile = @options.profile(profile)
-      return unless profile?.auth
+    for profile in profiles when profile.auth
       for scheme in OmegaPac.Profiles.schemes when profile[scheme.prop]
         auth = profile.auth?[scheme.prop]
         continue unless auth
@@ -59,13 +57,6 @@ module.exports = class ProxyAuth
           name: profile.name + '.' + 'all'
         })
 
-    if Array.isArray(profiles)
-      for profile in profiles
-        processProfile(profile)
-    else
-      for _, profile of profiles
-        processProfile(profile)
-
   _proxies: {}
   _fallbacks: []
   _requests: null
@@ -86,7 +77,7 @@ module.exports = class ProxyAuth
       proxy = list[req.authTries]
     else
       proxy = @_fallbacks[req.authTries - listLen]
-    @options.log.log('ProxyAuth', key, req.authTries, proxy?.name)
+    @log.log('ProxyAuth', key, req.authTries, proxy?.name)
 
     return {} unless proxy?
     req.authTries++

+ 41 - 0
omega-target-chromium-extension/src/module/proxy/proxy_impl.coffee

@@ -0,0 +1,41 @@
+OmegaTarget = require('omega-target')
+Promise = OmegaTarget.Promise
+ProxyAuth = require('./proxy_auth')
+
+class ProxyImpl
+  constructor: (log) ->
+    @log = log
+  @isSupported: -> false
+  applyProfile: (profile, meta) -> Promise.reject()
+  watchProxyChange: (callback) -> null
+  _profileNotFound: (name) ->
+    @log.error("Profile #{name} not found! Things may go very, very wrong.")
+    return OmegaPac.Profiles.create({
+      name: name
+      profileType: 'VirtualProfile'
+      defaultProfileName: 'direct'
+    })
+  setProxyAuth: (profile, options) ->
+    return Promise.try(=>
+      @_proxyAuth ?= new ProxyAuth(@log)
+      @_proxyAuth.listen()
+      referenced_profiles = []
+      ref_set = OmegaPac.Profiles.allReferenceSet(profile,
+        options, profileNotFound: @_profileNotFound.bind(this))
+      for own _, name of ref_set
+        referenced_profiles.push(OmegaPac.Profiles.byName(name, options))
+      @_proxyAuth.setProxies(referenced_profiles)
+    )
+  getProfilePacScript: (profile, meta, options) ->
+    meta ?= profile
+    ast = OmegaPac.PacGenerator.script(options, profile,
+      profileNotFound: @_profileNotFound.bind(this))
+    ast = OmegaPac.PacGenerator.compress(ast)
+    script = OmegaPac.PacGenerator.ascii(ast.print_to_string())
+    profileName = OmegaPac.PacGenerator.ascii(JSON.stringify(meta.name))
+    profileName = profileName.replace(/\*/g, '\\u002a')
+    profileName = profileName.replace(/\\/g, '\\u002f')
+    prefix = "/*OmegaProfile*#{profileName}*#{meta.revision}*/"
+    return prefix + script
+
+module.exports = ProxyImpl

+ 81 - 0
omega-target-chromium-extension/src/module/proxy/proxy_impl_listener.coffee

@@ -0,0 +1,81 @@
+OmegaTarget = require('omega-target')
+# The browser only accepts native promises as onRequest return values.
+# DO NOT USE Bluebird Promises here!
+NativePromise = Promise
+ProxyImpl = require('./proxy_impl')
+
+class ListenerProxyImpl extends ProxyImpl
+  @isSupported: -> browser?.proxy?.onRequest?
+  features: ['fullUrl', 'socks5Auth']
+  constructor: ->
+    super(arguments...)
+    @_optionsReady = new NativePromise (resolve) =>
+      @_optionsReadyCallback = resolve
+    # We want to register listeners early so that it can start blocking requests
+    # when starting the browser & extension, returning correct results later.
+    @_initRequestListeners()
+  _initRequestListeners: ->
+    browser.proxy.onRequest.addListener(@onRequest.bind(this),
+      {urls: ["<all_urls>"]})
+    browser.proxy.onError.addListener(@onError.bind(this))
+  watchProxyChange: (callback) -> null
+  applyProfile: (profile, state, options) ->
+    @_options = options
+    @_profile = profile
+    @_optionsReadyCallback?()
+    @_optionsReadyCallback = null
+    return @setProxyAuth(profile, options)
+  onRequest: (requestDetails) ->
+    # The browser only recognizes native promises return values, not Bluebird.
+    return NativePromise.resolve(@_optionsReady.then(=>
+      request = OmegaPac.Conditions.requestFromUrl(requestDetails.url)
+      profile = @_profile
+      while profile
+        result = OmegaPac.Profiles.match(profile, request)
+        if not result
+          switch profile.profileType
+            when 'DirectProfile'
+              return {type: 'direct'}
+            when 'SystemProfile'
+              # Returning undefined means using the default proxy from previous.
+              # https://hg.mozilla.org/mozilla-central/rev/9f0ee2f582a2#l1.337
+              return undefined
+            else
+              throw new Error('Unsupported profile: ' + profile.profileType)
+        if Array.isArray(result)
+          proxy = result[2]
+          auth = result[3]
+          return @proxyInfo(proxy, auth) if proxy
+          next = result[0]
+        else if result.profileName
+          next = OmegaPac.Profiles.nameAsKey(result.profileName)
+        else
+          break
+        profile = OmegaPac.Profiles.byKey(next, @_options)
+
+      throw new Error('Profile not found: ' + next)
+    ))
+  onError: (error) ->
+    @log.error(error)
+  proxyInfo: (proxy, auth) ->
+    proxyInfo =
+      type: proxy.scheme
+      host: proxy.host
+      port: proxy.port
+    if proxyInfo.type == 'socks5'
+      # MOZ: SOCKS5 proxies should be specified as "type": "socks".
+      # https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/proxy/ProxyInfo
+      proxyInfo.type = 'socks'
+      if auth
+        # Username & password here are only available for SOCKS5.
+        # https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/proxy/ProxyInfo
+        # HTTP proxy auth must be handled via webRequest.onAuthRequired.
+        proxyInfo.username = auth.username
+        proxyInfo.password = auth.password
+    if proxyInfo.type == 'socks' or proxyInfo.type == 'socks4'
+      # Enable SOCKS remote DNS.
+      # TODO(catus): Maybe allow the users to configure this?
+      proxyInfo.proxyDNS = true
+    return [proxyInfo]
+
+module.exports = ListenerProxyImpl

+ 107 - 0
omega-target-chromium-extension/src/module/proxy/proxy_impl_script.coffee

@@ -0,0 +1,107 @@
+OmegaTarget = require('omega-target')
+Promise = OmegaTarget.Promise
+ProxyImpl = require('./proxy_impl')
+
+class ScriptProxyImpl extends ProxyImpl
+  @isSupported: ->
+    return browser?.proxy?.register? or browser?.proxy?.registerProxyScript?
+  features: ['socks5Auth']
+  _proxyScriptUrl: 'js/omega_webext_proxy_script.min.js'
+  _proxyScriptDisabled: false
+  _proxyScriptInitialized: false
+  _proxyScriptState: {}
+  watchProxyChange: (callback) -> null
+  applyProfile: (profile, state, options) ->
+    @log.error(
+      'Your browser is outdated! Full-URL based matching, etc. unsupported! ' +
+      "Please update your browser ASAP!")
+    state = state ? {}
+    @_options = options
+    state.currentProfileName = profile.name
+    if profile.name == ''
+      state.tempProfile = profile
+    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 unregister the script.
+      if browser.proxy.unregister?
+        browser.proxy.unregister()
+      else
+        # Some older browers may not ship with .unregister API.
+        # In that case, let's just set an invalid script to unregister it.
+        browser.proxy.registerProxyScript('js/omega_invalid_proxy_script.js')
+      @_proxyScriptDisabled = true
+    else
+      @_proxyScriptState = state
+      Promise.all([
+        browser.runtime.getBrowserInfo(),
+        @_initWebextProxyScript(),
+      ]).then ([info]) =>
+        if info.vendor == 'Mozilla' and info.buildID < '20170918220054'
+          # MOZ: Legacy proxy support expects PAC-like string return type.
+          # TODO(catus): Remove support for string return type.
+          @log.error(
+            'Your browser is outdated! SOCKS5 DNS/Auth unsupported! ' +
+            "Please update your browser ASAP! (Current Build #{info.buildID})")
+          @_proxyScriptState.useLegacyStringReturn = true
+        @_proxyScriptStateChanged()
+    return @setProxyAuth(profile, options)
+  _initWebextProxyScript: ->
+    if not @_proxyScriptInitialized
+      browser.proxy.onProxyError.addListener (err) =>
+        if err?.message?
+          if err.message.indexOf('Invalid Proxy Rule: DIRECT') >= 0
+            # DIRECT cannot be parsed in Mozilla earlier 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
+          if err.message.indexOf('Return type must be a string') >= 0
+            # MOZ: Legacy proxy support expects PAC-like string return type.
+            # TODO(catus): Remove support for string return type.
+            #
+            @log.error(
+              'Your browser is outdated! SOCKS5 DNS/Auth unsupported! ' +
+              'Please update your browser ASAP!')
+            @_proxyScriptState.useLegacyStringReturn = true
+            @_proxyScriptStateChanged()
+            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.error(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
+      # The API has been renamed to .register but for some old browsers' sake:
+      if browser.proxy.register?
+        browser.proxy.register(@_proxyScriptUrl)
+      else
+        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
+    })
+
+module.exports = ScriptProxyImpl

+ 90 - 0
omega-target-chromium-extension/src/module/proxy/proxy_impl_settings.coffee

@@ -0,0 +1,90 @@
+sOmegaTarget = require('omega-target')
+Promise = OmegaTarget.Promise
+chromeApiPromisify = require('../chrome_api').chromeApiPromisify
+ProxyImpl = require('./proxy_impl')
+
+class SettingsProxyImpl extends ProxyImpl
+  @isSupported: -> chrome?.proxy?.settings?
+  features: ['fullUrlHttp', 'pacScript', 'watchProxyChange']
+  applyProfile: (profile, meta, options) ->
+    meta ?= profile
+    if profile.profileType == 'SystemProfile'
+      # Clear proxy settings, returning proxy control to Chromium.
+      return chromeApiPromisify(chrome.proxy.settings, 'clear')({}).then =>
+        chrome.proxy.settings.get {}, @_proxyChangeListener
+        return
+    config = {}
+    if profile.profileType == 'DirectProfile'
+      config['mode'] = 'direct'
+    else if profile.profileType == 'PacProfile'
+      config['mode'] = 'pac_script'
+
+      config['pacScript'] =
+        if !profile.pacScript || OmegaPac.Profiles.isFileUrl(profile.pacUrl)
+          url: profile.pacUrl
+          mandatory: true
+        else
+          data: OmegaPac.PacGenerator.ascii(profile.pacScript)
+          mandatory: true
+    else if profile.profileType == 'FixedProfile'
+      config = @_fixedProfileConfig(profile)
+    else
+      config['mode'] = 'pac_script'
+      config['pacScript'] =
+        mandatory: true
+        data: @getProfilePacScript(profile, meta, options)
+    return @setProxyAuth(profile, options).then(->
+      return chromeApiPromisify(chrome.proxy.settings, 'set')({value: config})
+    ).then(=>
+      chrome.proxy.settings.get {}, @_proxyChangeListener
+      return
+    )
+  _fixedProfileConfig: (profile) ->
+    config = {}
+    config['mode'] = 'fixed_servers'
+    rules = {}
+    protocols = ['proxyForHttp', 'proxyForHttps', 'proxyForFtp']
+    protocolProxySet = false
+    for protocol in protocols when profile[protocol]?
+      rules[protocol] = profile[protocol]
+      protocolProxySet = true
+
+    if profile.fallbackProxy
+      if profile.fallbackProxy.scheme == 'http'
+        # Chromium does not allow HTTP proxies in 'fallbackProxy'.
+        if not protocolProxySet
+          # Use 'singleProxy' if no proxy is configured for other protocols.
+          rules['singleProxy'] = profile.fallbackProxy
+        else
+          # Try to set the proxies of all possible protocols.
+          for protocol in protocols
+            rules[protocol] ?= JSON.parse(JSON.stringify(profile.fallbackProxy))
+      else
+        rules['fallbackProxy'] = profile.fallbackProxy
+    else if not protocolProxySet
+      config['mode'] = 'direct'
+
+    if config['mode'] != 'direct'
+      rules['bypassList'] = bypassList = []
+      for condition in profile.bypassList
+        bypassList.push(@_formatBypassItem(condition))
+      config['rules'] = rules
+    return config
+  _formatBypassItem: (condition) ->
+    str = OmegaPac.Conditions.str(condition)
+    i = str.indexOf(' ')
+    return str.substr(i + 1)
+
+  _proxyChangeWatchers: null
+  _proxyChangeListener: (details) ->
+    for watcher in (@_proxyChangeWatchers ? [])
+      watcher(details)
+  watchProxyChange: (callback) ->
+    if not @_proxyChangeWatchers?
+      @_proxyChangeWatchers = []
+      if chrome?.proxy?.settings?.onChange?
+        chrome.proxy.settings.onChange.addListener @_proxyChangeListener
+    @_proxyChangeWatchers.push(callback)
+    return
+
+module.exports = SettingsProxyImpl

+ 4 - 13
omega-target/src/options.coffee

@@ -54,7 +54,7 @@ class Options
         value = profile
     return value
 
-  constructor: (options, @_storage, @_state, @log, @sync) ->
+  constructor: (options, @_storage, @_state, @log, @sync, @proxyImpl) ->
     @_options = {}
     @_tempProfileRules = {}
     @_tempProfileRulesByProfile = {}
@@ -566,9 +566,10 @@ class Options
 
       @_watchingProfiles = OmegaPac.Profiles.allReferenceSet(@_tempProfile,
         @_options, profileNotFound: @_profileNotFound.bind(this))
-      applyProxy = @applyProfileProxy(@_tempProfile, profile)
+
+      applyProxy = @proxyImpl.applyProfile(@_tempProfile, profile, @_options)
     else
-      applyProxy = @applyProfileProxy(profile)
+      applyProxy = @proxyImpl.applyProfile(profile, profile, @_options)
 
     return applyProxy if options? and options.update == false
 
@@ -598,16 +599,6 @@ class Options
   ###
   isSystem: -> @_isSystem
 
-  ###*
-  # Set proxy settings based on the given profile.
-  # In base class, this method is not implemented and will always reject.
-  # @param {{}} profile The profile to apply
-  # @param {{}=profile} meta The metadata of the profile, like name and revision
-  # @returns {Promise} A promise which is fulfilled when the proxy is set.
-  ###
-  applyProfileProxy: (profile, meta) ->
-    Promise.reject new Error('not implemented')
-
   ###*
   # Called when current profile has changed.
   # In base class, this method is not implemented and will not do anything.