Files
dev-sidecar/packages/mitmproxy/src/lib/proxy/middleware/source/pac.js
2025-02-08 16:26:25 +08:00

680 lines
21 KiB
JavaScript

const fs = require('node:fs')
const path = require('node:path')
const log = require('../../../../utils/util.log')
function createPacClient (pacFilePath) {
const __PROXY__ = 'PROXY 127.0.0.1:1080;'
function readFile (location) {
try {
log.info('pac root dir:', path.resolve('./'))
log.info('pac location:', location)
const filePath = path.resolve(location)
log.info('read pac path:', filePath)
return fs.readFileSync(location).toString()
} catch (e) {
log.error('读取pac失败:', e)
return ''
}
}
const getRules = function (pacFilePath) {
let text = readFile(pacFilePath)
if (!text.includes('!---------------------EOF')) {
text = Buffer.from(text, 'base64').toString()
}
const rules = []
const arr = text.split('\n')
for (const line of arr) {
const row = line.trim()
if (row === '' || row.indexOf('!') === 0 || row.indexOf('[') === 0) {
continue
}
rules.push(row)
}
return rules
}
const __RULES__ = getRules(pacFilePath)
/* eslint-disable */
// Was generated by gfwlist2pac in precise mode
// https://github.com/clowwindy/gfwlist2pac
// 2019-10-06: More 'javascript' way to interaction with main program
// 2019-02-08: Updated to support shadowsocks-windows user rules.
const proxy = __PROXY__
const rules = []
// convert to abp grammar
for (let i = 0; i < __RULES__.length; i++) {
let s = __RULES__[i]
if (s.substring(0, 2) === "||") s += "^"
rules.push(s)
}
/*
* This file is part of Adblock Plus <http://adblockplus.org/>,
* Copyright (C) 2006-2014 Eyeo GmbH
*
* Adblock Plus is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* Adblock Plus is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
*/
function createDict () {
const result = {}
result.__proto__ = null
return result
}
function getOwnPropertyDescriptor (obj, key) {
if (obj.hasOwnProperty(key)) {
return obj[key]
}
return null
}
function extend (subClass, superClass, definition) {
if (Object.__proto__) {
definition.__proto__ = superClass.prototype
subClass.prototype = definition
} else {
const tmpClass = function () {}
tmpClass.prototype = superClass.prototype
subClass.prototype = new tmpClass()
subClass.prototype.constructor = superClass
for (const key in definition) {
if (definition.hasOwnProperty(key)) {
subClass.prototype[key] = definition[key]
}
}
}
}
function Filter (text) {
this.text = text
this.subscriptions = []
}
Filter.prototype = {
text: null,
subscriptions: null,
toString: function () {
return this.text
}
}
Filter.knownFilters = createDict()
Filter.elemhideRegExp = /^([^\/\*\|\@"!]*?)#(\@)?(?:([\w\-]+|\*)((?:\([\w\-]+(?:[$^*]?=[^\(\)"]*)?\))*)|#([^{}]+))$/
Filter.regexpRegExp = /^(@@)?\/.*\/(?:\$~?[\w\-]+(?:=[^,\s]+)?(?:,~?[\w\-]+(?:=[^,\s]+)?)*)?$/
Filter.optionsRegExp = /\$(~?[\w\-]+(?:=[^,\s]+)?(?:,~?[\w\-]+(?:=[^,\s]+)?)*)$/
Filter.fromText = function (text) {
if (text in Filter.knownFilters) {
return Filter.knownFilters[text]
}
let ret
if (text.charAt(0) === "!") {
ret = new CommentFilter(text)
} else {
ret = RegExpFilter.fromText(text)
}
Filter.knownFilters[ret.text] = ret
return ret
}
function InvalidFilter (text, reason) {
Filter.call(this, text)
this.reason = reason
}
extend(InvalidFilter, Filter, {
reason: null
})
function CommentFilter (text) {
Filter.call(this, text)
}
extend(CommentFilter, Filter, {})
function ActiveFilter (text, domains) {
Filter.call(this, text)
this.domainSource = domains
}
extend(ActiveFilter, Filter, {
domainSource: null,
domainSeparator: null,
ignoreTrailingDot: true,
domainSourceIsUpperCase: false,
getDomains: function () {
const prop = getOwnPropertyDescriptor(this, "domains")
if (prop) {
return prop
}
let domains = null
if (this.domainSource) {
let source = this.domainSource
if (!this.domainSourceIsUpperCase) {
source = source.toUpperCase()
}
const list = source.split(this.domainSeparator)
if (list.length === 1 && (list[0]).charAt(0) !== "~") {
domains = createDict()
domains[""] = false
if (this.ignoreTrailingDot) {
list[0] = list[0].replace(/\.+$/, "")
}
domains[list[0]] = true
} else {
let hasIncludes = false
for (let i = 0; i < list.length; i++) {
let domain = list[i]
if (this.ignoreTrailingDot) {
domain = domain.replace(/\.+$/, "")
}
if (domain === "") {
continue
}
let include
if (domain.charAt(0) === "~") {
include = false
domain = domain.substr(1)
} else {
include = true
hasIncludes = true
}
if (!domains) {
domains = createDict()
}
domains[domain] = include
}
domains[""] = !hasIncludes
}
this.domainSource = null
}
return this.domains
},
siteKeys: null,
isActiveOnDomain: function (docDomain, siteKey) {
if (this.getSiteKeys() && (!siteKey || this.getSiteKeys().indexOf(siteKey.toUpperCase()) < 0)) {
return false
}
if (!this.getDomains()) {
return true
}
if (!docDomain) {
return this.getDomains()[""]
}
if (this.ignoreTrailingDot) {
docDomain = docDomain.replace(/\.+$/, "")
}
docDomain = docDomain.toUpperCase()
while (true) {
if (docDomain in this.getDomains()) {
return this.domains[docDomain]
}
const nextDot = docDomain.indexOf(".")
if (nextDot < 0) {
break
}
docDomain = docDomain.substr(nextDot + 1)
}
return this.domains[""]
}/*,
isActiveOnlyOnDomain: function (docDomain) {
if (!docDomain || !this.getDomains() || this.getDomains()[""]) {
return false
}
if (this.ignoreTrailingDot) {
docDomain = docDomain.replace(/\.+$/, "")
}
docDomain = docDomain.toUpperCase()
for (const domain in this.getDomains()) {
if (this.domains[domain] && domain != docDomain && (domain.length <= docDomain.length || domain.indexOf("." + docDomain) != domain.length - docDomain.length - 1)) {
return false
}
}
return true
}*/
})
function RegExpFilter (text, regexpSource, contentType, matchCase, domains, thirdParty, siteKeys) {
ActiveFilter.call(this, text, domains, siteKeys)
if (contentType != null) {
this.contentType = contentType
}
if (matchCase) {
this.matchCase = matchCase
}
if (thirdParty != null) {
this.thirdParty = thirdParty
}
if (siteKeys != null) {
this.siteKeySource = siteKeys
}
if (regexpSource.length >= 2 && regexpSource.charAt(0) === "/" && regexpSource.charAt(regexpSource.length - 1) === "/") {
this.regexp = new RegExp(regexpSource.substr(1, regexpSource.length - 2), this.matchCase ? "" : "i")
} else {
this.regexpSource = regexpSource
}
}
extend(RegExpFilter, ActiveFilter, {
domainSourceIsUpperCase: true,
length: 1,
domainSeparator: "|",
regexpSource: null,
getRegexp: function () {
const prop = getOwnPropertyDescriptor(this, "regexp")
if (prop) {
return prop
}
const source = this.regexpSource.replace(/\*+/g, "*").replace(/\^\|$/, "^").replace(/\W/g, "\\$&").replace(/\\\*/g, ".*").replace(/\\\^/g, "(?:[\\x00-\\x24\\x26-\\x2C\\x2F\\x3A-\\x40\\x5B-\\x5E\\x60\\x7B-\\x7F]|$)").replace(/^\\\|\\\|/, "^[\\w\\-]+:\\/+(?!\\/)(?:[^\\/]+\\.)?").replace(/^\\\|/, "^").replace(/\\\|$/, "$").replace(/^(\.\*)/, "").replace(/(\.\*)$/, "")
const regexp = new RegExp(source, this.matchCase ? "" : "i")
this.regexp = regexp
return regexp
},
contentType: 2147483647,
matchCase: false,
thirdParty: null,
siteKeySource: null,
getSiteKeys: function () {
const prop = getOwnPropertyDescriptor(this, "siteKeys")
if (prop) {
return prop
}
let siteKeys = null
if (this.siteKeySource) {
siteKeys = this.siteKeySource.split("|")
this.siteKeySource = null
}
this.siteKeys = siteKeys
return this.siteKeys
},
matches: function (location, contentType, docDomain, thirdParty, siteKey) {
return !!(this.getRegexp().test(location) && this.isActiveOnDomain(docDomain, siteKey))
}
})
RegExpFilter.prototype["0"] = "#this"
RegExpFilter.fromText = function (text) {
let blocking = true
const origText = text
if (text.indexOf("@@") === 0) {
blocking = false
text = text.substr(2)
}
let contentType = null
let matchCase = null
let domains = null
let siteKeys = null
let thirdParty = null
let collapse = null
let options
const match = text.indexOf("$") >= 0 ? Filter.optionsRegExp.exec(text) : null
if (match) {
options = match[1].toUpperCase().split(",")
text = match.input.substr(0, match.index)
for (let _loopIndex6 = 0; _loopIndex6 < options.length; ++_loopIndex6) {
let option = options[_loopIndex6]
let value = null
const separatorIndex = option.indexOf("=")
if (separatorIndex >= 0) {
value = option.substr(separatorIndex + 1)
option = option.substr(0, separatorIndex)
}
option = option.replace(/-/, "_")
if (option in RegExpFilter.typeMap) {
if (contentType == null) {
contentType = 0
}
contentType |= RegExpFilter.typeMap[option]
} else if (option.charAt(0) === "~" && option.substr(1) in RegExpFilter.typeMap) {
if (contentType == null) {
contentType = RegExpFilter.prototype.contentType
}
contentType &= ~RegExpFilter.typeMap[option.substr(1)]
} else if (option === "MATCH_CASE") {
matchCase = true
} else if (option === "~MATCH_CASE") {
matchCase = false
} else if (option === "DOMAIN" && typeof value != "undefined") {
domains = value
} else if (option === "THIRD_PARTY") {
thirdParty = true
} else if (option === "~THIRD_PARTY") {
thirdParty = false
} else if (option === "COLLAPSE") {
collapse = true
} else if (option === "~COLLAPSE") {
collapse = false
} else if (option === "SITEKEY" && typeof value != "undefined") {
siteKeys = value
} else {
return new InvalidFilter(origText, "Unknown option " + option.toLowerCase())
}
}
}
if (!blocking && (contentType == null || contentType & RegExpFilter.typeMap.DOCUMENT) && (!options || options.indexOf("DOCUMENT") < 0) && !/^\|?[\w\-]+:/.test(text)) {
if (contentType == null) {
contentType = RegExpFilter.prototype.contentType
}
contentType &= ~RegExpFilter.typeMap.DOCUMENT
}
try {
if (blocking) {
return new BlockingFilter(origText, text, contentType, matchCase, domains, thirdParty, siteKeys, collapse)
} else {
return new WhitelistFilter(origText, text, contentType, matchCase, domains, thirdParty, siteKeys)
}
} catch (e) {
return new InvalidFilter(origText, e)
}
}
RegExpFilter.typeMap = {
OTHER: 1,
SCRIPT: 2,
IMAGE: 4,
STYLESHEET: 8,
OBJECT: 16,
SUBDOCUMENT: 32,
DOCUMENT: 64,
XBL: 1,
PING: 1,
XMLHTTPREQUEST: 2048,
OBJECT_SUBREQUEST: 4096,
DTD: 1,
MEDIA: 16384,
FONT: 32768,
BACKGROUND: 4,
POPUP: 268435456,
ELEMHIDE: 1073741824
}
RegExpFilter.prototype.contentType &= ~(RegExpFilter.typeMap.ELEMHIDE | RegExpFilter.typeMap.POPUP)
function BlockingFilter (text, regexpSource, contentType, matchCase, domains, thirdParty, siteKeys, collapse) {
RegExpFilter.call(this, text, regexpSource, contentType, matchCase, domains, thirdParty, siteKeys)
this.collapse = collapse
}
extend(BlockingFilter, RegExpFilter, {
collapse: null
})
function WhitelistFilter (text, regexpSource, contentType, matchCase, domains, thirdParty, siteKeys) {
RegExpFilter.call(this, text, regexpSource, contentType, matchCase, domains, thirdParty, siteKeys)
}
extend(WhitelistFilter, RegExpFilter, {})
function Matcher () {
this.clear()
}
Matcher.prototype = {
filterByKeyword: null,
keywordByFilter: null,
clear: function () {
this.filterByKeyword = createDict()
this.keywordByFilter = createDict()
},
add: function (filter) {
if (filter.text in this.keywordByFilter) {
return
}
const keyword = this.findKeyword(filter)
const oldEntry = this.filterByKeyword[keyword]
if (typeof oldEntry == "undefined") {
this.filterByKeyword[keyword] = filter
} else if (oldEntry.length === 1) {
this.filterByKeyword[keyword] = [oldEntry, filter]
} else {
oldEntry.push(filter)
}
this.keywordByFilter[filter.text] = keyword
},
remove: function (filter) {
if (!(filter.text in this.keywordByFilter)) {
return
}
const keyword = this.keywordByFilter[filter.text]
const list = this.filterByKeyword[keyword]
if (list.length <= 1) {
delete this.filterByKeyword[keyword]
} else {
const index = list.indexOf(filter)
if (index >= 0) {
list.splice(index, 1)
if (list.length === 1) {
this.filterByKeyword[keyword] = list[0]
}
}
}
delete this.keywordByFilter[filter.text]
},
findKeyword: function (filter) {
let result = ""
let text = filter.text
if (Filter.regexpRegExp.test(text)) {
return result
}
const match = Filter.optionsRegExp.exec(text)
if (match) {
text = match.input.substr(0, match.index)
}
if (text.substr(0, 2) === "@@") {
text = text.substr(2)
}
const candidates = text.toLowerCase().match(/[^a-z0-9%*][a-z0-9%]{3,}(?=[^a-z0-9%*])/g)
if (!candidates) {
return result
}
const hash = this.filterByKeyword
let resultCount = 16777215
let resultLength = 0
for (let i = 0, l = candidates.length; i < l; i++) {
const candidate = candidates[i].substr(1)
const count = candidate in hash ? hash[candidate].length : 0
if (count < resultCount || count === resultCount && candidate.length > resultLength) {
result = candidate
resultCount = count
resultLength = candidate.length
}
}
return result
},
hasFilter: function (filter) {
return filter.text in this.keywordByFilter
},
getKeywordForFilter: function (filter) {
if (filter.text in this.keywordByFilter) {
return this.keywordByFilter[filter.text]
} else {
return null
}
},
_checkEntryMatch: function (keyword, location, contentType, docDomain, thirdParty, siteKey) {
const list = this.filterByKeyword[keyword]
for (let i = 0; i < list.length; i++) {
let filter = list[i]
if (filter === "#this") {
filter = list
}
if (filter.matches(location, contentType, docDomain, thirdParty, siteKey)) {
return filter
}
}
return null
}/*,
matchesAny: function (location, contentType, docDomain, thirdParty, siteKey) {
let candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g)
if (candidates === null) {
candidates = []
}
candidates.push("")
for (let i = 0, l = candidates.length; i < l; i++) {
const substr = candidates[i]
if (substr in this.filterByKeyword) {
const result = this._checkEntryMatch(substr, location, contentType, docDomain, thirdParty, siteKey)
if (result) {
return result
}
}
}
return null
}*/
}
function CombinedMatcher () {
this.blacklist = new Matcher()
this.whitelist = new Matcher()
this.resultCache = createDict()
}
CombinedMatcher.maxCacheEntries = 1000
CombinedMatcher.prototype = {
blacklist: null,
whitelist: null,
resultCache: null,
cacheEntries: 0,
clear: function () {
this.blacklist.clear()
this.whitelist.clear()
this.resultCache = createDict()
this.cacheEntries = 0
},
add: function (filter) {
if (filter instanceof WhitelistFilter) {
this.whitelist.add(filter)
} else {
this.blacklist.add(filter)
}
if (this.cacheEntries > 0) {
this.resultCache = createDict()
this.cacheEntries = 0
}
},
remove: function (filter) {
if (filter instanceof WhitelistFilter) {
this.whitelist.remove(filter)
} else {
this.blacklist.remove(filter)
}
if (this.cacheEntries > 0) {
this.resultCache = createDict()
this.cacheEntries = 0
}
},
findKeyword: function (filter) {
if (filter instanceof WhitelistFilter) {
return this.whitelist.findKeyword(filter)
} else {
return this.blacklist.findKeyword(filter)
}
},
hasFilter: function (filter) {
if (filter instanceof WhitelistFilter) {
return this.whitelist.hasFilter(filter)
} else {
return this.blacklist.hasFilter(filter)
}
},
getKeywordForFilter: function (filter) {
if (filter instanceof WhitelistFilter) {
return this.whitelist.getKeywordForFilter(filter)
} else {
return this.blacklist.getKeywordForFilter(filter)
}
},
/*isSlowFilter: function (filter) {
const matcher = filter instanceof WhitelistFilter ? this.whitelist : this.blacklist
if (matcher.hasFilter(filter)) {
return !matcher.getKeywordForFilter(filter)
} else {
return !matcher.findKeyword(filter)
}
},*/
matchesAnyInternal: function (location, contentType, docDomain, thirdParty, siteKey) {
let candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g)
if (candidates === null) {
candidates = []
}
candidates.push("")
let blacklistHit = null
for (let i = 0, l = candidates.length; i < l; i++) {
const substr = candidates[i]
if (substr in this.whitelist.filterByKeyword) {
const result = this.whitelist._checkEntryMatch(substr, location, contentType, docDomain, thirdParty, siteKey)
if (result) {
return result
}
}
if (substr in this.blacklist.filterByKeyword && blacklistHit === null) {
blacklistHit = this.blacklist._checkEntryMatch(substr, location, contentType, docDomain, thirdParty, siteKey)
}
}
return blacklistHit
},
matchesAny: function (location, docDomain) {
const key = location + " " + docDomain + " "
if (key in this.resultCache) {
return this.resultCache[key]
}
const result = this.matchesAnyInternal(location, 0, docDomain, null, null)
if (this.cacheEntries >= CombinedMatcher.maxCacheEntries) {
this.resultCache = createDict()
this.cacheEntries = 0
}
this.resultCache[key] = result
this.cacheEntries++
return result
}
}
const userrulesMatcher = new CombinedMatcher()
const defaultMatcher = new CombinedMatcher()
const direct = 'DIRECT;'
for (let i = 0; i < rules.length; i++) {
defaultMatcher.add(Filter.fromText(rules[i]))
}
function FindProxyForURL (url, host) {
let matchedResult = userrulesMatcher.matchesAny(url, host)
if (matchedResult instanceof BlockingFilter) {
return proxy
} else if (matchedResult instanceof WhitelistFilter) {
return direct
}
// Hack for Geosite, it provides a whitelist...
matchedResult = defaultMatcher.matchesAny(url, host)
if (matchedResult instanceof BlockingFilter) {
return proxy
} else if (matchedResult instanceof WhitelistFilter) {
return direct
}
return direct
}
return {
FindProxyForURL,
proxyUrl: __PROXY__
}
}
module.exports = {
createPacClient
}