diff --git a/README.md b/README.md index 05aa446..3d4612b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ anyproxy ========== -another proxy written in NodeJS, which can handle HTTPS requests and CORS perfectly. Produced by Alipay-ct-wd. +another proxy written in NodeJS, which can handle HTTPS requests and CORS perfectly. Feature ------------ @@ -95,7 +95,7 @@ var rules = { ,{ "host" :/./, "path" :/\.(png|gif|jpg|jpeg)/, - "localFile" :"/Users/Stella/tmp/test.png", + "localFile" :"/Users/Username/tmp/test.png", "localDir" :"~/" } ] @@ -111,4 +111,3 @@ module.exports = rules; ## Contact * Please feel free to raise any issue about this project, or give us some advice on this doc. :) -* Email: alipay-sh-wd@list.alibaba-inc.com diff --git a/lib/httpsServerMgr.js b/lib/httpsServerMgr.js index 6f0e8a1..9132cb8 100644 --- a/lib/httpsServerMgr.js +++ b/lib/httpsServerMgr.js @@ -18,7 +18,7 @@ module.exports =function(){ var self = this; self.serverList = { /* schema sample - "www.alipay.com":{ + "www.example.com":{ port:123, server : serverInstance, lastestUse: 99999 //unix time stamp diff --git a/lib/requestHandler.js b/lib/requestHandler.js index 542eb7c..33a1213 100644 --- a/lib/requestHandler.js +++ b/lib/requestHandler.js @@ -1,13 +1,15 @@ -var http = require("http"), +var http = require("http"), https = require("https"), net = require("net"), fs = require("fs"), url = require("url"), pathUtil = require("path"), + zlib = require('zlib'), + async = require('async'), color = require("colorful"), - sleep = require("sleep"), Buffer = require('buffer').Buffer, - httpsServerMgr = require("./httpsServerMgr"); + httpsServerMgr = require("./httpsServerMgr"), + userRule = require("./rule.js"); //TODO - to be configurable var httpsServerMgrInstance = new httpsServerMgr(); @@ -31,16 +33,16 @@ function userRequestHandler(req,userRes){ var host = req.headers.host, urlPattern = url.parse(req.url), path = urlPattern.path, - ifLocalruleMatched = false, - callback = null, - ifHttps = !!req.connection.encrypted && !/http:/.test(req.url), + callback = null, //TODO : remove callback + protocol = (!!req.connection.encrypted && !/http:/.test(req.url)) ? "https" : "http", resourceInfo = {}, resourceInfoId = -1; + //record resourceInfo.host = host; resourceInfo.method = req.method; resourceInfo.path = path; - resourceInfo.url = (ifHttps ? "https://" :"http://") + host + path; + resourceInfo.url = protocol + "://" + host + path; resourceInfo.req = req; resourceInfo.startTime = new Date().getTime(); @@ -51,104 +53,144 @@ function userRequestHandler(req,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 http server : http://www.example.com/a/b/c in https server : /work/alibaba */ - //handle OPTIONS request - if(req.method == "OPTIONS"){ - console.log("==>OPTIONS req for CROS, will allow all"); - userRes.writeHead(200,mergeCORSHeader(req.headers)); //remove any cache related header, add crossdomain headers - userRes.end(); - return; - } + if(userRule.shouldUseLocalResponse(req)){ + console.log("==>use local rules"); + userRule.dealLocalResponse(req,function(statusCode,resHeader,resBody){ - //try to mactch rule file - 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; - - //localfile not set, map to dir - if(!targetLocalfile){ //find file in dir, /a/b/file.html -> dir + b/file.html - var remotePathWithoutPrefix = path.replace(new RegExp(rule.path),""); //remove prefix - targetLocalfile = pathUtil.join(rule.localDir,remotePathWithoutPrefix); + //update record info + resourceInfo.endTime = new Date().getTime(); + resourceInfo.res = { //construct a self-defined res object + statusCode : statusCode || "", + headers : resHeader || {} } + resourceInfo.resBody = resBody; + resourceInfo.length = resBody.length; - console.log("==>local file: " + targetLocalfile); - if(fs.existsSync(targetLocalfile)){ - try{ - var fsStream = fs.createReadStream(targetLocalfile); - userRes.writeHead(200,mergeCORSHeader( req.headers,{}) ); //CORS for localfiles - fsStream.pipe(userRes); - ifLocalruleMatched = true; - break; - }catch(e){ - console.log(e.message); - } - }else{ - console.log("file not exist : " + targetLocalfile); - } - } + try{ + GLOBAL.recorder.updateRecord(resourceInfoId,resourceInfo); + }catch(e){} - //sleep for seconds if configed in the rule file - //see rule_sample.js - if(hostTest && pathTest && !!rule.sleep){ - console.log(color.yellow('[' + req.url + '] will sleep for ' + rule.sleep + ' seconds.')); - sleep.sleep(rule.sleep); - } + userRes.writeHead(statusCode,resHeader); + userRes.end(resBody); + }); - if(hostTest && pathTest && !!rule.callback){ - callback = rule.callback; - } - } - - if(ifLocalruleMatched){ return; }else{ console.log("==>will forward to real server by proxy"); + //modify protocol if needed + protocol = userRule.replaceRequestProtocol(req,protocol) || protocol; + var options = { hostname : urlPattern.hostname || req.headers.host, - port : urlPattern.port || req.port || (ifHttps ? 443 : 80), + port : urlPattern.port || req.port || (/https/.test(protocol) ? 443 : 80), path : path, method : req.method, headers : req.headers }; - var proxyReq = (ifHttps ? https : http).request(options, function(res) { - userRes.writeHead(res.statusCode,mergeCORSHeader(req.headers,res.headers)); + //modify request options + options = userRule.replaceRequestOption(req,options) || options; + + var proxyReq = ( /https/.test(protocol) ? https : http).request(options, function(res) { + var statusCode = res.statusCode; + statusCode = userRule.replaceResponseStatusCode(req,res,statusCode) || statusCode; + + var resHeader = res.headers; + resHeader = userRule.replaceResponseHeader(req,res,resHeader) || resHeader; + + //remove content-encoding + // delete resHeader['content-encoding']; + + userRes.writeHead(statusCode, resHeader); var resData = [], - length = 0; + length; + res.on("data",function(chunk){ resData.push(chunk); - length += chunk.length; - userRes.write(chunk); }); res.on("end",function(){ - callback && callback.call(null,userRes); - userRes.end(); - //update record info - resourceInfo.endTime = new Date().getTime(); - resourceInfo.res = res; - resourceInfo.resBody = Buffer.concat(resData); - resourceInfo.length = length; + var serverResData, + userCustomResData; + + async.series([ + //TODO : manage gzip + + //unzip server res + function(callback){ + serverResData = Buffer.concat(resData); + if(/gzip/i.test(res.headers['content-encoding'])){ + zlib.gunzip(serverResData,function(err,buff){ + serverResData = buff; + callback(); + }); + }else{ + callback(); + } + + //get custom response + },function(callback){ + + userCustomResData = userRule.replaceServerResData(req,res,serverResData); + + //gzip users' string if necessary + if(typeof userCustomResData == "string" && /gzip/i.test(res.headers['content-encoding']) ){ + zlib.gzip(userCustomResData,function(err,data){ + userCustomResData = data; + console.log(data); + callback(); + }); + }else{ + callback(); + } + + //generate response data + },function(callback){ + serverResData = userCustomResData || serverResData; + callback(); + + //delay + },function(callback){ + var pauseTimeInMS = userRule.pauseBeforeSendingResponse(req,res); + if(pauseTimeInMS){ + setTimeout(callback,pauseTimeInMS); + }else{ + callback(); + } + + //send response + },function(callback){ + userRes.write(serverResData); + userRes.end(); + + callback(); + + //udpate record info + },function(callback){ + resourceInfo.endTime = new Date().getTime(); + resourceInfo.res = res; //TODO : replace res header / statusCode ? + resourceInfo.resBody = serverResData; + resourceInfo.length = serverResData.length; + + try{ + GLOBAL.recorder.updateRecord(resourceInfoId,resourceInfo); + }catch(e){} + + callback(); + } + + ],function(err,result){ + + }); - try{ - GLOBAL.recorder.updateRecord(resourceInfoId,resourceInfo); - }catch(e){} - }); res.on('error',function(error){ console.log('error',error); @@ -230,6 +272,7 @@ function connectReqHandler(req, socket, head){ } } +//TODO : reactive this function function setRules(newRule){ if(!newRule){ return; @@ -242,35 +285,6 @@ function setRules(newRule){ } } -// https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS -function mergeCORSHeader(reqHeader,originHeader){ - var targetObj = originHeader || {}; - - delete targetObj["Access-Control-Allow-Credentials"]; - delete targetObj["Access-Control-Allow-Origin"]; - delete targetObj["Access-Control-Allow-Methods"]; - delete targetObj["Access-Control-Allow-Headers"]; - - targetObj["access-control-allow-credentials"] = "true"; - targetObj["access-control-allow-origin"] = reqHeader['origin'] || "-___-||"; - targetObj["access-control-allow-methods"] = "GET, POST, PUT"; - targetObj["access-control-allow-headers"] = reqHeader['access-control-request-headers'] || "-___-||"; - - // - // targetObj["Transfer-Encoding"] = "chunked"; - - // // Disable caching - // // If the response status is 304 not modified, the data event of response will not emmit - // targetObj["Cache-Control"] = "no-cache, no-store, must-revalidate"; - // targetObj["Pragma"] = "no-cache"; - // targetObj["Expires"] = 0; - // // - // targetObj["server"] = "anyproxy server"; - // targetObj["x-powered-by"] = "Alipay-ct-wd"; - - return targetObj; -} - module.exports.userRequestHandler = userRequestHandler; module.exports.connectReqHandler = connectReqHandler; module.exports.setRules = setRules; diff --git a/package.json b/package.json index 2d83f88..0c32bd6 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,6 @@ "entities": "^1.1.1", "express": "^4.8.5", "nedb": "^0.11.0", - "sleep": "~1.1.8", "ws": "^0.4.32" }, "devDependencies": { diff --git a/rule_sample.js b/rule_sample.js index 9334d77..b585c3b 100644 --- a/rule_sample.js +++ b/rule_sample.js @@ -19,8 +19,7 @@ var rules = { "localDir" :"/Users/Stella/tmp/" },{ "host" :/./, - "path" :/response\.(json)/, - "sleep" :5//seconds + "path" :/response\.(json)/ },{ "host" :/./, "path" :/html/, @@ -33,7 +32,7 @@ var rules = { ] ,"httpsConfig":{ "bypassAll" : false, //by setting this to true, anyproxy will not intercept any https request - "interceptDomains":[/www\.alipay\.com/,/www\.b\.com/] //by setting bypassAll:false, requests towards these domains will be intercepted, and try to meet the map rules above + "interceptDomains":[/www\.example\.com/] //by setting bypassAll:false, requests towards these domains will be intercepted, and try to meet the map rules above } } diff --git a/test.js b/test.js index 2045938..f7e2d6a 100644 --- a/test.js +++ b/test.js @@ -52,7 +52,7 @@ module.exports.testHttpsOverHttp = function(test){ }); var req = https.request({ - host: 'www.alipay.com', + host: 'www.gotofail.com', port: 443, agent: tunnelingAgent },function(res){