From a9d56ef859f76bef018e67fa807d81f6e7b9bc73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8A=A0=E9=87=8C?= Date: Wed, 13 Aug 2014 11:51:39 +0800 Subject: [PATCH] add bin.js --- bin.js | 22 ++++- cert/gen-cer | 4 +- cert/gen-rootCA | 9 +- lib/certMgr.js | 43 +++++++- lib/httpsServerMgr.js | 75 ++++++++------ lib/httpsServerMgr_test.js | 8 ++ lib/requestHandler.js | 194 +++++++++++++++++++++++++++++++++---- package.json | 3 +- proxy.js | 51 +++------- rule_sample.js | 36 +++++++ test.js | 16 +++ 11 files changed, 361 insertions(+), 100 deletions(-) create mode 100644 lib/httpsServerMgr_test.js create mode 100644 rule_sample.js create mode 100644 test.js diff --git a/bin.js b/bin.js index 77436ca..76edbaf 100644 --- a/bin.js +++ b/bin.js @@ -1,22 +1,36 @@ #!/usr/bin/env node -var program = require('commander'), - mainProxy = require("./proxy.js"); +var program = require('commander'), + mainProxy = require("./proxy.js"), + color = require('colorful'), + fs = require("fs"); program .option('-u, --host [value]', 'hostname for https proxy, localhost for default') .option('-t, --type [value]', 'http|https,http for default') .option('-p, --port [value]', 'proxy port, 8001 for default') + .option('-r, --rule [value]', 'rule file to map localfiles') + .option('-g, --root [value]', 'generate root CA') .option('-c, --clear', 'clear all the tmp certificates') .parse(process.argv); if(program.clear){ require("./lib/certMgr").clearCerts(function(){ - console.log("all certs cleared"); + console.log( color.green("all certs cleared") ); process.exit(0); }); +}else if(program.root){ + require("./lib/certMgr").generateRootCA(function(){ + process.exit(0); + }); }else{ - mainProxy.startServer(program.type,program.port, program.host); + var handleRule; + if(program.rule){ + console.log("parsing rule file.."); + var handleRule = require(program.rule); + } + + mainProxy.startServer(program.type,program.port, program.host ,handleRule); } \ No newline at end of file diff --git a/cert/gen-cer b/cert/gen-cer index 141e699..6ac5d34 100755 --- a/cert/gen-cer +++ b/cert/gen-cer @@ -16,10 +16,10 @@ email=a@b.com #Optional password=a -if [ -z "$outputPath$domain" ] +if [ -z "$domain" ] then echo "Argument not present." - echo "Useage $0 [common name]" + echo "Useage $0 [domain] [outputPath]" exit 99 fi diff --git a/cert/gen-rootCA b/cert/gen-rootCA index 5e89008..4b649f7 100755 --- a/cert/gen-rootCA +++ b/cert/gen-rootCA @@ -3,4 +3,11 @@ outputPath=$1 cd $outputPath openssl genrsa -out rootCA.key 2048 -openssl req -x509 -new -nodes -key rootCA.key -days 36500 -out rootCA.crt \ No newline at end of file +openssl req -x509 -new -nodes -key rootCA.key -days 36500 -out rootCA.crt +echo "=============" +echo "rootCA generated at :" +pwd +echo "please trust them and add to your system keychain" +echo "=============" + +exit 0 \ No newline at end of file diff --git a/lib/certMgr.js b/lib/certMgr.js index 262df15..758e868 100644 --- a/lib/certMgr.js +++ b/lib/certMgr.js @@ -1,11 +1,14 @@ var exec = require('child_process').exec, + spawn = require('child_process').spawn, path = require("path"), fs = require("fs"), os = require("os"), - asyncTaskMgr = require("./asyncTaskMgr"); + color = require('colorful'), + asyncTask = require("./asyncTaskMgr"); var certDir = path.join(getUserHome(),"/.anyproxy_certs/"), - asyncTaskMgr = new asyncTaskMgr(); + cmdDir = "./cert/", + asyncTaskMgr = new asyncTask(); if(!fs.existsSync(certDir)){ fs.mkdirSync(certDir); @@ -38,14 +41,16 @@ function getUserHome() { } function createCert(hostname,callback){ - console.log("creating cert for :" + hostname); + console.log(hostname); + checkRootCA(); var cmd = "./gen-cer __host __path".replace(/__host/,hostname).replace(/__path/,certDir); - exec(cmd,{cwd:"./cert/"},function(err,stdout,stderr){ + exec(cmd,{ cwd : cmdDir },function(err,stdout,stderr){ if(err){ callback && callback(new Error("error when generating certificate"),null); }else{ - console.log("certificate created for __HOST".replace(/__HOST/,hostname)); + var tipText = "certificate created for __HOST".replace(/__HOST/,hostname); + console.log(color.yellow(color.bold("[internal https]")) + color.yellow(tipText)); callback(null); } }); @@ -55,6 +60,34 @@ function clearCerts(cb){ exec("rm *.key *.csr *.crt",{cwd : certDir},cb); } +function checkRootCA(){ + var crtFile = path.join(cmdDir,"rootCA.crt"), + keyFile = path.join(cmdDir,"rootCA.key"); + + if(!fs.existsSync(crtFile) || !fs.existsSync(keyFile)){ + console.log(color.red("can not find rootCA.crt or rootCA.key")); + process.exit(0); + } +} + +function generateRootCA(){ + var spawnSteam = spawn("./gen-rootCA",['.'],{cwd:cmdDir,stdio: 'inherit'}); + + spawnSteam.on('close', function (code) { + if(code == 0){ + console.log(color.green("rootCA generated")); + console.log("now clearing temp certs..."); + clearCerts(function(){ + console.log(color.green("done")); + process.exit(0); + }); + }else{ + + } + }); +} + +module.exports.generateRootCA = generateRootCA; module.exports.getCertificate = getCertificate; module.exports.createCert = createCert; module.exports.clearCerts = clearCerts; \ No newline at end of file diff --git a/lib/httpsServerMgr.js b/lib/httpsServerMgr.js index dd55eac..11f9fe2 100644 --- a/lib/httpsServerMgr.js +++ b/lib/httpsServerMgr.js @@ -6,11 +6,16 @@ var getPort = require('./getPort'), fs = require('fs'), net = require('net'), url = require('url'), + color = require('colorful'), certMgr = require("./certMgr"), - requestHandler = require("./requestHandler"); + requestHandler = require("./requestHandler"), + asyncTask = require("./asyncTaskMgr"); + var DEFAULT_RELEASE_TIME = 120*1000; +var asyncTaskMgr = new asyncTask(); + module.exports =function(){ var self = this; self.serverList = { @@ -30,40 +35,49 @@ module.exports =function(){ //server exists if(serverInfo){ - console.log("exists :" + hostname ); serverInfo.lastestUse = new Date().getTime(); port = serverInfo.port; userCB && userCB(null,port); //create server with corresponding CA }else{ - console.log("creating :" + hostname ); - async.series([ - //find a clean port - function(callback){ - getPort(function(cleanPort){ - port = cleanPort; - callback(null,port); - }); - } - - //create server - ,function(callback){ - certMgr.getCertificate(hostname,function(err,keyContent,crtContent){ - var server = createHttpsServer(port,keyContent,crtContent); - self.serverList[hostname] = { - port : port, - server : server, - lastestUse : new Date().getTime() - }; - console.log("https server @port __portNum for __HOST established".replace(/__portNum/,port).replace(/__HOST/,hostname)); - callback && callback(null,port); - - }); - } - ],function(err,result){ - userCB && userCB(err,result[result.length - 1]); + + asyncTaskMgr.addTask(hostname ,userCB,function(cb){ + createServer(cb); }); + + function createServer(cb){ + async.series([ + //find a clean port + function(callback){ + getPort(function(cleanPort){ + port = cleanPort; + callback(null,port); + }); + } + + //create server + ,function(callback){ + + certMgr.getCertificate(hostname,function(err,keyContent,crtContent){ + var server = createHttpsServer(port,keyContent,crtContent); + self.serverList[hostname] = { + port : port, + server : server, + lastestUse : new Date().getTime() + }; + + var tipText = "https server @port __portNum for __HOST established".replace(/__portNum/,port).replace(/__HOST/,hostname); + console.log(color.yellow(color.bold("[internal https]")) + color.yellow(tipText)); + callback && callback(null,port); + + }); + } + ],function(err,result){ + cb && cb(err,result[result.length - 1]); + + }); + } } }; @@ -75,7 +89,7 @@ module.exports =function(){ if( (timeNow - item.lastestUse) > DEFAULT_RELEASE_TIME){ item.server.close(); delete self.serverList[serverName]; - console.log("https server released : " + serverName); + console.log(color.yellow(color.bold("[internal https]")) + color.yellow("https server released : " + serverName)); } } @@ -86,6 +100,7 @@ function createHttpsServer(port,keyContent,crtContent){ return https.createServer({ key : keyContent, cert: crtContent - },requestHandler).listen(port); + },requestHandler.userRequestHandler).listen(port); + } diff --git a/lib/httpsServerMgr_test.js b/lib/httpsServerMgr_test.js new file mode 100644 index 0000000..09bc853 --- /dev/null +++ b/lib/httpsServerMgr_test.js @@ -0,0 +1,8 @@ +var httpsServerMgr = require("./httpsServerMgr"); + + +var instance = new httpsServerMgr(); + +instance.fetchPort("localhost",function(err,port){ + console.log(port); +}); \ No newline at end of file diff --git a/lib/requestHandler.js b/lib/requestHandler.js index c6c4e87..67a1962 100644 --- a/lib/requestHandler.js +++ b/lib/requestHandler.js @@ -1,29 +1,181 @@ var http = require("http"), - https = require("https"); + https = require("https"), + net = require("net"), + fs = require("fs"), + url = require("url"), + pathUtil = require("path"), + color = require("colorful"), + httpsServerMgr = require("./httpsServerMgr"); -function handler(req,userRes){ - console.log(req); +var httpsServerMgrInstance = new httpsServerMgr(); - var ifHttps = !!req.connection.encrypted && !/http:/.test(req.url); +//default rule +var handleRule = { + map :[ + // { + // host :".", + // path :"/path/test", + // localFile :"", + // localDir :"~/" + // } + ] + ,httpsConfig:{ + bypassAll : false, + interceptDomains:["^.*alibaba-inc\.com$"] + } +}; - var options = { - hostname : req.headers.host, - port : req.port || (ifHttps ? 443 : 80), - path : req.url, - method : req.method, - headers : req.headers - }; +function userRequestHandler(req,userRes){ + var host = req.headers.host, + path = url.parse(req.url).path, + ifLocalruleMatched = false; - var proxyReq = (ifHttps ? https : http).request(options, function(res) { - userRes.writeHead(res.statusCode,res.headers); - res.pipe(userRes); - }); + console.log(color.green("\nreceived request to : " + host + path)); + /* + req.url is wired + in http server : http://www.baidu.com/a/b/c + in https server : /work/alibaba + */ - proxyReq.on("error",function(e){ - console.log("err with request :" + req.url); - userRes.end(); - }); - proxyReq.end(); + for(var index in handleRule.map){ + var rule = handleRule.map[index]; + + var hostTest = new RegExp(rule.host).test(host), + pathTest = new RegExp(rule.path).test(path); + + if(hostTest && pathTest && (rule.localFile || rule.localDir) ){ + console.log("==>meet the rules, will map to local file"); + + var targetLocalfile = rule.localFile; + if(!targetLocalfile){ //find file in dir //TODO : /a/b/c -> b/c + var basename = pathUtil.basename(path); + basename = basename.slice(0,basename.indexOf("?")); //remove chars after question mark + targetLocalfile = pathUtil.join(rule.localDir,basename); + } + + console.log("==>local file: " + targetLocalfile); + if(fs.existsSync(targetLocalfile)){ + try{ + var fsStream = fs.createReadStream(targetLocalfile); + userRes.writeHead(200,{"cache-control":"max-age=0"}); //remove any cache related header + fsStream.pipe(userRes); + ifLocalruleMatched = true; + break; + }catch(e){ + console.log(e.message); + } + }else{ + console.log("file not exist : " + targetLocalfile); + } + } + } + + if(ifLocalruleMatched){ + return; + + }else{ + console.log("==>will forward to real server by proxy"); + var ifHttps = !!req.connection.encrypted && !/http:/.test(req.url); + + var options = { + hostname : req.headers.host, + port : req.port || (ifHttps ? 443 : 80), + path : path, + method : req.method, + headers : req.headers + }; + + var proxyReq = (ifHttps ? https : http).request(options, function(res) { + userRes.writeHead(res.statusCode,res.headers); + res.pipe(userRes); + }); + + proxyReq.on("error",function(e){ + console.log("err with request :" + req.url); + userRes.end(); + }); + proxyReq.end(); + + } } -module.exports = handler; \ No newline at end of file +function connectReqHandler(req, socket, head){ + var hostname = req.url.split(":")[0], + targetPort= req.url.split(":")[1], + httpsRule = handleRule.httpsConfig; + + var shouldBypass = httpsRule.bypassAll; + if(!shouldBypass){ //read rules + for(var index in httpsRule.interceptDomains){ + var reg = new RegExp(httpsRule.interceptDomains[index]); + if( reg.test(hostname) ){ + shouldBypass = true; + break; + } + } + } + + console.log(color.green("\nreceived https CONNECT request " + hostname)); + + if(shouldBypass){ + console.log("==>meet the rules, will bypass the man-in-the-middle proxy"); + try{ + var conn = net.connect(targetPort, hostname, function(){ + socket.write('HTTP/' + req.httpVersion + ' 200 OK\r\n\r\n', 'UTF-8', function() { + conn.pipe(socket); + socket.pipe(conn); + }); + }); + + conn.on("error",function(e){ + console.log("err when connect to __host".replace(/__host/,hostname)); + }); + }catch(e){ + console.log("err when connect to remote https server (__hostname)".replace(/__hostname/,hostname));//TODO + } + + }else{ + //TODO : remote port other than 433 + console.log("==>will forward to local https server"); + + //forward the https-request to local https server + httpsServerMgrInstance.fetchPort(hostname,function(err,port){ + if(!err && port){ + try{ + var conn = net.connect(port, 'localhost', function(){ //TODO : localhost -> server + socket.write('HTTP/' + req.httpVersion + ' 200 OK\r\n\r\n', 'UTF-8', function() { + conn.pipe(socket); + socket.pipe(conn); + + }); + }); + + conn.on("error",function(e){ + console.log("err when connect to __host".replace(/__host/,hostname)); + }); + }catch(e){ + console.log("err when connect to local https server (__hostname)".replace(/__hostname/,hostname));//TODO + } + + }else{ + console.log("err fetch HTTPS server for host:" + hostname); + } + }); + } +} + +function setRules(newRule){ + if(!newRule){ + return; + } + + if(!newRule.map || !newRule.httpsConfig){ + throw(new Error("invalid rule schema")); + }else{ + handleRule = newRule; + } +} + +module.exports.userRequestHandler = userRequestHandler; +module.exports.connectReqHandler = connectReqHandler; +module.exports.setRules = setRules; diff --git a/package.json b/package.json index d6ea4cc..a8067c5 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,11 @@ "description": "https proxy over http", "main": "proxy.js", "bin": { - "anyproxy":"bin.js" + "anyproxy": "bin.js" }, "dependencies": { "async": "~0.9.0", + "colorful": "^2.1.0", "commander": "~2.3.0" }, "devDependencies": {}, diff --git a/proxy.js b/proxy.js index e94a095..161d22f 100644 --- a/proxy.js +++ b/proxy.js @@ -5,9 +5,9 @@ var http = require('http'), async = require("async"), url = require('url'), exec = require('child_process').exec, - httpsServerMgr = require("./lib/httpsServerMgr"), - certMgr = require("./lib/certMgr"), program = require('commander'), + color = require('colorful'), + certMgr = require("./lib/certMgr"), requestHandler = require("./lib/requestHandler"); var T_TYPE_HTTP = 0, @@ -16,17 +16,18 @@ var T_TYPE_HTTP = 0, DEFAULT_HOST = "localhost", DEFAULT_TYPE = T_TYPE_HTTP; -var httpsServerMgrInstance = new httpsServerMgr(), - httpProxyServer; +var httpProxyServer; -function startServer(type, port, hostname){ +function startServer(type, port, hostname,rule){ var proxyType = /https/i.test(type || DEFAULT_TYPE) ? T_TYPE_HTTPS : T_TYPE_HTTP , proxyPort = port || DEFAULT_PORT, proxyHost = hostname || DEFAULT_HOST; + requestHandler.setRules(rule); + async.series([ - //creat server + //creat proxy server function(callback){ if(proxyType == T_TYPE_HTTPS){ certMgr.getCertificate(proxyHost,function(err,keyContent,crtContent){ @@ -36,21 +37,20 @@ function startServer(type, port, hostname){ httpProxyServer = https.createServer({ key : keyContent, cert: crtContent - },requestHandler); + },requestHandler.userRequestHandler); callback(null); } }); }else{ - httpProxyServer = http.createServer(requestHandler); + httpProxyServer = http.createServer(requestHandler.userRequestHandler); callback(null); - } }, //listen CONNECT method for https over http function(callback){ - httpProxyServer.on('connect',dealProxyConnectReq); + httpProxyServer.on('connect',requestHandler.connectReqHandler); httpProxyServer.listen(proxyPort); callback(null); } @@ -60,36 +60,15 @@ function startServer(type, port, hostname){ //final callback function(err,result){ if(!err){ - console.log( (proxyType == T_TYPE_HTTP ? "Http" : "Https") + " proxy started at port " + proxyPort); + var tipText = (proxyType == T_TYPE_HTTP ? "Http" : "Https") + " proxy started at port " + proxyPort; + console.log(color.green(tipText)); }else{ - console.log("err when start proxy server :("); + var tipText = "err when start proxy server :("; + console.log(color.red(tipText)); console.log(err); } } ); } -function dealProxyConnectReq(req, socket, head){ - var hostname = req.url.split(":")[0]; - - //forward the https-request to local https server - httpsServerMgrInstance.fetchPort(hostname,function(err,port){ - if(!err && port){ - try{ - var conn = net.connect(port, 'localhost', function(){ - socket.write('HTTP/' + req.httpVersion + ' 200 OK\r\n\r\n', 'UTF-8', function() { - conn.pipe(socket); - socket.pipe(conn); - }); - }); - }catch(e){ - console.log("err when connect to local https server (__hostname)".replace(/__hostname/,hostname));//TODO - } - - }else{ - console.log("err fetch HTTPS server for host:" + hostname); - } - }); -} - -module.exports.startServer = startServer; +module.exports.startServer = startServer; \ No newline at end of file diff --git a/rule_sample.js b/rule_sample.js new file mode 100644 index 0000000..2377030 --- /dev/null +++ b/rule_sample.js @@ -0,0 +1,36 @@ +var rules = { + "map" :[ + { + "host" :/./, + "path" :/\/path\/test/, + "localFile" :"", + "localDir" :"~/" + } + ,{ + "host" :/./, + "path" :/png/, + "localFile" :"/Users/Stella/tmp/test.png", + "localDir" :"~/" + } + ,{ + "host" :/./, + "path" :/jpg/, + "localFile" :"/Users/Stella/tmp/test.png", + "localDir" :"~/" + } + ,{ + "host" :/./, + "path" :/gif/, + "localFile" :"/Users/Stella/tmp/test.png", + "localDir" :"~/" + } + ] + ,"httpsConfig":{ + // "bypassAll" : true, + // "interceptDomains":[/^.*alibaba-inc\.com$/] + "bypassAll" : false, + "interceptDomains":[] + } +} + +module.exports = rules; \ No newline at end of file diff --git a/test.js b/test.js new file mode 100644 index 0000000..079afb5 --- /dev/null +++ b/test.js @@ -0,0 +1,16 @@ +var https = require("https"); + +process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0'; + +var options = { + host: "localhost", + port: 8001, + path: "/", + headers: { + Host: "www.alipay.com" + } +}; +https.get(options, function(res) { + console.log(res); + res.pipe(process.stdout); +}); \ No newline at end of file