Skip to content

Instantly share code, notes, and snippets.

@ayanamist
Last active January 6, 2017 22:58
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ayanamist/9566100 to your computer and use it in GitHub Desktop.
Save ayanamist/9566100 to your computer and use it in GitHub Desktop.
Proxy Pac Server
{
"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": [
"*"
]
}
]
}
#!/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);
});
}
#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
@ayanamist
Copy link
Author

可以把ahk文件编译成exe

@ayanamist
Copy link
Author

如果需要额外配置TLS以防止MITM,config.json支持https.request中的tls参数,其中几个文件的地方请使用文件地址。

@haohaolee
Copy link

今天用了一下 真不错 比起 stunnel 清爽多了

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment