mirror of
https://github.com/zero-peak/ZeroOmega.git
synced 2025-02-12 23:28:13 -05:00
444 lines
15 KiB
CoffeeScript
444 lines
15 KiB
CoffeeScript
U2 = require 'uglify-js'
|
|
ShexpUtils = require './shexp_utils'
|
|
Conditions = require './conditions'
|
|
RuleList = require './rule_list'
|
|
{AttachedCache, Revision} = require './utils'
|
|
|
|
# coffeelint: disable=camel_case_classes
|
|
class AST_Raw extends U2.AST_SymbolRef
|
|
# coffeelint: enable=camel_case_classes
|
|
constructor: (raw) ->
|
|
U2.AST_SymbolRef.call(this, name: raw)
|
|
@aborts = -> false
|
|
|
|
module.exports = exports =
|
|
builtinProfiles:
|
|
'+direct':
|
|
name: 'direct'
|
|
profileType: 'DirectProfile'
|
|
color: '#aaaaaa'
|
|
builtin: true
|
|
'+system':
|
|
name: 'system'
|
|
profileType: 'SystemProfile'
|
|
color: '#000000'
|
|
builtin: true
|
|
|
|
schemes: [
|
|
{scheme: 'http', prop: 'proxyForHttp'}
|
|
{scheme: 'https', prop: 'proxyForHttps'}
|
|
{scheme: 'ftp', prop: 'proxyForFtp'}
|
|
{scheme: '', prop: 'fallbackProxy'}
|
|
]
|
|
|
|
pacProtocols: {
|
|
'http': 'PROXY'
|
|
'https': 'HTTPS'
|
|
'socks4': 'SOCKS'
|
|
'socks5': 'SOCKS5'
|
|
}
|
|
|
|
formatByType: {
|
|
'SwitchyRuleListProfile': 'Switchy'
|
|
'AutoProxyRuleListProfile': 'AutoProxy'
|
|
}
|
|
|
|
ruleListFormats: [
|
|
'Switchy'
|
|
'AutoProxy'
|
|
]
|
|
|
|
parseHostPort: (str, scheme) ->
|
|
sep = str.lastIndexOf(':')
|
|
return if sep < 0
|
|
port = parseInt(str.substr(sep + 1)) || 80
|
|
host = str.substr(0, sep)
|
|
return unless host
|
|
return {
|
|
scheme: scheme
|
|
host: host
|
|
port: port
|
|
}
|
|
|
|
pacResult: (proxy) ->
|
|
if proxy
|
|
"#{exports.pacProtocols[proxy.scheme]} #{proxy.host}:#{proxy.port}"
|
|
else
|
|
'DIRECT'
|
|
|
|
isFileUrl: (url) -> !!(url?.substr(0, 5).toUpperCase() == 'FILE:')
|
|
|
|
nameAsKey: (profileName) ->
|
|
if typeof profileName != 'string'
|
|
profileName = profileName.name
|
|
'+' + profileName
|
|
byName: (profileName, options) ->
|
|
if typeof profileName == 'string'
|
|
key = exports.nameAsKey(profileName)
|
|
profileName = exports.builtinProfiles[key] ? options[key]
|
|
profileName
|
|
byKey: (key, options) ->
|
|
if typeof key == 'string'
|
|
key = exports.builtinProfiles[key] ? options[key]
|
|
key
|
|
|
|
each: (options, callback) ->
|
|
charCodePlus = '+'.charCodeAt(0)
|
|
for key, profile of options when key.charCodeAt(0) == charCodePlus
|
|
callback(key, profile)
|
|
for key, profile of exports.builtinProfiles
|
|
if key.charCodeAt(0) == charCodePlus
|
|
callback(key, profile)
|
|
|
|
profileResult: (profileName) ->
|
|
key = exports.nameAsKey(profileName)
|
|
if key == '+direct'
|
|
key = exports.pacResult()
|
|
new U2.AST_String value: key
|
|
|
|
isIncludable: (profile) ->
|
|
includable = exports._handler(profile).includable
|
|
if typeof includable == 'function'
|
|
includable = includable.call(exports, profile)
|
|
!!includable
|
|
isInclusive: (profile) -> !!exports._handler(profile).inclusive
|
|
|
|
updateUrl: (profile) ->
|
|
exports._handler(profile).updateUrl?.call(exports, profile)
|
|
update: (profile, data) ->
|
|
exports._handler(profile).update.call(exports, profile, data)
|
|
|
|
tag: (profile) -> exports._profileCache.tag(profile)
|
|
create: (profile, opt_profileType) ->
|
|
if typeof profile == 'string'
|
|
profile =
|
|
name: profile
|
|
profileType: opt_profileType
|
|
else if opt_profileType
|
|
profile.profileType = opt_profileType
|
|
create = exports._handler(profile).create
|
|
return profile unless create
|
|
create.call(exports, profile)
|
|
profile
|
|
updateRevision: (profile, revision) ->
|
|
revision ?= Revision.fromTime()
|
|
profile.revision = revision
|
|
replaceRef: (profile, fromName, toName) ->
|
|
return false if not exports.isInclusive(profile)
|
|
handler = exports._handler(profile)
|
|
handler.replaceRef.call(exports, profile, fromName, toName)
|
|
analyze: (profile) ->
|
|
cache = exports._profileCache.get profile, {}
|
|
if not Object::hasOwnProperty.call(cache, 'analyzed')
|
|
analyze = exports._handler(profile).analyze
|
|
result = analyze?.call(exports, profile)
|
|
cache.analyzed = result
|
|
return cache
|
|
dropCache: (profile) ->
|
|
exports._profileCache.drop profile
|
|
directReferenceSet: (profile) ->
|
|
return {} if not exports.isInclusive(profile)
|
|
cache = exports._profileCache.get profile, {}
|
|
return cache.directReferenceSet if cache.directReferenceSet
|
|
handler = exports._handler(profile)
|
|
cache.directReferenceSet = handler.directReferenceSet.call(exports, profile)
|
|
|
|
profileNotFound: (name, action) ->
|
|
if not action?
|
|
throw new Error("Profile #{name} does not exist!")
|
|
if typeof action == 'function'
|
|
action = action(name)
|
|
if typeof action == 'object' and action.profileType
|
|
return action
|
|
switch action
|
|
when 'ignore'
|
|
return null
|
|
when 'dumb'
|
|
return exports.create({
|
|
name: name
|
|
profileType: 'VirtualProfile'
|
|
defaultProfileName: 'direct'
|
|
})
|
|
throw action
|
|
|
|
allReferenceSet: (profile, options, opt_args) ->
|
|
o_profile = profile
|
|
profile = exports.byName(profile, options)
|
|
profile ?= exports.profileNotFound?(o_profile, opt_args.profileNotFound)
|
|
opt_args ?= {}
|
|
has_out = opt_args.out?
|
|
result = opt_args.out ?= {}
|
|
if profile
|
|
result[exports.nameAsKey(profile.name)] = profile.name
|
|
for key, name of exports.directReferenceSet(profile)
|
|
exports.allReferenceSet(name, options, opt_args)
|
|
delete opt_args.out if not has_out
|
|
result
|
|
referencedBySet: (profile, options, opt_args) ->
|
|
profileKey = exports.nameAsKey(profile)
|
|
opt_args ?= {}
|
|
has_out = opt_args.out?
|
|
result = opt_args.out ?= {}
|
|
exports.each options, (key, prof) ->
|
|
if exports.directReferenceSet(prof)[profileKey]
|
|
result[key] = prof.name
|
|
exports.referencedBySet(prof, options, opt_args)
|
|
delete opt_args.out if not has_out
|
|
result
|
|
validResultProfilesFor: (profile, options) ->
|
|
profile = exports.byName(profile, options)
|
|
return [] if not exports.isInclusive(profile)
|
|
profileKey = exports.nameAsKey(profile)
|
|
ref = exports.referencedBySet(profile, options)
|
|
ref[profileKey] = profileKey
|
|
result = []
|
|
exports.each options, (key, prof) ->
|
|
if not ref[key] and exports.isIncludable(prof)
|
|
result.push(prof)
|
|
result
|
|
match: (profile, request, opt_profileType) ->
|
|
opt_profileType ?= profile.profileType
|
|
cache = exports.analyze(profile)
|
|
match = exports._handler(opt_profileType).match
|
|
match?.call(exports, profile, request, cache)
|
|
compile: (profile, opt_profileType) ->
|
|
opt_profileType ?= profile.profileType
|
|
cache = exports.analyze(profile)
|
|
return cache.compiled if cache.compiled
|
|
handler = exports._handler(opt_profileType)
|
|
cache.compiled = handler.compile.call(exports, profile, cache)
|
|
|
|
_profileCache: new AttachedCache (profile) -> profile.revision
|
|
|
|
_handler: (profileType) ->
|
|
if typeof profileType != 'string'
|
|
profileType = profileType.profileType
|
|
|
|
handler = profileType
|
|
while typeof handler == 'string'
|
|
handler = exports._profileTypes[handler]
|
|
if not handler?
|
|
throw new Error "Unknown profile type: #{profileType}"
|
|
return handler
|
|
|
|
_profileTypes:
|
|
# These functions are .call()-ed with `this` set to module.exports.
|
|
# coffeelint: disable=missing_fat_arrows
|
|
'SystemProfile':
|
|
compile: (profile) ->
|
|
throw new Error "SystemProfile cannot be used in PAC scripts"
|
|
'DirectProfile':
|
|
includable: true
|
|
compile: (profile) ->
|
|
return new U2.AST_String(value: @pacResult())
|
|
'FixedProfile':
|
|
includable: true
|
|
create: (profile) ->
|
|
profile.bypassList ?= [{
|
|
conditionType: 'BypassCondition'
|
|
pattern: '<local>'
|
|
}]
|
|
match: (profile, request) ->
|
|
if profile.bypassList
|
|
for cond in profile.bypassList
|
|
if Conditions.match(cond, request)
|
|
return [@pacResult(), cond]
|
|
for s in @schemes when s.scheme == request.scheme and profile[s.prop]
|
|
return [@pacResult(profile[s.prop]), s.scheme]
|
|
return [@pacResult(profile.fallbackProxy), '']
|
|
compile: (profile) ->
|
|
if ((not profile.bypassList or not profile.fallbackProxy) and
|
|
not profile.proxyForHttp and not profile.proxyForHttps and
|
|
not profile.proxyForFtp)
|
|
return new U2.AST_String value:
|
|
@pacResult profile.fallbackProxy
|
|
body = [
|
|
new U2.AST_Directive value: 'use strict'
|
|
]
|
|
if profile.bypassList and profile.bypassList.length
|
|
conditions = null
|
|
for cond in profile.bypassList
|
|
condition = Conditions.compile cond
|
|
if conditions?
|
|
conditions = new U2.AST_Binary(
|
|
left: conditions
|
|
operator: '||'
|
|
right: condition
|
|
)
|
|
else
|
|
conditions = condition
|
|
body.push new U2.AST_If(
|
|
condition: conditions
|
|
body: new U2.AST_Return value: new U2.AST_String value: @pacResult()
|
|
)
|
|
if (not profile.proxyForHttp and not profile.proxyForHttps and
|
|
not profile.proxyForFtp)
|
|
body.push new U2.AST_Return value:
|
|
new U2.AST_String value: @pacResult profile.fallbackProxy
|
|
else
|
|
body.push new U2.AST_Switch(
|
|
expression: new U2.AST_SymbolRef name: 'scheme'
|
|
body: for s in @schemes when not s.scheme or profile[s.prop]
|
|
ret = [new U2.AST_Return value:
|
|
new U2.AST_String value: @pacResult profile[s.prop]
|
|
]
|
|
if s.scheme
|
|
new U2.AST_Case(
|
|
expression: new U2.AST_String value: s.scheme
|
|
body: ret
|
|
)
|
|
else
|
|
new U2.AST_Default body: ret
|
|
)
|
|
new U2.AST_Function(
|
|
argnames: [
|
|
new U2.AST_SymbolFunarg name: 'url'
|
|
new U2.AST_SymbolFunarg name: 'host'
|
|
new U2.AST_SymbolFunarg name: 'scheme'
|
|
]
|
|
body: body
|
|
)
|
|
'PacProfile':
|
|
includable: (profile) -> !@isFileUrl(profile.pacUrl)
|
|
create: (profile) ->
|
|
profile.pacScript ?= '''
|
|
function FindProxyForURL(url, host) {
|
|
return "DIRECT";
|
|
}
|
|
'''
|
|
compile: (profile) ->
|
|
new U2.AST_Call args: [new U2.AST_This], expression:
|
|
new U2.AST_Dot property: 'call', expression: new U2.AST_Function(
|
|
argnames: []
|
|
body: [
|
|
# TODO(catus): Remove the hack needed to insert raw code.
|
|
new AST_Raw ';\n' + profile.pacScript + ';'
|
|
new U2.AST_Return value:
|
|
new U2.AST_SymbolRef name: 'FindProxyForURL'
|
|
]
|
|
)
|
|
updateUrl: (profile) ->
|
|
if @isFileUrl(profile.pacUrl)
|
|
undefined
|
|
else
|
|
profile.pacUrl
|
|
update: (profile, data) ->
|
|
return false if profile.pacScript == data
|
|
profile.pacScript = data
|
|
return true
|
|
'AutoDetectProfile': 'PacProfile'
|
|
'SwitchProfile':
|
|
includable: true
|
|
inclusive: true
|
|
create: (profile) ->
|
|
profile.defaultProfileName ?= 'direct'
|
|
profile.rules ?= []
|
|
directReferenceSet: (profile) ->
|
|
refs = {}
|
|
refs[exports.nameAsKey(profile.defaultProfileName)] =
|
|
profile.defaultProfileName
|
|
for rule in profile.rules
|
|
refs[exports.nameAsKey(rule.profileName)] = rule.profileName
|
|
refs
|
|
analyze: (profile) -> profile.rules
|
|
replaceRef: (profile, fromName, toName) ->
|
|
changed = false
|
|
if profile.defaultProfileName == fromName
|
|
profile.defaultProfileName = toName
|
|
changed = true
|
|
for rule in profile.rules
|
|
if rule.profileName == fromName
|
|
rule.profileName = toName
|
|
changed = true
|
|
return changed
|
|
match: (profile, request, cache) ->
|
|
for rule in cache.analyzed
|
|
if Conditions.match(rule.condition, request)
|
|
return rule
|
|
return [exports.nameAsKey(profile.defaultProfileName), null]
|
|
compile: (profile, cache) ->
|
|
rules = cache.analyzed
|
|
if rules.length == 0
|
|
return @profileResult profile.defaultProfileName
|
|
body = [
|
|
new U2.AST_Directive value: 'use strict'
|
|
]
|
|
for rule in rules
|
|
body.push new U2.AST_If
|
|
condition: Conditions.compile rule.condition
|
|
body: new U2.AST_Return value:
|
|
@profileResult(rule.profileName)
|
|
body.push new U2.AST_Return value:
|
|
@profileResult profile.defaultProfileName
|
|
new U2.AST_Function(
|
|
argnames: [
|
|
new U2.AST_SymbolFunarg name: 'url'
|
|
new U2.AST_SymbolFunarg name: 'host'
|
|
new U2.AST_SymbolFunarg name: 'scheme'
|
|
]
|
|
body: body
|
|
)
|
|
'VirtualProfile': 'SwitchProfile'
|
|
'RuleListProfile':
|
|
includable: true
|
|
inclusive: true
|
|
create: (profile) ->
|
|
profile.profileType ?= 'RuleListProfile'
|
|
profile.format ?= exports.formatByType[profile.profileType] ? 'Switchy'
|
|
profile.defaultProfileName ?= 'direct'
|
|
profile.matchProfileName ?= 'direct'
|
|
profile.ruleList ?= ''
|
|
directReferenceSet: (profile) ->
|
|
refs = RuleList[profile.format]?.directReferenceSet?(profile)
|
|
return refs if refs
|
|
refs = {}
|
|
for name in [profile.matchProfileName, profile.defaultProfileName]
|
|
refs[exports.nameAsKey(name)] = name
|
|
refs
|
|
replaceRef: (profile, fromName, toName) ->
|
|
changed = false
|
|
if profile.defaultProfileName == fromName
|
|
profile.defaultProfileName = toName
|
|
changed = true
|
|
if profile.matchProfileName == fromName
|
|
profile.matchProfileName = toName
|
|
changed = true
|
|
return changed
|
|
analyze: (profile) ->
|
|
format = profile.format ? exports.formatByType[profile.profileType]
|
|
formatHandler = RuleList[format]
|
|
if not formatHandler
|
|
throw new Error "Unsupported rule list format #{format}!"
|
|
ruleList = profile.ruleList?.trim() || ''
|
|
if formatHandler.preprocess?
|
|
ruleList = formatHandler.preprocess(ruleList)
|
|
return formatHandler.parse(ruleList, profile.matchProfileName,
|
|
profile.defaultProfileName)
|
|
match: (profile, request) ->
|
|
result = exports.match(profile, request, 'SwitchProfile')
|
|
compile: (profile) ->
|
|
exports.compile(profile, 'SwitchProfile')
|
|
updateUrl: (profile) -> profile.sourceUrl
|
|
update: (profile, data) ->
|
|
data = data.trim()
|
|
original = profile.format ? exports.formatByType[profile.profileType]
|
|
profile.profileType = 'RuleListProfile'
|
|
format = original
|
|
if RuleList[format].detect?(data) == false
|
|
# Wrong data for the current format.
|
|
format = null
|
|
for own formatName of RuleList
|
|
result = RuleList[formatName].detect?(data)
|
|
if result == true or (result != false and not format?)
|
|
profile.format = format = formatName
|
|
format ?= original
|
|
formatHandler = RuleList[format]
|
|
if formatHandler.preprocess?
|
|
data = formatHandler.preprocess(data)
|
|
return false if profile.ruleList == data
|
|
profile.ruleList = data
|
|
return true
|
|
'SwitchyRuleListProfile': 'RuleListProfile'
|
|
'AutoProxyRuleListProfile': 'RuleListProfile'
|
|
# coffeelint: enable=missing_fat_arrows
|