PAC网址 - 可动态修改代理地址实现

181 阅读3分钟

实现思路

访问PAC网址时将代理信息携带在query参数中,响应时通过nginx修改响应内容

nginx 修改响应内容相关文档:

使用示例

使用示例1:

http://pac.qq.emsfly.cn/pac.SwitchyOmega.txt?proxy=PROXY&host=192.168.1.109&port=808

响应内容:

...
var proxy = 'PROXY 192.168.1.109:808;';
...

使用示例2:

http://pac.qq.emsfly.cn/pac.SwitchyOmega.txt?proxy=SOCKS5&host=192.168.1.109&port=1080

响应内容:

...
var proxy = 'SOCKS5 192.168.1.109:1080;';
...

nginx 配置

nginx.conf

server {
    listen 80; 
    server_name pac.qq.emsfly.cn;
    
    location / {
        sub_filter_types *;
        sub_filter "__PROXY__" "'$arg_proxy $arg_host:$arg_port;'";
        sub_filter_once on;
        root "/workspace/nginx/pac/root";
        # try_files $uri $uri/ /index.html;
    }
    # 增加类型后浏览器不能预览文本内容
    # add_header Content-Type application/x-ns-proxy-autoconfig;
}

或者,请求格式不正确时返回401状态码

server {
    listen 80; 
    server_name pac.qq.emsfly.cn;
    
    # root指令需在 error_page上一级
    root "/workspace/nginx/pac/root";
    location /pacplus.txt {
        error_page 401 /401.html;
        set $condition "$uri $arg_proxy $arg_host $arg_port";
        if ($condition !~ "/pacplus.txt .+ .+ .+"){
            return 401;
        }
        
        sub_filter_types *;
        sub_filter "__PROXY__" "'$arg_proxy $arg_host:$arg_port;'";
        sub_filter_once on;
        # try_files $uri $uri/ /index.html;
    }
    # 增加类型后浏览器不能预览文本内容
    # add_header Content-Type application/x-ns-proxy-autoconfig;
}

401.html

<!DOCTYPE html>
<html>
<head>
<title>Error</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>The request path format is incorrect.</h1>
<p>Correct format: http://www.example.com/pacplus.txt?proxy=$proxy&host=$host&port=$port.</p>
<p>HTTP eg: http://www.example.com/pacplus.txt?proxy=HTTP&host=127.0.0.1&port=808</p>
<p>HTTPS eg: http://www.example.com/pacplus.txt?proxy=HTTPS&host=127.0.0.1&port=808</p>
<p>SOCKS4 eg: http://www.example.com/pacplus.txt?proxy=SOCKS4&host=127.0.0.1&port=1080</p>
<p>SOCKS5 eg: http://www.example.com/pacplus.txt?proxy=SOCKS5&host=127.0.0.1&port=1080</p>
</body>
</html>

PAC 脚本

pac.txt

