Add test cases for the proxy, and do some tiny fixes.

the fixes are:
1. add "content-type" in headers for when dealing with localresponse
2. make a more accurate tip for throttle rate when lower than 1
3. make the crtMgr funcionality a more independent one
4. uppercase the request header before sending it out

update the tip
This commit is contained in:
砚然
2016-08-15 17:48:47 +08:00
parent a925cbed55
commit e489e188f4
31 changed files with 1948 additions and 187 deletions

View File

@@ -1,174 +1,81 @@
var exec = require('child_process').exec,
spawn = require('child_process').spawn,
path = require("path"),
fs = require("fs"),
os = require("os"),
color = require('colorful'),
readline = require('readline'),
util = require('./util'),
logUtil = require("./log"),
certGenerator = require("./certGenerator"),
asyncTask = require("async-task-mgr");
var logUtil = require('./log');
var util = require('./util');
var color = require('colorful');
var EasyCert = require('node-easy-cert');
var exec = require('child_process').exec;
var path = require('path');
var readline = require('readline');
var isWin = /^win/.test(process.platform),
certDir = path.join(util.getUserHome(),"/.anyproxy_certs/"),
rootCAcrtFilePath = path.join(certDir,"rootCA.crt"),
rootCAkeyFilePath = path.join(certDir,"rootCA.key"),
createCertTaskMgr = new asyncTask();
var isWin = /^win/.test(process.platform);
var options = {
rootDirPath: util.getUserHome() + '/.anyproxy_certs',
defaultCertAttrs: [
{ name: 'countryName', value: 'CN' },
{ name: 'organizationName', value: 'AnyProxy' },
{ shortName: 'ST', value: 'SH' },
{ shortName: 'OU', value: 'AnyProxy SSL Proxy' }
]
};
if(!fs.existsSync(certDir)){
try{
fs.mkdirSync(certDir,0777);
}catch(e){
logUtil.printLog("===========", logUtil.T_ERR);
logUtil.printLog("failed to create cert dir ,please create one by yourself - " + certDir, logUtil.T_ERR);
logUtil.printLog("this error will not block main thread unless you use https-related features in anyproxy", logUtil.T_ERR);
logUtil.printLog("===========", logUtil.T_ERR);
}
}
var easyCert = new EasyCert(options);
var crtMgr = util.merge({}, easyCert);
var cache_rootCACrtFileContent, cache_rootCAKeyFileContent;
function getCertificate(hostname,certCallback){
checkRootCA();
var keyFile = path.join(certDir , "__hostname.key".replace(/__hostname/,hostname) ),
crtFile = path.join(certDir , "__hostname.crt".replace(/__hostname/,hostname) );
if(!cache_rootCACrtFileContent || !cache_rootCAKeyFileContent){
cache_rootCACrtFileContent = fs.readFileSync(rootCAcrtFilePath, {encoding: 'utf8'});
cache_rootCAKeyFileContent = fs.readFileSync(rootCAkeyFilePath, {encoding: 'utf8'});
}
createCertTaskMgr.addTask(hostname,function(callback){
if(!fs.existsSync(keyFile) || !fs.existsSync(crtFile)){
try{
var result = certGenerator.generateCertsForHostname(hostname, {
cert: cache_rootCACrtFileContent,
key: cache_rootCAKeyFileContent
});
fs.writeFileSync(keyFile, result.privateKey);
fs.writeFileSync(crtFile, result.certificate);
callback(null, result.privateKey, result.certificate);
}catch(e){
callback(e);
}
}else{
callback(null , fs.readFileSync(keyFile) , fs.readFileSync(crtFile));
// catch specified error, such as ROOT_CA_NOT_EXISTS
crtMgr.getCertificate = function (host, cb) {
easyCert.getCertificate(host, (error, keyContent, crtContent) => {
if (error === 'ROOT_CA_NOT_EXISTS') {
util.showRootInstallTip();
process.exit(0);
return;
}
},function(err,keyContent,crtContent){
if(!err){
certCallback(null ,keyContent,crtContent);
}else{
certCallback(err);
}
cb(error, keyContent, crtContent);
});
}
};
function createCert(hostname,callback){
checkRootCA();
// set default common name of the cert
crtMgr.generateRootCA = function (cb) {
doGenerate(false);
var cmd = cmd_genCert + " __host __path".replace(/__host/,hostname).replace(/__path/,certDir);
exec(cmd,{ cwd : certDir },function(err,stdout,stderr){
if(err){
callback && callback(new Error("error when generating certificate"),null);
}else{
var tipText = "certificate created for __HOST".replace(/__HOST/,hostname);
logUtil.printLog(color.yellow(color.bold("[internal https]")) + color.yellow(tipText)) ;
callback(null);
}
});
}
function doGenerate(overwrite) {
const rootOptions = {
commonName: 'AnyProxy',
overwrite: !!overwrite
};
function clearCerts(cb){
if(isWin){
exec("del * /q",{cwd : certDir},cb);
}else{
exec("rm *.key *.csr *.crt *.srl",{cwd : certDir},cb);
}
}
function isRootCAFileExists(){
return (fs.existsSync(rootCAcrtFilePath) && fs.existsSync(rootCAkeyFilePath));
}
var rootCAExists = false;
function checkRootCA(){
if(rootCAExists) return;
if(!isRootCAFileExists()){
logUtil.printLog(color.red("can not find rootCA.crt or rootCA.key"), logUtil.T_ERR);
logUtil.printLog(color.red("you may generate one by the following methods"), logUtil.T_ERR);
logUtil.printLog(color.red("\twhen using globally : anyproxy --root"), logUtil.T_ERR);
logUtil.printLog(color.red("\twhen using as a module : require(\"anyproxy\").generateRootCA();"), logUtil.T_ERR);
logUtil.printLog(color.red("\tmore info : https://github.com/alibaba/anyproxy/wiki/How-to-config-https-proxy"), logUtil.T_ERR);
process.exit(0);
} else{
rootCAExists = true;
}
}
function generateRootCA(){
if(isRootCAFileExists()){
logUtil.printLog(color.yellow("rootCA exists at " + certDir));
var rl = readline.createInterface({
input : process.stdin,
output: process.stdout
});
rl.question("do you really want to generate a new one ?)(yes/NO)", function(answer) {
if(/yes/i.test(answer)){
startGenerating();
}else{
logUtil.printLog("will not generate a new one");
process.exit(0);
easyCert.generateRootCA(rootOptions, (error, keyPath, crtPath) => {
if (!error) {
const certDir = path.dirname(keyPath);
logUtil.printLog(color.cyan('The cert is generated at "' + certDir + '"'));
if(isWin){
exec("start .",{ cwd : certDir });
}else{
exec("open .",{ cwd : certDir });
}
}
rl.close();
});
}else{
startGenerating();
}
if (error === 'ROOT_CA_EXISTED') {
var rl = readline.createInterface({
input : process.stdin,
output: process.stdout
});
function startGenerating(){
//clear old certs
clearCerts(function(){
logUtil.printLog(color.green("temp certs cleared"));
try{
var result = certGenerator.generateRootCA();
fs.writeFileSync(rootCAkeyFilePath, result.privateKey);
fs.writeFileSync(rootCAcrtFilePath, result.certificate);
rl.question("do you really want to generate a new one ?)(yes/NO)", function(answer) {
if(/yes/i.test(answer)){
doGenerate(true);
}else{
console.log("will not generate a new one");
logUtil.printLog(color.green("rootCA generated"));
logUtil.printLog(color.green(color.bold("please trust the rootCA.crt in " + certDir)));
logUtil.printLog(color.green(color.bold("or you may get it via anyproxy webinterface")));
}
rl.close();
});
if(isWin){
exec("start .",{cwd : certDir});
}else{
exec("open .",{cwd : certDir});
}
}catch(e){
logUtil.printLog(color.red(e));
logUtil.printLog(color.red(e.stack));
logUtil.printLog(color.red("fail to generate root CA"),logUtil.T_ERR);
return;
}
cb(error, keyPath, crtPath);
});
}
}
function getRootCAFilePath(){
if(isRootCAFileExists()){
return rootCAcrtFilePath;
}else{
return "";
}
}
};
module.exports.getRootCAFilePath = getRootCAFilePath;
module.exports.generateRootCA = generateRootCA;
module.exports.getCertificate = getCertificate;
module.exports.createCert = createCert;
module.exports.clearCerts = clearCerts;
module.exports.isRootCAFileExists = isRootCAFileExists;
module.exports = crtMgr;

View File

@@ -14,8 +14,7 @@ var http = require("http"),
logUtil = require("./log"),
httpsServerMgr = require("./httpsServerMgr");
var defaultRule = require("./rule_default.js"),
userRule = defaultRule; //init
var userRule = util.freshRequire('./rule_default');
function userRequestHandler(req,userRes){
/*
@@ -103,7 +102,7 @@ function userRequestHandler(req,userRes){
resourceInfo.resBody = resBody;
resourceInfo.length = resBody ? resBody.length : 0;
resourceInfo.statusCode = statusCode;
GLOBAL.recorder && GLOBAL.recorder.updateRecord(resourceInfoId,resourceInfo);
userRes.writeHead(statusCode,resHeader);
@@ -140,6 +139,8 @@ function userRequestHandler(req,userRes){
options.headers = util.lower_keys(options.headers);
options.headers["content-length"] = reqData.length; //rewrite content length info
options.headers = util.upper_keys(options.headers);
//send request
var proxyReq = ( /https/.test(protocol) ? https : http).request(options, function(res) {
@@ -202,7 +203,7 @@ function userRequestHandler(req,userRes){
//delay
},function(callback){
var pauseTimeInMS = userRule.pauseBeforeSendingResponse(req,res);
var pauseTimeInMS = userRule.pauseBeforeSendingResponse(req,res);
if(pauseTimeInMS){
setTimeout(callback,pauseTimeInMS);
}else{
@@ -232,7 +233,7 @@ function userRequestHandler(req,userRes){
resourceInfo.resHeader = resHeader;
resourceInfo.resBody = serverResData;
resourceInfo.length = serverResData ? serverResData.length : 0;
GLOBAL.recorder && GLOBAL.recorder.updateRecord(resourceInfoId,resourceInfo);
callback();
@@ -260,7 +261,7 @@ function userRequestHandler(req,userRes){
userRes.end();
});
proxyReq.end(reqData);
proxyReq.end(reqData);
}
}
@@ -272,14 +273,14 @@ function connectReqHandler(req, socket, head){
var shouldIntercept = userRule.shouldInterceptHttpsReq(req);
//bypass webSocket on webinterface
//bypass webSocket on webinterface
if(targetPort == 8003){
shouldIntercept = false; // TODO : a more general solution?
}
logUtil.printLog(color.green("\nreceived https CONNECT request " + host));
if(shouldIntercept){
logUtil.printLog("==>will forward to local https server");
logUtil.printLog("==>will forward to local https server");
}else{
logUtil.printLog("==>will bypass the man-in-the-middle proxy");
}
@@ -325,7 +326,7 @@ function connectReqHandler(req, socket, head){
//determine the target server
function(callback){
if(shouldIntercept){
proxyPort = internalHttpsPort;
proxyHost = "127.0.0.1";
@@ -356,11 +357,11 @@ function connectReqHandler(req, socket, head){
callback();
});
});
});
conn.on("error",function(e){
logUtil.printLog("err when connect to + " + host + " , " + e, logUtil.T_ERR);
});
});
}catch(e){
logUtil.printLog("err when connect to remote https server (__host)".replace(/__host/,host), logUtil.T_ERR);
}
@@ -372,7 +373,7 @@ function connectReqHandler(req, socket, head){
resourceInfo.resHeader = {};
resourceInfo.resBody = "";
resourceInfo.length = 0;
GLOBAL.recorder && GLOBAL.recorder.updateRecord(resourceInfoId,resourceInfo);
callback();
@@ -385,9 +386,13 @@ function connectReqHandler(req, socket, head){
});
}
/**
* @return return the merged rule for reference
*/
function setRules(newRule){
if(!newRule){
return;
return userRule;
}else{
if(!newRule.summary){
@@ -396,8 +401,8 @@ function setRules(newRule){
};
}
userRule = util.merge(defaultRule,newRule);
userRule = util.merge(userRule,newRule);
var functions = [];
if('function' == typeof(userRule.init)){
functions.push(function(cb){
@@ -416,6 +421,7 @@ function setRules(newRule){
}
});
return userRule;
}
}
@@ -427,3 +433,4 @@ module.exports.userRequestHandler = userRequestHandler;
module.exports.connectReqHandler = connectReqHandler;
module.exports.setRules = setRules;
module.exports.getRuleSummary = getRuleSummary;
module.exports.token = Date.now();

View File

@@ -73,6 +73,7 @@ setTimeout(function(){
module.exports = {
token: Date.now(),
summary:function(){
var tip = "the default rule for AnyProxy.";
if(!isRootCAFileExists){
@@ -102,7 +103,10 @@ module.exports = {
if(err){
callback(200, {}, "[AnyProxy failed to load local file] " + err);
}else{
callback(200, {}, buffer);
var header = {
'Content-Type': utils.contentType(req.anyproxy_map_local)
};
callback(200, header, buffer);
}
});
}

View File

@@ -1,8 +1,11 @@
var fs = require("fs"),
path = require("path"),
mime = require('mime-types'),
color = require('colorful'),
logUtil = require("./log"),
exec = require('child_process').exec;
const changeCase = require('change-case');
// {"Content-Encoding":"gzip"} --> {"content-encoding":"gzip"}
module.exports.lower_keys = function(obj){
for(var key in obj){
@@ -34,7 +37,7 @@ module.exports.getAnyProxyHome = function(){
if(!fs.existsSync(home)){
try{
fs.mkdirSync(home,0777);
fs.mkdirSync(home, '0777');
}catch(e){
return null;
}
@@ -48,7 +51,7 @@ module.exports.generateCacheDir = function(){
var rand = Math.floor(Math.random() * 1000000),
cachePath = path.join(util.getAnyProxyHome(),"./" + CACHE_DIR_PREFIX + rand);
fs.mkdirSync(cachePath,0777);
fs.mkdirSync(cachePath, '0777');
return cachePath;
}
@@ -60,7 +63,7 @@ module.exports.clearCacheDir = function(cb){
if(isWin){
exec("for /D %f in (" + dirNameWildCard + ") do rmdir %f /s /q",{cwd : home},cb);
}else{
exec("rm -rf " + dirNameWildCard + "",{cwd : home},cb);
exec("rm -rf " + dirNameWildCard + "",{cwd : home},cb);
}
}
@@ -102,4 +105,53 @@ module.exports.filewalker = function(root,cb){
cb && cb.apply(null,[null,ret]);
});
}
};
/*
* 获取文件所对应的content-type以及content-length等信息
* 比如在useLocalResponse的时候会使用到
*/
module.exports.contentType = function (filepath) {
return mime.contentType(path.extname(filepath));
};
/*
* 读取file的大小以byte为单位
*/
module.exports.contentLength = function (filepath) {
try {
var stat = fs.statSync(filepath);
return stat.size;
} catch (e) {
logUtil.printLog(color.red("\nfailed to ready local file : " + filepath));
logUtil.printLog(color.red(e));
return 0;
}
};
module.exports.showRootInstallTip = function () {
logUtil.printLog(color.red("can not find rootCA.crt or rootCA.key"), logUtil.T_ERR);
logUtil.printLog(color.red("you may generate one by the following methods"), logUtil.T_ERR);
logUtil.printLog(color.red("\twhen using globally : anyproxy --root"), logUtil.T_ERR);
logUtil.printLog(color.red("\twhen using as a module : require(\"anyproxy\").generateRootCA();"), logUtil.T_ERR);
logUtil.printLog(color.red("\tmore info : https://github.com/alibaba/anyproxy/wiki/How-to-config-https-proxy"), logUtil.T_ERR);
};
/*
* remove the cache before requering, the path SHOULD BE RELATIVE TO UTIL.JS
*/
module.exports.freshRequire = function (path) {
delete require.cache[require.resolve(path)];
return require(path);
};
module.exports.upper_keys = function (obj) {
var upperObject = {};
for(var key in obj) {
var upperKey = changeCase.headerCase(key);
upperObject[upperKey] = obj[key];
}
return upperObject;
};