ZeroOmega/omega-pac/src/profiles.coffee
2015-02-09 13:56:18 +08:00

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