Implement proxy script for WebExtensions. #102.

This commit is contained in:
FelisCatus 2017-04-13 22:41:08 -04:00
parent d65c29f34d
commit f421810959
7 changed files with 216 additions and 2 deletions

View File

@ -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]

View File

@ -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')]

View File

@ -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']

View File

@ -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);
}
}
});
}
})();

View File

@ -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": "switchyomega@example.com",
"strict_min_version": "55.0a1"
}
}
}

View File

@ -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"
}
}

View File

@ -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