Last active
January 6, 2017 22:58
-
-
Save ayanamist/9566100 to your computer and use it in GitHub Desktop.
Proxy Pac Server
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"local": "127.0.0.1:8124", | |
"remote": [ | |
{ | |
"proxy": "direct", | |
"rules": [ | |
["10.0.0.0", "255.0.0.0"], | |
["100.64.0.0", "255.192.0.0"], | |
["127.0.0.0", "255.0.0.0"], | |
["172.16.0.0", "255.240.0.0"], | |
["192.0.0.0", "255.255.255.0"], | |
["192.168.0.0", "255.255.0.0"], | |
["198.18.0.0", "255.254.0.0"] | |
] | |
}, | |
{ | |
"proxy": "direct", | |
"rules": [ | |
"abc.com", | |
"xyz.com" | |
] | |
}, | |
{ | |
"proxy": "https://user:pass@proxy:port", | |
"rules": [ | |
"*" | |
] | |
} | |
] | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env node | |
var fs = require('fs'); | |
var http = require('http'); | |
var https = require('https'); | |
var net = require('net'); | |
var path = require('path'); | |
var url = require('url'); | |
var util = require('util'); | |
var PAC_ROUTE = "/pac"; | |
var globalProxyConfigs = []; | |
var globalProxyServer; | |
var log = function () { | |
util.log(util.format.apply(util, arguments)); | |
}; | |
function dnsDomainIs (host, pattern) { | |
return host.length >= pattern.length && | |
(host === pattern || host.substring(host.length - pattern.length - 1) === '.' + pattern); | |
} | |
function convertAddr (ipchars) { | |
var bytes = ipchars.split('.'); | |
return ((bytes[0] & 0xff) << 24) | | |
((bytes[1] & 0xff) << 16) | | |
((bytes[2] & 0xff) << 8) | | |
(bytes[3] & 0xff); | |
} | |
function isInNet (ipaddr, pattern, maskstr) { | |
var test = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(ipaddr); | |
if (test == null) { | |
return false; | |
} else if (test[1] > 255 || test[2] > 255 || | |
test[3] > 255 || test[4] > 255) { | |
return false; | |
} | |
var host = convertAddr(ipaddr); | |
var pat = convertAddr(pattern); | |
var mask = convertAddr(maskstr); | |
return ((host & mask) == (pat & mask)); | |
} | |
var determineRoute = function (requestUrl) { | |
var urlParsed = url.parse(requestUrl); | |
var urlHost = urlParsed.slashes ? urlParsed.hostname : requestUrl.split(":")[0]; | |
var matchedConfig = null; | |
globalProxyConfigs.some(function (proxyConfig) { | |
return proxyConfig.rules.some(function (rule) { | |
var result = false; | |
if (typeof rule === "string") { | |
result = rule === "*" || dnsDomainIs(urlHost, rule); | |
} else { | |
if (net.isIPv4(urlHost)) { | |
result = isInNet(urlHost, rule[0], rule[1]); | |
} | |
} | |
if (result) { | |
matchedConfig = proxyConfig; | |
} | |
return result; | |
}); | |
}); | |
return matchedConfig; | |
}; | |
var newProxyRequest = function (proxyConfig, request) { | |
if (proxyConfig.auth) { | |
request.headers['Proxy-Authorization'] = 'Basic ' + proxyConfig.auth; | |
} | |
var requestOptions = { | |
hostname: proxyConfig.host, | |
port: proxyConfig.port, | |
path: request.url, | |
method: request.method, | |
headers: request.headers, | |
agent: proxyConfig.agent, | |
}; | |
return proxyConfig.protocol.request(requestOptions); | |
}; | |
//noinspection JSUnusedLocalSymbols | |
function FindProxyForURL (url, host) { | |
host = host.split(":")[0]; | |
var shouldDirect = DIRECT_RULES.some(function (rule) { | |
if (typeof rule === "string") { | |
return dnsDomainIs(host, rule); | |
} else { | |
return isInNet(host, rule[0], rule[1]); | |
} | |
}); | |
if (!shouldDirect) { | |
var shouldProxy = PROXY_RULES.some(function (rule) { | |
return rule === "*" || dnsDomainIs(host, rule); | |
}); | |
if (shouldProxy) { | |
return PROXY; | |
} | |
} | |
return "direct"; | |
} | |
var buildPac = function () { | |
var address = globalProxyServer.address(); | |
return [ | |
util.format("var PROXY = 'PROXY 127.0.0.1:%s';", address.port), | |
util.format("var DIRECT_RULES = %s;", JSON.stringify(globalProxyConfigs.reduce(function (acc, config) { | |
if (config.direct) { | |
Array.prototype.push.apply(acc, config.rules); | |
} | |
return acc; | |
}, []))), | |
util.format("var PROXY_RULES = %s;", JSON.stringify(globalProxyConfigs.reduce(function (acc, config) { | |
if (!config.direct) { | |
Array.prototype.push.apply(acc, config.rules); | |
} | |
return acc; | |
}, []))), | |
convertAddr.toString(), | |
isInNet.toString(), | |
dnsDomainIs.toString(), | |
FindProxyForURL.toString() | |
].join("\n"); | |
}; | |
var newProxyServer = function () { | |
var proxyServer = http.createServer(); | |
proxyServer.on('request', function (cltRequest, cltResponse) { | |
if (cltRequest.url.split("?")[0] === PAC_ROUTE) { | |
log("visit %s", cltRequest.url); | |
cltResponse.setHeader("Content-Type", "text/plain; charset=UTF-8"); | |
cltResponse.setHeader("Expires", "0"); | |
cltResponse.setHeader("Cache-Control", "private, no-cache, no-store, must-revalidate, max-age=0"); | |
cltResponse.end(buildPac()); | |
return; | |
} | |
var srvRequest; | |
var matchedConfig = determineRoute(cltRequest.url); | |
var isDirect = !matchedConfig || matchedConfig.direct; | |
log('%s %s HTTP/%s via %s', cltRequest.method, cltRequest.url, cltRequest.httpVersion, !isDirect ? matchedConfig.host : "direct"); | |
if (matchedConfig && !matchedConfig.direct) { | |
srvRequest = newProxyRequest(matchedConfig, cltRequest); | |
} else { | |
var urlParsed = url.parse(cltRequest.url); | |
// nodejs will make all names of http headers lower case, which breaks many old clients. | |
// Should not directly manipulate socket, because cltResponse.socket will sometimes become null. | |
var rawHeader = {}; | |
cltRequest.allHeaders.map(function (header) { | |
// We don't need to validate split result, since nodejs has guaranteed by valid srvResponse.headers. | |
var key = header.split(':')[0].trim(); | |
rawHeader[key] = cltRequest.headers[key.toLowerCase()]; | |
}); | |
srvRequest = http.request({ | |
hostname: urlParsed.hostname, | |
port: urlParsed.port, | |
path: urlParsed.path, | |
method: cltRequest.method, | |
headers: rawHeader | |
}); | |
} | |
srvRequest.on("error", function (err) { | |
srvRequest.abort(); | |
cltResponse.writeHead && cltResponse.writeHead(504); | |
cltResponse.end('504 Bad Gateway: ' + String(err.valueOf())); | |
log('%s %s HTTP/%s via %s error %s', cltRequest.method, cltRequest.url, cltRequest.httpVersion, !isDirect ? matchedConfig.host : "direct", String(err.valueOf())); | |
}); | |
srvRequest.on('response', function (srvResponse) { | |
srvResponse.on('close', function () { | |
cltResponse.end(); | |
}); | |
cltResponse.on('error', function (err) { | |
log('cltResponse %s: %s', err.valueOf(), cltRequest.url); | |
cltResponse.abort(); | |
srvResponse.abort(); | |
}); | |
srvResponse.on('error', function (err) { | |
log('srvResponse %s: %s', err.valueOf(), cltRequest.url); | |
srvResponse.abort(); | |
cltResponse.abort(); | |
}); | |
// nodejs will make all names of http headers lower case, which breaks many old clients. | |
// Should not directly manipulate socket, because cltResponse.socket will sometimes become null. | |
var rawHeader = {}; | |
srvResponse.allHeaders.map(function (header) { | |
// We don't need to validate split result, since nodejs has guaranteed by valid srvResponse.headers. | |
var key = header.split(':')[0].trim(); | |
rawHeader[key] = srvResponse.headers[key.toLowerCase()]; | |
}); | |
cltResponse.writeHead(srvResponse.statusCode, rawHeader); | |
srvResponse.pipe(cltResponse); | |
}); | |
cltRequest.pipe(srvRequest); | |
cltResponse.on('close', function () { | |
srvRequest.abort(); | |
}); | |
}); | |
proxyServer.on('connect', function (cltRequest, cltSocket) { | |
cltSocket.setNoDelay(true); | |
var connectedListener = function (srvResponse, srvSocket) { | |
srvSocket.setNoDelay(true); | |
srvSocket.on('close', function () { | |
cltSocket.end(); | |
}); | |
cltSocket.on('error', function (err) { | |
log('cltSocket %s: %s', err.valueOf(), cltRequest.url); | |
cltSocket.end(); | |
srvSocket.end(); | |
}); | |
srvSocket.on('error', function (err) { | |
log('srvSocket %s: %s', err.valueOf(), cltRequest.url); | |
srvSocket.end(); | |
cltSocket.end(); | |
}); | |
cltSocket.write(util.format("HTTP/1.1 %d %s\r\n\r\n", srvResponse.statusCode, http.STATUS_CODES[srvResponse.statusCode])); | |
srvSocket.pipe(cltSocket); | |
cltSocket.pipe(srvSocket); | |
}; | |
var matchedConfig = determineRoute(cltRequest.url); | |
var isDirect = !matchedConfig || matchedConfig.direct; | |
log('%s %s HTTP/%s via %s', cltRequest.method, cltRequest.url, cltRequest.httpVersion, !isDirect ? matchedConfig.host : "direct"); | |
if (!isDirect) { | |
var srvRequest = newProxyRequest(matchedConfig, cltRequest); | |
srvRequest.end(); | |
srvRequest.on("error", function (err) { | |
log('SocketError %s: %s', cltRequest.url, err.valueOf()); | |
srvRequest.abort(); | |
cltSocket.end("HTTP/1.1 504 Bad Gateway\r\n504 Bad Gateway: " + String(err.valueOf())); | |
}); | |
srvRequest.on('connect', connectedListener); | |
cltSocket.on('close', function () { | |
srvRequest.abort(); | |
}); | |
} else { | |
var splitted = cltRequest.url.split(":"); | |
var connected = false; | |
var srvSocket = net.createConnection(Number(splitted[1]), splitted[0], function () { | |
connected = true; | |
connectedListener({"statusCode": 200}, srvSocket); | |
}); | |
srvSocket.on("error", function (err) { | |
log('SocketError %s: %s', cltRequest.url, err.valueOf()); | |
if (!connected) { | |
cltSocket.end("HTTP/1.1 504 Bad Gateway\r\n\r\n" + err.valueOf()); | |
} else { | |
cltSocket.destroy(); | |
srvSocket.destroy(); | |
} | |
}); | |
} | |
}); | |
proxyServer.on('error', function (err) { | |
log('ServerError: %s', err.valueOf()); | |
throw err; | |
}); | |
return proxyServer; | |
}; | |
var patchHttp = function (http) { | |
var IMPrototype = http.IncomingMessage.prototype, | |
_addHeaderLine = IMPrototype._addHeaderLine; | |
//Patch ServerRequest to save unmodified copy of headers | |
IMPrototype._addHeaderLine = function (field, value) { | |
var list = this.complete ? | |
(this.allTrailers || (this.allTrailers = [])) : | |
(this.allHeaders || (this.allHeaders = [])); | |
list.push(field + ': ' + value); | |
_addHeaderLine.apply(this, arguments); | |
}; | |
// Patch createSocket to avoid the last argument `req` pollutes servername | |
// We are connecting a proxy, so host header in req is useless when creating socket. | |
var APrototype = http.Agent.prototype; | |
var _createSocket = APrototype.createSocket; | |
APrototype.createSocket = function () { | |
return _createSocket.apply(this, Array.prototype.slice.call(arguments, 0, 4)); | |
}; | |
// Keep raw header when sending request | |
http.OutgoingMessage.prototype.setHeader = function(name, value) { | |
if (arguments.length < 2) { | |
throw new Error('`name` and `value` are required for setHeader().'); | |
} | |
if (this._header) { | |
throw new Error('Can\'t set headers after they are sent.'); | |
} | |
var key = name;//.toLowerCase(); | |
this._headers = this._headers || {}; | |
this._headerNames = this._headerNames || {}; | |
this._headers[key] = value; | |
this._headerNames[key] = name; | |
}; | |
}; | |
var initProxy = function (config) { | |
globalProxyConfigs = config["remote"].map(function (configRemoteRule) { | |
var config = { | |
"direct": true, | |
"agent": null, | |
"protocol": null, | |
"host": null, | |
"port": null, | |
"auth": null, | |
"rules": configRemoteRule["rules"], | |
}; | |
var proxyStr = configRemoteRule["proxy"]; | |
if (proxyStr !== "direct") { | |
config.direct = false; | |
var parsed = url.parse(proxyStr); | |
if (parsed.auth === null) { | |
config.auth = null; | |
} else { | |
config.auth = (new Buffer(parsed.auth)).toString('base64'); | |
} | |
config.host = parsed.hostname; | |
if (parsed.protocol === "http:") { | |
config.agent = new http.Agent({ | |
"maxSockets": Infinity | |
}); | |
config.port = parsed.port || 80; | |
config.protocol = http; | |
} else if (parsed.protocol === "https:") { | |
config.agent = new https.Agent({ | |
"rejectUnauthorized": false, | |
"maxSockets": Infinity | |
}); | |
config.port = parsed.port || 443; | |
config.protocol = https; | |
} else { | |
throw new Error("Unsupported scheme: " + proxyStr); | |
} | |
} | |
return config; | |
}); | |
var localConfig = config["local"]; | |
var localConfigSplitted = localConfig.split(":"); | |
log('proxy started on %s', localConfig); | |
log('pac file served on http://%s%s', localConfig, PAC_ROUTE); | |
var proxyServer = newProxyServer(); | |
proxyServer.listen(Number(localConfigSplitted[1]), localConfigSplitted[0]); | |
return proxyServer; | |
}; | |
if (!module.parent) { | |
patchHttp(http); | |
http.Agent.defaultMaxSockets = Infinity; | |
http.globalAgent.maxSockets = Infinity; | |
https.globalAgent.maxSockets = Infinity; | |
process.on("uncaughtException", function (err) { | |
log("Uncaught: %s", err.stack); | |
}); | |
var filename = process.argv[2] || path.join(__dirname, 'config.json'); | |
var config = JSON.parse(fs.readFileSync(filename)); | |
globalProxyServer = initProxy(config); | |
var reloadTimer; | |
fs.watch(filename, function (event, filename) { | |
if (event !== "change") { | |
return; | |
} | |
clearTimeout(reloadTimer); | |
reloadTimer = setTimeout(function () { | |
if (globalProxyServer === null) { | |
return; | |
} | |
try { | |
config = JSON.parse(fs.readFileSync(filename)); | |
} catch (e) { | |
log("parse config file %s error, ignore", filename); | |
return; | |
} | |
log("file %s changed, reload proxy server", filename); | |
globalProxyServer.close(); | |
globalProxyServer = null; | |
setImmediate(function () { | |
globalProxyServer = initProxy(config); | |
}); | |
}, 1000); | |
}); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#NoEnv | |
#SingleInstance | |
#Persistent | |
Running = 1 | |
Shown = 0 | |
SetWorkingDir %A_ScriptDir% ; Ensures a consistent starting directory. | |
DetectHiddenWindows, On | |
Menu, Tray, Icon, proxy.ico | |
Menu, Tray, Tip, SSL Proxy | |
Menu, Tray, NoStandard | |
Menu, Tray, Add, &Show, ShowWindow | |
Menu, Tray, Add, &Hide, HideWindow | |
Menu, Tray, Add | |
Menu, Tray, Add, E&xit, CloseWindow | |
OnExit, CloseWindow | |
while (Running > 0) | |
{ | |
if (Shown > 0) | |
{ | |
Run, node.exe proxy.js,,UseErrorLevel, procPid | |
Gosub MenusShow | |
} | |
else | |
{ | |
Run, node.exe proxy.js,,Hide UseErrorLevel, procPid | |
Gosub MenusHide | |
} | |
WinWait, ahk_pid %procPid% ahk_class ConsoleWindowClass | |
WinGet activeWindow, ID, ahk_pid %procPid% ahk_class ConsoleWindowClass | |
Process, WaitClose, %procPid% | |
} | |
return | |
ShowWindow: | |
WinShow, ahk_id %activeWindow% | |
WinActivate, ahk_id %activeWindow% | |
Shown = 1 | |
Gosub MenusShow | |
return | |
MenusShow: | |
Menu, Tray, Disable, &Show | |
Menu, Tray, Enable, &Hide | |
Menu, Tray, Default, &Hide | |
return | |
HideWindow: | |
Shown = 0 | |
WinHide, ahk_id %activeWindow% | |
Gosub MenusHide | |
return | |
MenusHide: | |
Menu, Tray, Disable, &Hide | |
Menu, Tray, Enable, &Show | |
Menu, Tray, Default, &Show | |
return | |
CloseWindow: | |
Running = 0 | |
Process, Close, %procPid% | |
ExitApp, 0 | |
return |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
可以把ahk文件编译成exe