var __USERRULES__ = [
  "@@@||bxe.comp.360os.com^"
];
var __RULES__ = [
  "/.*/",
];
/* 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.

var proxy = __PROXY__;
var userrules = [];
var rules = [];

// convert to abp grammar
var re = /^(@@)?(@@@)?\|\|.*?[^\^]$/;
for (var i = 0; i < __RULES__.length; i++) {
    var s = __RULES__[i];
    if (s.match(re)) s += "^";
    rules.push(s);
}

for (var i = 0; i < __USERRULES__.length; i++) {
    var s = __USERRULES__[i];
    if (s.match(re))  s += "^";
    userrules.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()
{
    var 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
    {
        var tmpclass = function(){}, ret;
        tmpclass.prototype = superclass.prototype;
        subclass.prototype = new tmpclass();
        subclass.prototype.constructor = superclass;
        for (var i in definition)
        {
            if (definition.hasOwnProperty(i))
            {
                subclass.prototype[i] = definition[i];
            }
        }
    }
}

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];
    }
    var 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()
    {
        var prop = getOwnPropertyDescriptor(this, "domains");
        if (prop)
        {
            return prop;
        }
        var domains = null;
        if (this.domainSource)
        {
            var source = this.domainSource;
            if (!this.domainSourceIsUpperCase)
            {
                source = source.toUpperCase();
            }
            var 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
            {
                var hasIncludes = false;
                for (var i = 0; i < list.length; i++)
                {
                    var domain = list[i];
                    if (this.ignoreTrailingDot)
                    {
                        domain = domain.replace(/\.+$/, "");
                    }
                    if (domain == "")
                    {
                        continue;
                    }
                    var 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];
            }
            var 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 (var 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) == "/")
    {
        var regexp = new RegExp(regexpSource.substr(1, regexpSource.length - 2), this.matchCase ? "" : "i");
        this.regexp = regexp;
    }
    else
    {
        this.regexpSource = regexpSource;
    }
}
extend(RegExpFilter, ActiveFilter, {
    domainSourceIsUpperCase: true,
    length: 1,
    domainSeparator: "|",
    regexpSource: null,
    getRegexp: function()
    {
        var prop = getOwnPropertyDescriptor(this, "regexp");
        if (prop)
        {
            return prop;
        }
        var 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(/(\.\*)$/, "");
        var regexp = new RegExp(source, this.matchCase ? "" : "i");
        this.regexp = regexp;
        return regexp;
    },
    contentType: 2147483647,
    matchCase: false,
    thirdParty: null,
    sitekeySource: null,
    getSitekeys: function()
    {
        var prop = getOwnPropertyDescriptor(this, "sitekeys");
        if (prop)
        {
            return prop;
        }
        var 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)
    {
        if (this.getRegexp().test(location) && this.isActiveOnDomain(docDomain, sitekey))
        {
            return true;
        }
        return false;
    }
});
RegExpFilter.prototype["0"] = "#this";
RegExpFilter.fromText = function(text)
{
    var blocking = true;
    var proxied = false;
    var origText = text;
    if (text.indexOf("@@@") == 0) {
        proxied = true;
        text = text.substr(3);
    }
    else if (text.indexOf("@@") == 0)
    {
        blocking = false;
        text = text.substr(2);
    }
    var contentType = null;
    var matchCase = null;
    var domains = null;
    var sitekeys = null;
    var thirdParty = null;
    var collapse = null;
    var options;
    var match = text.indexOf("$") >= 0 ? Filter.optionsRegExp.exec(text) : null;
    if (match)
    {
        options = match[1].toUpperCase().split(",");
        text = match.input.substr(0, match.index);
        for (var _loopIndex6 = 0; _loopIndex6 < options.length; ++_loopIndex6)
        {
            var option = options[_loopIndex6];
            var value = null;
            var 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 (proxied) {
            return new ProxiedFilter(origText, text, contentType, matchCase, domains, thirdParty, sitekeys, collapse);
        }
        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 ProxiedFilter(text, regexpSource, contentType, matchCase, domains, thirdParty, sitekeys)
{
    RegExpFilter.call(this, text, regexpSource, contentType, matchCase, domains, thirdParty, sitekeys);
}
extend(ProxiedFilter, 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;
        }
        var keyword = this.findKeyword(filter);
        var 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;
        }
        var keyword = this.keywordByFilter[filter.text];
        var list = this.filterByKeyword[keyword];
        if (list.length <= 1)
        {
            delete this.filterByKeyword[keyword];
        }
        else
        {
            var 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)
    {
        var result = "";
        var text = filter.text;
        if (Filter.regexpRegExp.test(text))
        {
            return result;
        }
        var match = Filter.optionsRegExp.exec(text);
        if (match)
        {
            text = match.input.substr(0, match.index);
        }
        if (text.substr(0, 2) == "@@")
        {
            text = text.substr(2);
        }
        var candidates = text.toLowerCase().match(/[^a-z0-9%*][a-z0-9%]{3,}(?=[^a-z0-9%*])/g);
        if (!candidates)
        {
            return result;
        }
        var hash = this.filterByKeyword;
        var resultCount = 16777215;
        var resultLength = 0;
        for (var i = 0, l = candidates.length; i < l; i++)
        {
            var candidate = candidates[i].substr(1);
            var 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)
    {
        var list = this.filterByKeyword[keyword];
        for (var i = 0; i < list.length; i++)
        {
            var 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)
    {
        var candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g);
        if (candidates === null)
        {
            candidates = [];
        }
        candidates.push("");
        for (var i = 0, l = candidates.length; i < l; i++)
        {
            var substr = candidates[i];
            if (substr in this.filterByKeyword)
            {
                var 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)
    {
        var 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)
    {
        var candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g);
        if (candidates === null)
        {
            candidates = [];
        }
        candidates.push("");
        var blacklistHit = null;
        for (var i = 0, l = candidates.length; i < l; i++)
        {
            var substr = candidates[i];
            if (substr in this.whitelist.filterByKeyword)
            {
                var 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)
    {
        var key = location + " " + docDomain + " ";
        if (key in this.resultCache)
        {
            return this.resultCache[key];
        }
        var 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;
    }
};

var userrulesMatcher = new CombinedMatcher();
var defaultMatcher = new CombinedMatcher();

var direct = 'DIRECT;';

for (var i = 0; i < userrules.length; i++) {
    userrulesMatcher.add(Filter.fromText(userrules[i]));
}

for (var i = 0; i < rules.length; i++) {
    defaultMatcher.add(Filter.fromText(rules[i]));
}

// PAC has no v6 support, it sucks
var ip4Re = /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/

var privateNet = [
    ["10.0.0.0", "255.0.0.0"],
    ["127.0.0.0", "255.0.0.0"],
    ["172.16.0.0", "255.240.0.0"],
    ["192.168.0.0", "255.255.0.0"],
]

function FindProxyForURL(url, host) {
    if (host.match(ip4Re)) {
        for (var i = 0; i < privateNet.length; i++) {
            if (isInNet(host, privateNet[i][0], privateNet[i][1])) return direct;
        }
    }
    var proxied = "PROXY 127.0.0.1:27001;";
    var userrulesMatchResult = userrulesMatcher.matchesAny(url, host);
    if (userrulesMatchResult instanceof ProxiedFilter) {
        return proxied;
    }
    if (userrulesMatchResult instanceof BlockingFilter) {
        return proxy;
    }
    if (userrulesMatchResult instanceof WhitelistFilter) {
        return direct;
    }
    // Hack for Geosite, it provides a whitelist...
    var defaultMatchResult = defaultMatcher.matchesAny(url, host);
    if (defaultMatchResult instanceof ProxiedFilter) {
        return proxied;
    }
    if (defaultMatchResult instanceof WhitelistFilter) {
        return direct;
    }
    if (defaultMatchResult instanceof BlockingFilter) {
        return proxy;
    }
    return direct;
}