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 , * 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 . */ 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 }