add PAC Profiles support on Firefox PC #74

This commit is contained in:
proxy.zeroomega 2025-01-13 09:15:16 +08:00
parent 0fd0a945b2
commit ae7c07cb4c
6 changed files with 199 additions and 8 deletions

View File

@ -1,8 +1,14 @@
FirefoxProxyImpl = require('./proxy_impl_firefox')
ListenerProxyImpl = require('./proxy_impl_listener') ListenerProxyImpl = require('./proxy_impl_listener')
SettingsProxyImpl = require('./proxy_impl_settings') SettingsProxyImpl = require('./proxy_impl_settings')
ScriptProxyImpl = require('./proxy_impl_script') ScriptProxyImpl = require('./proxy_impl_script')
exports.proxyImpls = [ListenerProxyImpl, ScriptProxyImpl, SettingsProxyImpl] exports.proxyImpls = [
FirefoxProxyImpl,
ListenerProxyImpl,
ScriptProxyImpl,
SettingsProxyImpl
]
exports.getProxyImpl = (log) -> exports.getProxyImpl = (log) ->
for Impl in exports.proxyImpls for Impl in exports.proxyImpls
if Impl.isSupported() if Impl.isSupported()

View File

@ -0,0 +1,121 @@
OmegaTarget = require('omega-target')
# The browser only accepts native promises as onRequest return values.
# DO NOT USE Bluebird Promises here!
NativePromise = Promise ? null
ProxyImpl = require('./proxy_impl')
blobUrl = null
class FirefoxProxyImpl extends ProxyImpl
@isSupported: ->
# chrome.contextMenus check is Android or PC,
# browser.proxy.settings doesn't support on Firefox Android
return !!chrome.contextMenus and
Promise? and
browser?.proxy?.onRequest? and
browser?.proxy?.settings
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) ->
if blobUrl
URL.revokeObjectURL(blobUrl)
if browser.extension.isAllowedIncognitoAccess()
if profile.profileType is 'DirectProfile'
browser.proxy.settings.set({
value: {
proxyType: 'none'
}
})
else if profile.profileType is 'SystemProfile'
# Clear proxy settings, returning proxy control to Firefox.
browser.proxy.settings.clear({})
else
pacScript = @getProfilePacScript(profile, state, options)
blob = new Blob(
[pacScript],
{ type: 'application/x-ns-proxy-autoconfig' })
blobUrl = URL.createObjectURL(blob)
browser.proxy.settings.set({
value: {
proxyType: 'autoConfig',
autoConfigUrl: blobUrl
}
})
@_options = options
@_profile = profile
@_optionsReadyCallback?()
@_optionsReadyCallback = null
return @setProxyAuth(profile, options)
onRequest: (requestDetails) ->
return undefined if browser.extension.isAllowedIncognitoAccess()
# TODO
#return undefined
# 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'
# Enable SOCKS remote DNS.
# TODO(catus): Maybe allow the users to configure this?
proxyInfo.proxyDNS = true
# TODO(catus): Maybe allow proxyDNS for socks4? Server may support SOCKS4a.
# It cannot default to true though, since SOCKS4 servers that does not have
# the SOCKS4a extension may simply refuse to work.
return [proxyInfo]
module.exports = FirefoxProxyImpl

View File

@ -2,10 +2,48 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title></title> <title>Permissions required</title>
<style>
*{
box-sizing: border-box;
}
html,body{
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}
.incognito-access-img-container{
margin-right: 20px;
}
.incognito-access-img{
width: 100%;
max-width: 100%;
border: 2px solid;
border-radius: 5px;
height: auto;
}
</style>
</head> </head>
<body> <body style="opacity: 0">
<button id="grant-permissions-btn">grant site permissions</button> <ol style="margin-right: 20px;">
<li class="site-permissions-required">
<div>
<p>
<strong>Site permissions is required.</strong> The Api <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/proxy/onRequest">proxy.onRequest</a> requires site permissions. <button id="grant-permissions-btn">grant site permissions</button>
</p>
</div>
</li>
<li class="incognito-access-required">
<div>
<p><strong>IncognitoAccess is required.</strong>According to <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/proxy/settings" target="_blank"/>Firefox API</a> requirements.Changing proxy settings requires private browsing window access.</p>
<div class="incognito-access-img-container">
<img class="incognito-access-img" src="./img/AllowedIncognitoAccess.png"/>
</div>
</div>
</li>
</ol>
<dialog opened class="loading-dialog">loading...</dialog>
<script src="../js/omega_target_popup.js"></script> <script src="../js/omega_target_popup.js"></script>
<script src="js/style.js"></script> <script src="js/style.js"></script>
<script src="js/grant_permissions.js"></script> <script src="js/grant_permissions.js"></script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

View File

@ -1,9 +1,32 @@
window.addEventListener('load', async function(){
const dialogEl = document.querySelector('.loading-dialog')
dialogEl.showModal()
dialogEl.addEventListener('cancel', (e)=>e.preventDefault())
const permissionValue = {origins: ["<all_urls>"]}
const hasPermission = await browser.permissions.contains(permissionValue);
const isAllowedIncognitoAccess = !chrome.contextMenus || await browser.extension.isAllowedIncognitoAccess()
if (hasPermission) {
document.querySelector('.site-permissions-required').remove()
}
if (isAllowedIncognitoAccess) {
document.querySelector('.incognito-access-required').remove()
}
dialogEl.remove()
document.body.style.opacity = '';
}, {once: true})
const btn = document.querySelector('#grant-permissions-btn') const btn = document.querySelector('#grant-permissions-btn')
btn.onclick = async ()=>{ btn.onclick = async ()=>{
const permissionValue = {origins: ["<all_urls>"]} const permissionValue = {origins: ["<all_urls>"]}
const hasPermission = await browser.permissions.request(permissionValue); const hasPermission = await browser.permissions.request(permissionValue);
const isAllowedIncognitoAccess = !chrome.contextMenus || await browser.extension.isAllowedIncognitoAccess()
if (hasPermission) { if (hasPermission) {
document.querySelector('.site-permissions-required')?.remove()
}
if (isAllowedIncognitoAccess) {
document.querySelector('.incognito-access-required')?.remove()
}
if (hasPermission && isAllowedIncognitoAccess) {
location.href = 'index.html' location.href = 'index.html'
} }
} }

View File

@ -25,11 +25,14 @@ $script('../js/omega_target_popup.js', 'om-target', function() {
} }
const permissionValue = {origins: ["<all_urls>"]} const permissionValue = {origins: ["<all_urls>"]}
if (globalThis.browser && browser.proxy && browser.proxy.onRequest){ if (globalThis.browser && browser.proxy && browser.proxy.onRequest){
chrome.permissions.contains(permissionValue).then((hasPermission)=>{ Promise.all([browser.permissions.contains(permissionValue), browser.extension.isAllowedIncognitoAccess()])
if (!hasPermission) { .then(([sitePermissions, isAllowedIncognitoAccess])=>{
location.href = 'grant_permissions.html' // chrome.contextMenus check is Android or PC,
} else { // browser.proxy.settings doesn't support Android
if (sitePermissions && (!chrome.contextMenus || isAllowedIncognitoAccess)) {
init(); init();
} else {
location.href = 'grant_permissions.html'
} }
}) })
} else { } else {