Merge branch '3.10.3beta1' into 'master'

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

See merge request !2
This commit is contained in:
砚然 2016-08-31 10:59:40 +08:00
commit 530491d2a9
31 changed files with 1948 additions and 187 deletions

6
.babelrc Normal file
View File

@ -0,0 +1,6 @@
{
"presets": [
"es2015",
"stage-0"
]
}

245
.eslintrc.js Normal file
View File

@ -0,0 +1,245 @@
module.exports = {
// http://eslint.org/docs/rules/
"ecmaFeatures": {
"arrowFunctions": false, // enable arrow functions
"binaryLiterals": false, // enable binary literals
"blockBindings": false, // enable let and const (aka block bindings)
"classes": false, // enable classes
"defaultParams": false, // enable default function parameters
"destructuring": false, // enable destructuring
"forOf": false, // enable for-of loops
"generators": false, // enable generators
"modules": false, // enable modules and global strict mode
"objectLiteralComputedProperties": false, // enable computed object literal property names
"objectLiteralDuplicateProperties": false, // enable duplicate object literal properties in strict mode
"objectLiteralShorthandMethods": false, // enable object literal shorthand methods
"objectLiteralShorthandProperties": false, // enable object literal shorthand properties
"octalLiterals": false, // enable octal literals
"regexUFlag": false, // enable the regular expression u flag
"regexYFlag": false, // enable the regular expression y flag
"restParams": false, // enable the rest parameters
"spread": false, // enable the spread operator
"superInFunctions": false, // enable super references inside of functions
"templateStrings": false, // enable template strings
"unicodeCodePointEscapes": false, // enable code point escapes
"globalReturn": false, // allow return statements in the global scope
"jsx": false // enable JSX
},
"parser": "babel-eslint",
"env": {
"browser": true, // browser global variables.
"node": false, // Node.js global variables and Node.js-specific rules.
"worker": false, // web workers global variables.
"amd": true, // defines require() and define() as global variables as per the amd spec.
"mocha": false, // adds all of the Mocha testing global variables.
"jasmine": true, // adds all of the Jasmine testing global variables for version 1.3 and 2.0.
"phantomjs": false, // phantomjs global variables.
"jquery": false, // jquery global variables.
"prototypejs": false, // prototypejs global variables.
"shelljs": false, // shelljs global variables.
"meteor": false, // meteor global variables.
"mongo": false, // mongo global variables.
"applescript": false, // applescript global variables.
"es6": true, // enable all ECMAScript 6 features except for modules.
},
"globals": {
"goog": true,
"module": true,
"exports": true,
"__dirname": true,
"process": true
},
"plugins": [
// e.g. "react" (must run `npm install eslint-plugin-react` first)
],
"rules": {
// Possible Errors
"comma-dangle": 0, // disallow trailing commas in object literals
"no-cond-assign": 0, // disallow assignment in conditional expressions
"no-console": 0, // disallow use of console (off by default in the node environment)
"no-constant-condition": 0, // disallow use of constant expressions in conditions
"no-control-regex": 0, // disallow control characters in regular expressions
"no-debugger": 0, // disallow use of debugger
"no-dupe-args": 0, // disallow duplicate arguments in functions
"no-dupe-keys": 0, // disallow duplicate keys when creating object literals
"no-duplicate-case": 0, // disallow a duplicate case label
"no-empty-character-class": 0, // disallow the use of empty character classes in regular expressions
"no-empty": 0, // disallow empty statements
"no-ex-assign": 0, // disallow assigning to the exception in a catch block
"no-extra-boolean-cast": 0, // disallow double-negation boolean casts in a boolean context
"no-extra-parens": 0, // disallow unnecessary parentheses (off by default)
"no-extra-semi": 1, // disallow unnecessary semicolons
"no-func-assign": 0, // disallow overwriting functions written as function declarations
"no-inner-declarations": 2, // disallow function or variable declarations in nested blocks
"no-invalid-regexp": 0, // disallow invalid regular expression strings in the RegExp constructor
"no-irregular-whitespace": 0, // disallow irregular whitespace outside of strings and comments
"no-negated-in-lhs": 0, // disallow negation of the left operand of an in expression
"no-obj-calls": 0, // disallow the use of object properties of the global object (Math and JSON) as functions
"no-regex-spaces": 0, // disallow multiple spaces in a regular expression literal
"no-reserved-keys": 0, // disallow reserved words being used as object literal keys (off by default)
"no-sparse-arrays": 0, // disallow sparse arrays
"no-unreachable": 0, // disallow unreachable statements after a return, throw, continue, or break statement
"use-isnan": 0, // disallow comparisons with the value NaN
"valid-jsdoc": 0, // Ensure JSDoc comments are valid (off by default)
"valid-typeof": 0, // Ensure that the results of typeof are compared against a valid string
"no-unexpected-multiline": 0, // Avoid code that looks like two expressions but is actually one (off by default)
// Best Practices
"accessor-pairs": 0, // enforces getter/setter pairs in objects (off by default)
"block-scoped-var": 0, // treat var statements as if they were block scoped (off by default)
"complexity": 0, // specify the maximum cyclomatic complexity allowed in a program (off by default)
"consistent-return": 0, // require return statements to either always or never specify values
"curly": 2, // specify curly brace conventions for all control statements
"default-case": 0, // require default case in switch statements (off by default)
"dot-notation": 0, // encourages use of dot notation whenever possible
"dot-location": 0, // enforces consistent newlines before or after dots (off by default)
"eqeqeq": 0, // require the use of === and !==
"guard-for-in": 0, // make sure for-in loops have an if statement (off by default)
"no-alert": 0, // disallow the use of alert, confirm, and prompt
"no-caller": 0, // disallow use of arguments.caller or arguments.callee
"no-div-regex": 0, // disallow division operators explicitly at beginning of regular expression (off by default)
"no-else-return": 0, // disallow else after a return in an if (off by default)
"no-empty-label": 0, // disallow use of labels for anything other then loops and switches
"no-eq-null": 0, // disallow comparisons to null without a type-checking operator (off by default)
"no-eval": 2, // disallow use of eval()
"no-extend-native": 2, // disallow adding to native types
"no-extra-bind": 0, // disallow unnecessary function binding
"no-fallthrough": 0, // disallow fallthrough of case statements
"no-floating-decimal": 0, // disallow the use of leading or trailing decimal points in numeric literals (off by default)
"no-implied-eval": 0, // disallow use of eval()-like methods
"no-iterator": 0, // disallow usage of __iterator__ property
"no-labels": 0, // disallow use of labeled statements
"no-lone-blocks": 0, // disallow unnecessary nested blocks
"no-loop-func": 0, // disallow creation of functions within loops
"no-multi-spaces": 0, // disallow use of multiple spaces
"no-multi-str": 0, // disallow use of multiline strings
"no-native-reassign": 0, // disallow reassignments of native objects
"no-new-func": 0, // disallow use of new operator for Function object
"no-new-wrappers": 2, // disallows creating new instances of String, Number, and Boolean
"no-new": 0, // disallow use of new operator when not part of the assignment or comparison
"no-octal-escape": 0, // disallow use of octal escape sequences in string literals, such as var foo = "Copyright \251";
"no-octal": 0, // disallow use of octal literals
"no-param-reassign": 0, // disallow reassignment of function parameters (off by default)
"no-process-env": 0, // disallow use of process.env (off by default)
"no-proto": 0, // disallow usage of __proto__ property
"no-redeclare": 0, // disallow declaring the same variable more then once
"no-return-assign": 0, // disallow use of assignment in return statement
"no-script-url": 0, // disallow use of javascript: urls
"no-self-compare": 0, // disallow comparisons where both sides are exactly the same (off by default)
"no-sequences": 0, // disallow use of comma operator
"no-throw-literal": 0, // restrict what can be thrown as an exception (off by default)
"no-unused-expressions": 0, // disallow usage of expressions in statement position
"no-void": 0, // disallow use of void operator (off by default)
"no-warning-comments": 0, // disallow usage of configurable warning terms in comments, e.g. TODO or FIXME (off by default)
"no-with": 2, // disallow use of the with statement
"radix": 0, // require use of the second argument for parseInt() (off by default)
"vars-on-top": 0, // requires to declare all vars on top of their containing scope (off by default)
"wrap-iife": 0, // require immediate function invocation to be wrapped in parentheses (off by default)
"yoda": 0, // require or disallow Yoda conditions
// Strict Mode
"strict": 0, // controls location of Use Strict Directives
// Variables
"no-catch-shadow": 0, // disallow the catch clause parameter name being the same as a variable in the outer scope (off by default in the node environment)
"no-delete-var": 0, // disallow deletion of variables
"no-label-var": 0, // disallow labels that share a name with a variable
"no-shadow": 0, // disallow declaration of variables already declared in the outer scope
"no-shadow-restricted-names": 0, // disallow shadowing of names such as arguments
"no-undef": 2, // disallow use of undeclared variables unless mentioned in a /*global */ block
"no-undef-init": 0, // disallow use of undefined when initializing variables
"no-undefined": 0, // disallow use of undefined variable (off by default)
"no-unused-vars": 0, // disallow declaration of variables that are not used in the code
"no-use-before-define": 0, // disallow use of variables before they are defined
// Node.js
"handle-callback-err": 0, // enforces error handling in callbacks (off by default) (on by default in the node environment)
"no-mixed-requires": 0, // disallow mixing regular variable and require declarations (off by default) (on by default in the node environment)
"no-new-require": 0, // disallow use of new operator with the require function (off by default) (on by default in the node environment)
"no-path-concat": 0, // disallow string concatenation with __dirname and __filename (off by default) (on by default in the node environment)
"no-process-exit": 0, // disallow process.exit() (on by default in the node environment)
"no-restricted-modules": 0, // restrict usage of specified node modules (off by default)
"no-sync": 0, // disallow use of synchronous methods (off by default)
// Stylistic Issues
"array-bracket-spacing": [2, "never"], // enforce spacing inside array brackets (off by default)
"brace-style": 0, // enforce one true brace style (off by default)
"camelcase": 0, // require camel case names
"comma-spacing": 0, // enforce spacing before and after comma
"comma-style": 0, // enforce one true comma style (off by default)
"computed-property-spacing": 0, // require or disallow padding inside computed properties (off by default)
"consistent-this": 0, // enforces consistent naming when capturing the current execution context (off by default)
"eol-last": 0, // enforce newline at the end of file, with no multiple empty lines
"func-names": 0, // require function expressions to have a name (off by default)
"func-style": 0, // enforces use of function declarations or expressions (off by default)
"indent": [2, 4], // this option sets a specific tab width for your code (off by default)
"key-spacing": 0, // enforces spacing between keys and values in object literal properties
"lines-around-comment": 0, // enforces empty lines around comments (off by default)
"linebreak-style": 0, // disallow mixed 'LF' and 'CRLF' as linebreaks (off by default)
"max-nested-callbacks": 0, // specify the maximum depth callbacks can be nested (off by default)
"new-cap": 0, // require a capital letter for constructors
"new-parens": 0, // disallow the omission of parentheses when invoking a constructor with no arguments
"new-parens": 0, // disallow the omission of parentheses when invoking a constructor with no arguments
"newline-after-var": 0, // allow/disallow an empty newline after var statement (off by default)
"no-array-constructor": 2, // disallow use of the Array constructor
"no-continue": 0, // disallow use of the continue statement (off by default)
"no-inline-comments": 0, // disallow comments inline after code (off by default)
"no-lonely-if": 0, // disallow if as the only statement in an else block (off by default)
"no-mixed-spaces-and-tabs": 2, // disallow mixed spaces and tabs for indentation
"no-multiple-empty-lines": 0, // disallow multiple empty lines (off by default)
"no-nested-ternary": 0, // disallow nested ternary expressions (off by default)
"no-new-object": 2, // disallow use of the Object constructor
"no-spaced-func": 0, // disallow space between function identifier and application
"no-ternary": 0, // disallow the use of ternary operators (off by default)
"no-trailing-spaces": 0, // disallow trailing whitespace at the end of lines
"no-underscore-dangle": 0, // disallow dangling underscores in identifiers
"object-curly-spacing": [2, "always"], // require or disallow padding inside curly braces (off by default)
"one-var": 0, // allow just one var statement per function (off by default)
"operator-assignment": 0, // require assignment operator shorthand where possible or prohibit it entirely (off by default)
"operator-linebreak": 0, // enforce operators to be placed before or after line breaks (off by default)
"padded-blocks": 0, // enforce padding within blocks (off by default)
"quote-props": 0, // require quotes around object literal property names (off by default)
"quotes": 0, // specify whether double or single quotes should be used
"semi-spacing": 0, // enforce spacing before and after semicolons
"semi": [2, "always"], // require or disallow use of semicolons instead of ASI
"sort-vars": 0, // sort variables within the same declaration block (off by default)
"space-after-keywords": 0, // require a space after certain keywords (off by default)
"space-before-blocks": 0, // require or disallow space before blocks (off by default)
"space-before-function-paren": 0, // require or disallow space before function opening parenthesis (off by default)
"space-in-parens": 0, // require or disallow spaces inside parentheses (off by default)
"space-infix-ops": 0, // require spaces around operators
"space-return-throw-case": 0, // require a space after return, throw, and case
"space-unary-ops": 0, // require or disallow spaces before/after unary operators (words on by default, nonwords off by default)
"spaced-comment": 0, // require or disallow a space immediately following the // or /* in a comment (off by default)
"wrap-regex": 0, // require regex literals to be wrapped in parentheses (off by default)
// ECMAScript 6
"constructor-super": 0, // verify super() callings in constructors (off by default)
"generator-star-spacing": 0, // enforce the spacing around the * in generator functions (off by default)
"no-this-before-super": 0, // disallow to use this/super before super() calling in constructors (off by default)
"no-var": 0, // require let or const instead of var (off by default)
"object-shorthand": 0, // require method and property shorthand syntax for object literals (off by default)
"prefer-const": 0, // suggest using of const declaration for variables that are never modified after declared (off by default)
// Legacy
"max-depth": 0, // specify the maximum depth that blocks can be nested (off by default)
"max-len": [1, 120, 2], // specify the maximum length of a line in your program (off by default)
"max-params": 0, // limits the number of parameters that can be used in the function declaration. (off by default)
"max-statements": 0, // specify the maximum number of statement allowed in a function (off by default)
"no-bitwise": 0, // disallow use of bitwise operators (off by default)
"no-plusplus": 0 // disallow use of unary operators, ++ and -- (off by default)
}
}

3
.gitignore vendored
View File

@ -24,4 +24,5 @@ coverage
.grunt .grunt
build/Release build/Release
node_modules node_modules
.lock-wscript .lock-wscript
temp

12
jasmine.json Normal file
View File

@ -0,0 +1,12 @@
{
"spec_dir": "test",
"spec_files": [
"**/*[sS]pec.js"
],
"helpers": [
"../node_modules/babel-register/lib/node.js",
"../node_modules/babel-polyfill/dist/polyfill.js"
],
"stopSpecOnExpectationFailure": false,
"random": false
}

View File

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

View File

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

View File

@ -73,6 +73,7 @@ setTimeout(function(){
module.exports = { module.exports = {
token: Date.now(),
summary:function(){ summary:function(){
var tip = "the default rule for AnyProxy."; var tip = "the default rule for AnyProxy.";
if(!isRootCAFileExists){ if(!isRootCAFileExists){
@ -102,7 +103,10 @@ module.exports = {
if(err){ if(err){
callback(200, {}, "[AnyProxy failed to load local file] " + err); callback(200, {}, "[AnyProxy failed to load local file] " + err);
}else{ }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"), var fs = require("fs"),
path = require("path"), path = require("path"),
mime = require('mime-types'),
color = require('colorful'),
logUtil = require("./log"),
exec = require('child_process').exec; exec = require('child_process').exec;
const changeCase = require('change-case');
// {"Content-Encoding":"gzip"} --> {"content-encoding":"gzip"} // {"Content-Encoding":"gzip"} --> {"content-encoding":"gzip"}
module.exports.lower_keys = function(obj){ module.exports.lower_keys = function(obj){
for(var key in obj){ for(var key in obj){
@ -34,7 +37,7 @@ module.exports.getAnyProxyHome = function(){
if(!fs.existsSync(home)){ if(!fs.existsSync(home)){
try{ try{
fs.mkdirSync(home,0777); fs.mkdirSync(home, '0777');
}catch(e){ }catch(e){
return null; return null;
} }
@ -48,7 +51,7 @@ module.exports.generateCacheDir = function(){
var rand = Math.floor(Math.random() * 1000000), var rand = Math.floor(Math.random() * 1000000),
cachePath = path.join(util.getAnyProxyHome(),"./" + CACHE_DIR_PREFIX + rand); cachePath = path.join(util.getAnyProxyHome(),"./" + CACHE_DIR_PREFIX + rand);
fs.mkdirSync(cachePath,0777); fs.mkdirSync(cachePath, '0777');
return cachePath; return cachePath;
} }
@ -60,7 +63,7 @@ module.exports.clearCacheDir = function(cb){
if(isWin){ if(isWin){
exec("for /D %f in (" + dirNameWildCard + ") do rmdir %f /s /q",{cwd : home},cb); exec("for /D %f in (" + dirNameWildCard + ") do rmdir %f /s /q",{cwd : home},cb);
}else{ }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]); 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;
};

View File

@ -10,6 +10,7 @@
"async": "~0.9.0", "async": "~0.9.0",
"async-task-mgr": ">=1.1.0", "async-task-mgr": ">=1.1.0",
"body-parser": "^1.13.1", "body-parser": "^1.13.1",
"change-case": "^3.0.0",
"colorful": "^2.1.0", "colorful": "^2.1.0",
"commander": "~2.3.0", "commander": "~2.3.0",
"compression": "^1.4.4", "compression": "^1.4.4",
@ -17,19 +18,35 @@
"iconv-lite": "^0.4.6", "iconv-lite": "^0.4.6",
"ip": "^0.3.2", "ip": "^0.3.2",
"juicer": "^0.6.6-stable", "juicer": "^0.6.6-stable",
"mime-types": "2.1.11",
"nedb": "^0.11.0", "nedb": "^0.11.0",
"node-easy-cert": "^1.0.0",
"node-forge": "^0.6.39", "node-forge": "^0.6.39",
"npm": "^2.7.0", "npm": "^2.7.0",
"promise": "^7.0.4", "promise": "^7.0.4",
"qrcode-npm": "0.0.3", "qrcode-npm": "0.0.3",
"stream-throttle": "^0.1.3", "stream-throttle": "^0.1.3",
"ws": "^0.4.32" "ws": "^1.1.0"
}, },
"devDependencies": { "devDependencies": {
"proxy-eval": ">=1.1.2" "babel-polyfill": "^6.13.0",
"babel-preset-es2015": "^6.13.2",
"babel-preset-stage-0": "^6.5.0",
"babel-register": "^6.11.6",
"https-proxy-agent": "^1.0.0",
"jasmine": "^2.4.1",
"koa": "^1.2.1",
"koa-body": "^1.4.0",
"koa-router": "^5.4.0",
"koa-send": "^3.2.0",
"koa-websocket": "^2.0.0",
"nodeunit": "^0.9.1",
"request": "^2.74.0",
"stream-equal": "0.1.8"
}, },
"scripts": { "scripts": {
"test": "sh test.sh" "test": "sh test/test.sh",
"testserver": "node test/server/startServer.js"
}, },
"optionalDependencies": {}, "optionalDependencies": {},
"repository": { "repository": {

View File

@ -11,7 +11,6 @@ var http = require('http'),
color = require('colorful'), color = require('colorful'),
certMgr = require("./lib/certMgr"), certMgr = require("./lib/certMgr"),
getPort = require("./lib/getPort"), getPort = require("./lib/getPort"),
requestHandler = require("./lib/requestHandler"),
Recorder = require("./lib/recorder"), Recorder = require("./lib/recorder"),
logUtil = require("./lib/log"), logUtil = require("./lib/log"),
wsServer = require("./lib/wsServer"), wsServer = require("./lib/wsServer"),
@ -37,7 +36,8 @@ var T_TYPE_HTTP = 0,
DEFAULT_HOST = "localhost", DEFAULT_HOST = "localhost",
DEFAULT_TYPE = T_TYPE_HTTP; DEFAULT_TYPE = T_TYPE_HTTP;
var default_rule = require('./lib/rule_default'); var default_rule = util.freshRequire('./rule_default');
var requestHandler = util.freshRequire('./requestHandler');
//option //option
//option.type : 'http'(default) or 'https' //option.type : 'http'(default) or 'https'
@ -70,22 +70,39 @@ function proxyServer(option){
logUtil.setPrintStatus(false); logUtil.setPrintStatus(false);
} }
// copy the rule to keep the original proxyRules independent
proxyRules = Object.assign({}, proxyRules);
var currentRule = requestHandler.setRules(proxyRules); //TODO : optimize calling for set rule
if(!!option.interceptHttps){ if(!!option.interceptHttps){
default_rule.setInterceptFlag(true); if (!certMgr.isRootCAFileExists()) {
util.showRootInstallTip();
process.exit(0);
return;
}
currentRule.setInterceptFlag(true);
//print a tip when using https features in Node < v0.12 //print a tip when using https features in Node < v0.12
var nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]); var nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
if(nodeVersion < 0.12){ if(nodeVersion < 0.12){
logUtil.printLog(color.red("node >= v0.12 is required when trying to intercept HTTPS requests :("), logUtil.T_ERR); logUtil.printLog(color.red("node >= v0.12 is required when trying to intercept HTTPS requests :("), logUtil.T_ERR);
} }
logUtil.printLog(color.blue("The WebSocket will not work properly in the https intercept mode :("), logUtil.T_TIP);
} }
if(option.throttle){ if(option.throttle){
logUtil.printLog("throttle :" + option.throttle + "kb/s"); logUtil.printLog("throttle :" + option.throttle + "kb/s");
GLOBAL._throttle = new ThrottleGroup({rate: 1024 * parseInt(option.throttle) }); // rate - byte/sec const rate = parseInt(option.throttle);
if (rate < 1) {
logUtil.printLog(color.red('Invalid throttle rate value, should be positive integer\n'), logUtil.T_ERR);
process.exit(0);
}
GLOBAL._throttle = new ThrottleGroup({rate: 1024 * parseFloat(option.throttle) }); // rate - byte/sec
} }
requestHandler.setRules(proxyRules); //TODO : optimize calling for set rule
self.httpProxyServer = null; self.httpProxyServer = null;
async.series( async.series(

28
test/data/headers.js Normal file
View File

@ -0,0 +1,28 @@
/*
* 用于放置所有header信息的测试数据
*
*/
// Get 和 Post共有的header信息
/*eslint max-len: ["off"]*/
const CommonRequestHeader = {
Accept: 'application/json;charset=utf-8,text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Charset': 'utf-8',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'zh-CN',
'Accept-Datetime': 'Thu, 31 May 2007 20:35:00 GMT',
'Authorization': 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Cookie': 'testCookie1=cookie1; testCookie2=cookie2',
'Content-Type': 'application/x-www-form-urlencoded',
'Date': 'Tue, 15 Nov 1994 08:12:31 GMT',
'Origin': 'http://localhost',
'Pragma': 'no-cache',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36'
};
module.exports = {
CommonRequestHeader
};

3
test/data/test.css Normal file
View File

@ -0,0 +1,3 @@
.test {
display: block;
}

BIN
test/data/test.eot Executable file

Binary file not shown.

3
test/data/test.js Normal file
View File

@ -0,0 +1,3 @@
function test () {
console.info('This is nothing but a js file, to test the js download');
}

3
test/data/test.json Normal file
View File

@ -0,0 +1,3 @@
{
'testkey': 'this is just a normal json file'
}

BIN
test/data/test.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

14
test/data/test.svg Executable file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Copyright (C) 2016 by original authors @ fontello.com</metadata>
<defs>
<font id="fontello" horiz-adv-x="1000" >
<font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
<missing-glyph horiz-adv-x="1000" />
<glyph glyph-name="glass" unicode="&#xe800;" d="M948 746q0-19-24-43l-353-353v-429h179q15 0 25-10t11-25-11-25-25-11h-500q-14 0-25 11t-11 25 11 25 25 10h179v429l-353 353q-24 24-24 43 0 13 10 21t21 9 24 3h786q13 0 24-3t21-9 10-21z" horiz-adv-x="1000" />
<glyph glyph-name="music" unicode="&#xe801;" d="M857 725v-625q0-28-19-50t-48-33-58-18-53-6-54 6-58 18-48 33-19 50 19 50 48 33 58 18 54 6q58 0 107-22v300l-429-132v-396q0-28-19-50t-48-33-58-18-53-6-54 6-58 18-48 33-19 50 19 50 48 34 58 17 54 6q58 0 107-21v539q0 17 10 32t28 20l464 142q7 3 16 3 22 0 38-16t15-38z" horiz-adv-x="857.1" />
</font>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
test/data/test.ttf Executable file

Binary file not shown.

BIN
test/data/test.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

BIN
test/data/test.woff Executable file

Binary file not shown.

BIN
test/data/test.woff2 Executable file

Binary file not shown.

459
test/no_rule_spec.js Normal file
View File

@ -0,0 +1,459 @@
const http = require('http');
const querystring = require('querystring');
const path = require('path');
const fs = require('fs');
const Buffer = require('buffer').Buffer;
const Server = require('./server/server.js');
const
{
proxyGet,
proxyPost,
directGet,
directPost,
directUpload,
proxyUpload,
generateUrl,
proxyPut,
directPut,
proxyDelete,
directDelete,
directHead,
proxyHead,
directOptions,
proxyOptions,
proxyPutUpload,
directPutUpload
} = require('./util/HttpUtil.js');
const { CommonRequestHeader } = require('./data/headers.js');
const { isCommonResHeaderEqual, isCommonReqEqual, printLog } = require('./util/CommonUtil.js');
const color = require('colorful');
const streamEqual = require('stream-equal');
const WebSocket = require('ws');
const ProxyServerUtil = require('./util/ProxyServerUtil.js');
const wsHost = 'ws://localhost:3000/test/socket';
testRequest('http');
testRequest('https');
// Test suites for http and https request
function testRequest(protocol = 'http') {
function constructUrl(urlPath) {
return generateUrl(protocol, urlPath);
}
describe('Test request without proxy rules in protocol ' + protocol, () => {
let proxyServer ;
let serverInstance;
beforeAll((done) => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 200000;
printLog('Start server for no_rule_spec');
serverInstance = new Server();
proxyServer = ProxyServerUtil.defaultProxyServer();
setTimeout(function() {
done();
}, 2000);
});
afterAll(() => {
serverInstance && serverInstance.close();
proxyServer && proxyServer.close();
printLog('Closed server for no_rule_spec');
});
it('Get should work as direct without proxy rules', (done) => {
const url = constructUrl('/test');
const getParam = {
param: 'nothing'
};
proxyGet(url, getParam, CommonRequestHeader).then((proxyRes) => {
directGet(url, getParam, CommonRequestHeader).then(directRes => {
expect(proxyRes.statusCode).toEqual(200);
expect(isCommonResHeaderEqual(directRes.headers, proxyRes.headers, url)).toBe(true);
expect(isCommonReqEqual(url, serverInstance)).toBe(true);
expect(proxyRes.statusCode).toEqual(directRes.statusCode);
expect(directRes.body).toEqual(proxyRes.body);
done();
}, error => {
console.error('error happend in direct get:', error);
done.fail('error happend in direct get');
});
}, error => {
console.log('error happened in proxy get:', error);
done.fail('error happend in proxy get');
});
});
it('Post should work as direct without proxy rules', (done) => {
const url = constructUrl('/test/getuser');
const param = {
param: 'postnothing'
};
proxyPost(url, param, CommonRequestHeader).then(proxyRes => {
directPost(url, param, CommonRequestHeader).then(directRes => {
expect(proxyRes.statusCode).toEqual(200);
expect(isCommonResHeaderEqual(directRes.headers, proxyRes.headers, url)).toBe(true);
expect(proxyRes.statusCode).toEqual(directRes.statusCode);
expect(directRes.body).toEqual(proxyRes.body);
expect(isCommonReqEqual(url, serverInstance)).toBe(true);
done();
}, error => {
console.error('error in direct post:', error);
done.fail('error happend in direct post');
});
}, error => {
console.log('error happened in proxy post,', error);
done.fail('error happend in proxy post');
});
});
it('PUT should work as direct without proxy rules', done => {
const url = constructUrl('/test/put');
const param = {
param: 'putsomething'
};
proxyPut(url, param, CommonRequestHeader).then(proxyRes => {
directPut(url, param, CommonRequestHeader).then(directRes => {
expect(directRes.statusCode).toEqual(200);
expect(isCommonResHeaderEqual(directRes.headers, proxyRes.headers, url)).toBe(true);
expect(directRes.statusCode).toEqual(proxyRes.statusCode);
expect(directRes.body).toEqual(proxyRes.body);
expect(isCommonReqEqual(url, serverInstance)).toBe(true);
done();
}, error => {
console.error('error happened in direct put', error);
done.fail('error happened in direct put');
});
}, error => {
console.error('error happened in proxy put', error);
done.fail('error happened in proxy put');
});
});
it('DELETE rquest should work as direct without proxy rules', (done) => {
const url = constructUrl('/test/delete/123456');
proxyDelete(url, {}, CommonRequestHeader).then(proxyRes => {
directDelete(url, {}, CommonRequestHeader).then(directRes => {
expect(directRes.statusCode).toEqual(200);
expect(directRes.statusCode).toEqual(proxyRes.statusCode);
expect(isCommonResHeaderEqual(directRes.headers, proxyRes.headers, url)).toBe(true);
expect(directRes.body).toEqual(proxyRes.body);
expect(isCommonReqEqual(url, serverInstance)).toBe(true);
done();
}, error => {
console.error('error happened in direct delete :', error);
done.fail('error happened in direct delete');
});
}, error => {
console.error('error happened in proxy delete :', error);
done.fail('error happened in proxy delete');
});
});
it('HEAD request should work as direct without proxy rules', (done) => {
const url = constructUrl('/test/head');
proxyHead(url, CommonRequestHeader)
.then(proxyRes => {
directHead(url, CommonRequestHeader)
.then(directRes => {
expect(directRes.statusCode).toEqual(200);
expect(directRes.body).toEqual('');
expect(directRes.statusCode).toEqual(proxyRes.statusCode);
expect(isCommonResHeaderEqual(directRes.headers, proxyRes.headers, url)).toBe(true);
expect(directRes.body).toEqual(proxyRes.body);
expect(isCommonReqEqual(url, serverInstance)).toBe(true);
done();
}, error => {
console.error('error happened in direct head request:', error);
done.fail('error happened in direct head request');
});
}, error => {
console.error('error happened in proxy head request:', error);
done.fail('error happened in proxy head request');
});
});
it('OPTIONS request should work as direct without proxy rules', (done) => {
const url = constructUrl('/test/options');
proxyOptions(url, CommonRequestHeader)
.then(proxyRes => {
directOptions(url, CommonRequestHeader)
.then(directRes => {
expect(directRes.statusCode).toEqual(200);
expect(directRes.body).toEqual('could_be_empty');
expect(directRes.statusCode).toEqual(proxyRes.statusCode);
expect(isCommonResHeaderEqual(directRes.headers, proxyRes.headers, url)).toBe(true);
expect(directRes.body).toEqual(proxyRes.body);
expect(isCommonReqEqual(url, serverInstance)).toBe(true);
done();
}, error => {
console.error('error happened in direct options request:', error);
done.fail('error happened in direct options request');
});
}, error => {
console.error('error happened in proxy options request:', error);
done.fail('error happened in proxy options request');
});
});
describe('Response code should be honored as direct without proxy rules', () => {
[301, 302, 303].forEach(code => {
testRedirect(code);
});
function testRedirect (redirectCode) {
it(`${redirectCode} response should work as direct without proxy rules`, (done) => {
const url = constructUrl(`/test/response/${redirectCode}`);
proxyGet(url)
.then(proxyRes => {
directGet(url)
.then(directRes => {
const redirects = directRes.request._redirect.redirects || [];
const proxyRedirects = proxyRes.request._redirect.redirects || [];
expect(redirects.length).toEqual(1);
expect(proxyRedirects.length).toEqual(1);
expect(redirects[0].statusCode).toEqual(redirectCode);
expect(redirects[0].redirectUri).toEqual(proxyRedirects[0].redirectUri);
expect(redirects[0].statusCode).toEqual(proxyRedirects[0].statusCode);
if (protocol === 'https') {
expect(redirects[0].redirectUri).toEqual('https://localhost:3001/test');
} else {
expect(redirects[0].redirectUri).toEqual('http://localhost:3000/test');
}
done();
}, error => {
console.log(`error happened in direct ${redirectCode}:`, error);
done.fail(`error happened in direct ${redirectCode}`);
});
}, error => {
console.log(`error happened in proxy ${redirectCode}:`, error);
done.fail(`error happened in proxy ${redirectCode}`);
});
});
}
});
describe('Test file download ', () => {
const testArray = [
{
url: constructUrl('/test/download/png'),
type: 'png',
contentType: 'image/png'
},
{
url: constructUrl('/test/download/webp'),
type: 'WEBP',
contentType: 'image/webp'
},
{
url: constructUrl('/test/download/json'),
type: 'JSON',
contentType: 'application/json; charset=utf-8'
},
{
url: constructUrl('/test/download/css'),
type: 'CSS',
contentType: 'text/css; charset=utf-8'
},
{
url: constructUrl('/test/download/ttf'),
type: 'TTF',
contentType: 'application/x-font-ttf'
},
{
url: constructUrl('/test/download/eot'),
type: 'EOT',
contentType: 'application/vnd.ms-fontobject'
},
{
url: constructUrl('/test/download/svg'),
type: 'SVG',
contentType: 'image/svg+xml'
},
{
url: constructUrl('/test/download/woff'),
type: 'WOFF',
contentType: 'application/font-woff'
},
{
url: constructUrl('/test/download/woff2'),
type: 'WOFF2',
contentType: 'application/font-woff2'
}
];
testArray.forEach(item => {
testFileDownload(item.url, item.type, item.contentType);
});
// 封装测试文件下载的测试工具类
function testFileDownload (url, filetype, contentType) {
const describe = `${filetype} file download without rules should be work as direct download`;
const param = {};
it(describe, (done) => {
proxyGet(url, param).then(proxyRes => {
directGet(url, param).then(directRes => {
expect(proxyRes.statusCode).toEqual(200);
expect(isCommonResHeaderEqual(directRes.headers, proxyRes.headers, url)).toBe(true);
expect(proxyRes.statusCode).toEqual(directRes.statusCode);
expect(proxyRes.body).toEqual(directRes.body);
expect(isCommonReqEqual(url, serverInstance)).toBe(true);
done();
}, error => {
console.error('error in direct get :', filetype, error);
done.fail(`error happend in direct get ${filetype}`);
});
}, error => {
console.error('error in proxy get :', filetype, error);
done.fail(`error happend in proxy get ${filetype}`);
});
});
}
});
describe('Test file upload', () => {
const formParams = {
param1: 'param_1',
param2: 'param2'
};
it('POST upload should be working', (done) => {
const url = constructUrl('/test/upload/png');
const filePath = path.resolve('./test/data/test.png');
proxyUpload(url, filePath, formParams)
.then(proxyRes => {
directUpload(url, filePath, formParams)
.then((directRes) => {
expect(isCommonResHeaderEqual(directRes.headers, proxyRes.headers, url)).toBe(true);
expect(isCommonReqEqual(url, serverInstance)).toBe(true);
assertReponse(proxyRes, directRes, filePath, done);
}, error => {
console.error('error in direct upload:', error);
done.fail('error in direct upload');
});
}, error => {
console.error('error in proxy upload:', error);
done.fail('error in proxy upload:');
});
});
it('PUT upload should be working', (done) => {
const url = constructUrl('/test/upload/putpng');
const filePath = path.resolve('./test/data/test.png');
proxyPutUpload(url, filePath, formParams)
.then(proxyRes => {
directPutUpload(url, filePath, formParams)
.then((directRes) => {
expect(isCommonResHeaderEqual(directRes.headers, proxyRes.headers, url)).toBe(true);
assertReponse(proxyRes, directRes, filePath, done);
}, error => {
console.error('error in direct upload:', error);
done.fail('error in direct upload');
});
}, error => {
console.error('error in proxy upload:', error);
done.fail('error in proxy upload:');
});
});
function assertReponse (proxyRes, directRes, originFilePath, done) {
expect(proxyRes.statusCode).toEqual(200);
expect(proxyRes.statusCode).toEqual(directRes.statusCode);
// expect(proxyRes.headers.reqbody).toEqual(directRes.headers.reqbody);
// the body will be the file path
const directUploadedStream = fs.createReadStream(directRes.body);
const proxyUploadedStream = fs.createReadStream(proxyRes.body);
const localFileStream = fs.createReadStream(originFilePath);
streamEqual(directUploadedStream, localFileStream)
.then(isLocalEqual => {
expect(isLocalEqual).toBe(true);
streamEqual(directUploadedStream, proxyUploadedStream)
.then(isUploadedEqual => {
expect(isUploadedEqual).toBe(true);
done();
}, error => {
console.error('error in comparing directUpload with proxy:\n',error);
done.fail('error in comparing directUpload with proxy');
});
done();
}, error => {
console.error('error in comparing directUpload with local:\n',error);
done.fail('error in comparing directUpload with local');
});
}
});
// describe('Test Big file download', () => {
// // const url = '/test/download/bigfile';
// const url = 'http://yunpan.alibaba-inc.com/downloadService.do?token=pZWiXMXUguIUQDvR098qnUVqVAWhNVY6';
// const contentType = 'application/octet-stream';
// const param = {};
// it('BIG file downlaod should be working', (done) => {
// directGet(url, param, CommonRequestHeader).then(proxyRes => {
// console.info('proxyRes body:', proxyRes.body);
// directGet(url, param, CommonRequestHeader).then(directRes => {
// expect(proxyRes.statusCode).toEqual(200);
// expect(proxyRes.headers['content-type']).toEqual(contentType);
// expect(proxyRes.statusCode).toEqual(directRes.statusCode);
// expect(proxyRes.headers['content-type']).toEqual(directRes.headers['content-type']);
// expect(proxyRes.body).toEqual(directRes.body);
// done();
// }, error => {
// console.error('error in direct get bigfile :', error);
// done.fail(`error happend in direct get bigfile`);
// });
// }, error => {
// console.error('error in proxy get bigfile :', error);
// done.fail(`error happend in proxy get bigfile`);
// });
// });
// });
});
}

View File

@ -0,0 +1,144 @@
/*
* Test suites for WebSocket.
* ONLY TO ENSURE THE REQUEST WILL BE BYPASSED SUCCESSFULLY, WE HAVEN'T SUPPORTTED WEBSOCKET YET.
*
*/
const ProxyServerUtil = require('./util/ProxyServerUtil.js');
const { generateWsUrl, directWs, proxyWs } = require('./util/HttpUtil.js');
const Server = require('./server/server.js');
const { printLog } = require('./util/CommonUtil.js');
const wsHost = 'ws://localhost:3000/test/socket';
testWebsocket('ws');
testWebsocket('wss');
function testWebsocket(protocol) {
describe('Test WebSocket in protocol : ' + protocol , () =>{
const url = generateWsUrl(protocol, '/test/socket');
let serverInstance ;
let proxyServer ;
beforeAll((done) => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 200000;
printLog('Start server for no_rule_websocket_spec');
serverInstance = new Server();
proxyServer = ProxyServerUtil.proxyServerWithoutHttpsIntercept();
setTimeout(function() {
done();
}, 2000);
});
afterAll(() => {
serverInstance && serverInstance.close();
proxyServer && proxyServer.close();
printLog('Closed server for no_rule_websocket_spec');
});
it('Default websocket option', done => {
const sendMessage = 'Send the message with default option';
let directMessage ; // set the flag for direct message, compare when both direct and proxy got message
let proxyMessage;
const ws = directWs(url);
const porxyWsRef = proxyWs(url);
ws.on('open', () => {
ws.send(sendMessage);
});
porxyWsRef.on('open', () => {
porxyWsRef.send(sendMessage);
});
ws.on('message', (data, flag) => {
const message = JSON.parse(data);
if (message.type === 'onMessage') {
directMessage = message.content;
compareMessageIfReady();
}
});
porxyWsRef.on('message', (data, flag) => {
const message = JSON.parse(data);
if (message.type === 'onMessage') {
proxyMessage = message.content;
compareMessageIfReady();
}
});
ws.on('error', error => {
console.error('error happened in direct websocket:', error);
done.fail('Error happened in direct websocket');
});
porxyWsRef.on('error', error => {
console.error('error happened in proxy websocket:', error);
done.fail('Error happened in proxy websocket');
});
function compareMessageIfReady () {
if (directMessage && proxyMessage) {
expect(directMessage).toEqual(proxyMessage);
expect(directMessage).toEqual(sendMessage);
done();
}
}
});
it('masked:true', done => {
const sendMessage = 'Send the message with option masked:true';
let directMessage ; // set the flag for direct message, compare when both direct and proxy got message
let proxyMessage;
const ws = directWs(url);
const porxyWsRef = proxyWs(url);
ws.on('open', () => {
ws.send(sendMessage, { masked: true });
});
porxyWsRef.on('open', () => {
porxyWsRef.send(sendMessage, { masked: true });
});
ws.on('message', (data, flag) => {
const message = JSON.parse(data);
if (message.type === 'onMessage') {
directMessage = message.content;
compareMessageIfReady();
}
});
porxyWsRef.on('message', (data, flag) => {
const message = JSON.parse(data);
if (message.type === 'onMessage') {
proxyMessage = message.content;
compareMessageIfReady();
}
});
ws.on('error', error => {
console.error('error happened in direct websocket:', error);
done.fail('Error happened in direct websocket');
});
porxyWsRef.on('error', error => {
console.error('error happened in proxy websocket:', error);
done.fail('Error happened in proxy websocket');
});
function compareMessageIfReady () {
if (directMessage && proxyMessage) {
expect(directMessage).toEqual(proxyMessage);
expect(directMessage).toEqual(sendMessage);
done();
}
}
});
});
}

View File

@ -0,0 +1,57 @@
/*
* test for rule shouldUseLocal
*
*/
const ProxyServerUtil = require('./util/ProxyServerUtil.js');
const { proxyGet, generateUrl } = require('./util/HttpUtil.js');
const Server = require('./server/server.js');
const { printLog } = require('./util/CommonUtil.js');
const rule = require('./test_rules/shouldUseLocalResponseRule.js');
const expectedLocalBody = 'handled_in_local_response';
testWrapper('http');
testWrapper('https');
function testWrapper(protocol, ) {
describe('Rule shouldUseLocalResponse should be working in :' + protocol, () => {
let proxyServer ;
let serverInstance ;
beforeAll((done) => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000000;
printLog('Start server for rule_shouldUseLocalResponse_spec');
serverInstance = new Server();
proxyServer = ProxyServerUtil.proxyServerWithRule(rule);
setTimeout(function() {
done();
}, 2000);
});
afterAll(() => {
serverInstance && serverInstance.close();
proxyServer && proxyServer.close();
printLog('Close server for rule_shouldUseLocalResponse_spec');
});
it('Should use local response if the assertion is true', done => {
const url = generateUrl(protocol, '/test/uselocal');
proxyGet(url, {})
.then(res => {
expect(res.body).toEqual(expectedLocalBody);
expect(res.headers['via-proxy-local']).toEqual('true');
done();
}, error => {
console.log('error happened in proxy get for shouldUseLocal: ',error);
done.fail('error happened when test shouldUseLocal rule');
});
});
});
}

274
test/server/server.js Normal file
View File

@ -0,0 +1,274 @@
const Koa = require('koa');
const KoaRouter = require('koa-router');
const koaBody = require('koa-body');
const send = require('koa-send');
const path = require('path');
const https = require('https');
const certMgr = require("../../lib/certMgr");
const fs = require('fs');
const websocket = require('koa-websocket');
const wsRouter = require('koa-router')();
const color = require('colorful');
const WebSocketServer = require('ws').Server;
const DEFAULT_PORT = 3000;
const HTTPS_PORT = 3001;
const UPLOAD_DIR = './test/temp';
const PROXY_KEY_PREFIX = 'proxy-';
function KoaServer() {
this.httpServer = null;
this.httpsServer = null;
this.requestRecordMap = {}; // store all request data to the map
const self = this;
/**
* log the request info, write as
*/
this.logRequest = function* (next) {
const headers = this.request.headers;
let key = this.request.host + this.request.url;
// take proxy data with 'proxy-' + url
if (headers['via-proxy'] === 'true') {
key = PROXY_KEY_PREFIX + key;
}
let body = this.request.body;
body = typeof body === 'object' ? JSON.stringify(body) : body;
self.requestRecordMap[key] = {
headers: headers,
body: body
};
yield next;
};
this.start();
}
KoaServer.prototype.constructRouter = function() {
const router = KoaRouter();
router.post('/test/getuser', this.logRequest, koaBody(), function*(next) {
printLog('requesting post /test/getuser');
this.response.set('reqbody', JSON.stringify(this.request.body));
this.response.body = 'something in post';
});
router.get('/test', this.logRequest, function*(next) {
printLog('request in get: ' + JSON.stringify(this.request));
this.response.body = 'something';
this.response.__req = this.request;
printLog('response in get:' + JSON.stringify(this.response));
});
router.get('/test/uselocal', this.logRequest, function*(next) {
printLog('request in get local:' + JSON.stringify(this.request));
this.response.body = 'something should be in local';
this.response.__req = this.request;
printLog('response in get:' + JSON.stringify(this.response));
});
['png', 'webp', 'json', 'js', 'css', 'ttf', 'eot', 'svg', 'woff', 'woff2'].forEach(item => {
router.get(`/test/download/${item}`, this.logRequest, function* (next) {
printLog(`now downloading the ${item}`);
yield send(this, `test/data/test.${item}`);
});
});
router.get('/test/response/303', function*(next) {
printLog('now to redirect 303');
this.redirect('/test');
this.status = 303;
});
router.get('/test/response/302', function*(next) {
printLog('now to redirect 302');
this.redirect('/test');
});
router.get('/test/response/301', function*(next) {
printLog('now to redirect permanently');
this.redirect('/test');
this.status = 301;
});
const onFileBegin = function(name, file) {
if (!fs.existsSync('./test/temp')) {
try {
fs.mkdirSync('./test/temp', '0777');
} catch (e) {
return null;
}
}
file.name = 'test_upload_' + Date.now() + '.png';
var folder = path.dirname(file.path);
file.path = path.join(folder, file.name);
};
router.post('/test/upload/png',
this.logRequest,
koaBody({
multipart: true,
formidable: {
uploadDir: UPLOAD_DIR,
onFileBegin: onFileBegin
}
}),
function*(next) {
const file = this.request.body.files.file;
this.response.set('reqbody', JSON.stringify(this.request.body.fields));
this.response.body = file.path;
}
);
router.put('/test/upload/putpng',
this.logRequest,
koaBody({
multipart: true,
formidable: {
uploadDir: UPLOAD_DIR,
onFileBegin: onFileBegin
}
}),
function*(next) {
const file = this.request.body.files.file;
this.response.body = file.path;
}
);
router.put('/test/put', koaBody(), this.logRequest, function*(next) {
printLog('requesting put /test/put' + JSON.stringify(this.request));
this.response.body = 'something in put';
});
router.delete('/test/delete/:id', this.logRequest, function*(next) {
printLog('requesting delete /test/delete/:id'+ JSON.stringify(this.params));
this.response.body = 'something in delete';
});
router.head('/test/head', this.logRequest, function*(next) {
printLog('requesting head /test/head');
this.response.body = ''; // the body will not be passed to response, in HEAD request
this.response.set('reqBody', 'head_request_contains_no_resbody');
});
router.options('/test/options', this.logRequest, function*(next) {
printLog('requesting options /test/options');
this.response.body = 'could_be_empty';
this.response.set('Allow', 'GET, HEAD, POST, OPTIONS');
});
// router.connect('/test/connect', function *(next) {
// printLog('requesting connect /test/connect');
// this.response.body = 'connect_established_body';
// });
return router;
};
KoaServer.prototype.constructWsRouter = function() {
const wsRouter = KoaRouter();
const self = this;
wsRouter.get('/test/socket', function*(next) {
const ws = this.websocket;
const messageObj = {
type: 'initial',
content: 'default message'
};
ws.send(JSON.stringify(messageObj));
ws.on('message', message => {
printLog('message from request socket: ' + message);
self.handleRecievedMessage(ws, message);
});
yield next;
});
return wsRouter;
};
KoaServer.prototype.getRequestRecord = function (key) {
return this.requestRecordMap[key] || {};
};
KoaServer.prototype.getProxyRequestRecord = function (key) {
key = PROXY_KEY_PREFIX + key;
return this.requestRecordMap[key] || {};
};
KoaServer.prototype.handleRecievedMessage = function(ws, message) {
const newMessage = {
type: 'onMessage',
content: message
};
ws.send(JSON.stringify(newMessage));
};
KoaServer.prototype.start = function() {
printLog('Starting the server...');
const router = this.constructRouter();
const wsRouter = this.constructWsRouter();
const self = this;
const app = Koa();
websocket(app);
app.use(router.routes());
app.ws.use(wsRouter.routes());
this.httpServer = app.listen(DEFAULT_PORT);
printLog('HTTP is now listening on port :' + DEFAULT_PORT);
certMgr.getCertificate('localhost', function(error, keyContent, crtContent) {
if (error) {
console.error('failed to create https server:', error);
} else {
self.httpsServer = https.createServer({
key: keyContent,
cert: crtContent
}, app.callback());
// create wss server
const wss = new WebSocketServer({
server: self.httpsServer
});
wss.on('connection', function connection(ws) {
ws.on('message', function incoming(message) {
printLog('received in wss: ' + message);
self.handleRecievedMessage(ws, message);
});
});
wss.on('error', error => {
console.error('erro happened in wss:%s', error);
});
self.httpsServer.listen(HTTPS_PORT);
printLog('HTTPS is now listening on port :' + HTTPS_PORT);
printLog('Server started successfully');
}
});
return this;
};
KoaServer.prototype.close = function() {
printLog('Closing server now...');
this.httpServer && this.httpServer.close();
this.httpsServer && this.httpsServer.close();
this.requestRecordMap = {};
printLog('Server closed successfully');
};
function printLog(content) {
console.log(color.cyan('===SERVER LOG===' + content));
}
module.exports = KoaServer;

View File

@ -0,0 +1,3 @@
const Server = require('./server.js');
new Server();

View File

@ -1,5 +1,8 @@
#!/bin/bash #!/bin/bash
echo "nodeunit is required to run these test cases" echo "Begin to run the test suites, JASMINE is required.\n"
echo "Removing test temp directory before running"
rm -rf ./test/temp/*
echo "Removing done, test cases now running"
node -v node -v
nodeunit test.js jasmine JASMINE_CONFIG_PATH=./jasmine.json

View File

@ -0,0 +1,22 @@
/*
* Rule defination for shouldUseLocalResponse
*
*/
const dealLocalBody = 'handled_in_local_response';
module.exports = {
shouldUseLocalResponse: function (req, reqBody) {
return req.url.indexOf('uselocal') > -1;
},
shouldInterceptHttpsReq: function () {
return true;
},
dealLocalResponse: function (req, reqBody, callback) {
const header = {
'Via-Proxy-Local': 'true'
};
callback(200, header, dealLocalBody);
}
};

153
test/util/CommonUtil.js Normal file
View File

@ -0,0 +1,153 @@
/**
*
* The utility class for test
*/
const zlib = require('zlib');
const color = require('colorful');
/*
* Compare whether tow object are equal
*/
function isObjectEqual (source = {} , target = {}, url = '') {
source = Object.assign({}, source);
target = Object.assign({}, target);
let isEqual = true;
for(const key in source) {
isEqual = isEqual && source[key] === target[key];
if (!isEqual) {
console.info('source object :', source);
console.info('target object :', target);
printError(`different key in isObjectEqual is: "${key}", source is "${source[key]}",
target is "${target[key]}" the url is ${url}`);
break;
}
delete source[key];
delete target[key];
}
for(const key in target) {
isEqual = isEqual && source[key] === target[key];
if (!isEqual) {
console.info('source object :', source);
console.info('target object :', target);
printError(`different key in isObjectEqual is: "${key}", source is "${source[key]}",
target is "${target[key]}" the url is ${url}`);
break;
}
delete source[key];
delete target[key];
}
return isEqual;
}
/*
* Compare the header between direct with proxy
* Will exclude the header(s) which modified by proxy
*/
function isCommonResHeaderEqual (directHeaders, proxyHeaders, requestUrl) {
directHeaders = Object.assign({}, directHeaders);
proxyHeaders = Object.assign({}, proxyHeaders);
let isEqual = true;
const mustEqualFileds = []; // the fileds that have to be equal, or the assert will be failed
if (!/gzip/i.test(directHeaders['content-encoding'])) {
// if the content is gzipped, proxy will unzip and remove the header
mustEqualFileds.push('content-encoding');
mustEqualFileds.push('content-length');
}
mustEqualFileds.push('content-type');
mustEqualFileds.push('cache-control');
mustEqualFileds.push('allow');
// ensure the required fileds are same
mustEqualFileds.forEach(filedName => {
isEqual = directHeaders[filedName] === proxyHeaders[filedName];
delete directHeaders[filedName];
delete proxyHeaders[filedName];
});
// remained filed are good to be same, but are allowed to be different
// will warn out those different fileds
for (const key in directHeaders) {
if (directHeaders[key] !== proxyHeaders[key]) {
printWarn(`key "${key}" of two response headers are different in request "${requestUrl}" :
direct is: "${directHeaders[key]}", proxy is: "${proxyHeaders[key]}"`);
}
continue;
}
return isEqual;
}
/*
* Compare the request between direct with proxy
*
*/
function isCommonReqEqual(url, serverInstance) {
try{
url = url.replace('https://', '').replace('http://', ''); // only the remained path is required
let isEqual = true;
const directReqObj = serverInstance.getRequestRecord(url);
const proxyReqObj = serverInstance.getProxyRequestRecord(url);
// ensure the proxy header is correct
isEqual = isEqual && proxyReqObj.headers['via-proxy'] === 'true';
delete proxyReqObj.headers['via-proxy'];
// exclued accept-encoding from comparing, since the proxy will remove it before sending it out
delete directReqObj.headers['accept-encoding'];
// per undefined header, proxy will set it with 0, and an empty request body
if (typeof directReqObj.headers['content-length'] === 'undefined') {
directReqObj.headers['content-length'] = "0";
}
directReqObj.headers['content-type'] = trimFormContentType(directReqObj.headers['content-type']);
proxyReqObj.headers['content-type'] = trimFormContentType(proxyReqObj.headers['content-type']);
isEqual = isEqual && directReqObj.url === proxyReqObj.url;
isEqual = isEqual && isObjectEqual(directReqObj.headers, proxyReqObj.headers, url);
isEqual = isEqual && directReqObj.body === proxyReqObj.body;
return isEqual;
} catch (e) {
console.error(e);
}
}
/*
* for multipart-form, the boundary will be different with each update, we trim it here
*/
function trimFormContentType (contentType = '') {
return contentType.replace(/boundary.*/, '');
}
function printLog (content) {
console.log(color.blue('==LOG==: ' + content));
}
function printWarn(content) {
console.log(color.magenta('==WARN==: ' + content));
}
function printError(content) {
console.log(color.red('==ERROR==: ' + content));
}
module.exports = {
isObjectEqual,
isCommonResHeaderEqual,
printLog,
printWarn,
printError,
isCommonReqEqual
};

262
test/util/HttpUtil.js Normal file
View File

@ -0,0 +1,262 @@
/**
* An util to make the request out
*
*/
const querystring = require('querystring');
const http = require('http');
const zlib = require('zlib');
const Buffer = require('buffer').Buffer;
const request = require('request');
const fs = require('fs');
const WebSocket = require('ws');
const HttpsProxyAgent = require('https-proxy-agent');
const DEFAULT_HOST = 'localhost';
const PROXY_HOST = 'http://localhost:8001';
const SOCKET_PROXY_HOST = 'http://localhost:8001';
const HTTP_SERVER_BASE = 'http://localhost:3000';
const HTTPS_SERVER_BASE = 'https://localhost:3001';
const WS_SERVER_BASE = 'ws://localhost:3000';
const WSS_SERVER_BASE = 'wss://localhost:3001';
const DEFAULT_PROXY_OPTIONS = {
port: 8001, // proxy的端口
method: 'GET',
host: 'localhost'
};
const DEFAULT_OPTIONS = {
};
function getHostFromUrl (url = '') {
const hostReg = /^(https{0,1}:\/\/)(\w+)/;
const match = url.match(hostReg);
return match && match[2] ? match[2] : '';
}
function getPortFromUrl (url = '') {
const portReg = /^https{0,1}:\/\/\w+(:(\d+)){0,1}/;
const match = url.match(portReg);
let port = match && match[2] ? match[2] : '';
if (!port) {
port = url.indexOf('https://') === 0 ? 443 : 80;
}
return port;
}
/**
* 获取url中的path
*/
function getPathFromUrl (url = '') {
const pathReg = /^https{0,1}:\/\/\w+(:\d+){0,1}(.+)/;
const match = url.match(pathReg);
const path = match && match[3] ? match[2] : url;
return path;
}
function proxyRequest (method = 'GET', url, params, headers = {}) {
return doRequest(method, url, params, headers, true);
}
/*
* 直接请求到真实服务器不经过代理服务器
*
*/
function directRequest (method = 'GET', url, params, headers = {}) {
return doRequest(method, url, params, headers);
}
function directUpload (url, filepath, formParams = {}, headers = {}) {
return doUpload(url, 'POST', filepath, formParams, headers);
}
function proxyUpload (url, filepath, formParams = {}, headers = {}) {
return doUpload(url, 'POST', filepath, formParams, headers, true);
}
function directPutUpload (url, filepath, formParams = {}, headers = {}) {
return doUpload(url, 'PUT', filepath, formParams, headers);
}
function proxyPutUpload (url, filepath, headers = {}) {
return doUpload(url, 'PUT', filepath, headers, true);
}
function doRequest (method = 'GET', url, params, headers = {}, isProxy) {
headers = Object.assign({}, headers);
const requestData = {
method: method,
form: params,
url: url,
headers: headers,
rejectUnauthorized: false
};
if (isProxy) {
requestData.proxy = PROXY_HOST;
requestData.headers['via-proxy'] = 'true';
}
const requestTask = new Promise((resolve, reject) => {
request(
requestData,
function (error, response, body) {
if (error) {
reject(error);
return;
}
resolve(response);
}
);
});
return requestTask;
}
function doUpload (url, method, filepath, formParams, headers = {}, isProxy) {
let formData = {
file: fs.createReadStream(filepath)
};
formData = Object.assign({}, formData, formParams);
headers = Object.assign({}, headers);
const requestData = {
formData: formData,
url: url,
method: method,
headers: headers,
json: true,
rejectUnauthorized: false
};
if (isProxy) {
requestData.proxy = PROXY_HOST;
requestData.headers['via-proxy'] = 'true';
}
const requestTask = new Promise((resolve, reject) => {
request(
requestData,
function (error, response, body) {
if (error) {
reject(error);
return;
}
resolve(response);
}
);
});
return requestTask;
}
function doWebSocket(url, isProxy) {
let ws;
if (isProxy) {
const agent = new HttpsProxyAgent(SOCKET_PROXY_HOST);
ws = new WebSocket(url, {
agent: agent,
rejectUnauthorized: false
});
} else {
ws = new WebSocket(url, {
rejectUnauthorized: false
});
}
return ws;
}
function proxyGet (url, params, headers = {}) {
return proxyRequest('GET', url, params, headers);
}
function proxyPost (url, params, headers = {}) {
return proxyRequest('POST', url, params, headers);
}
function proxyPut (url, params, headers = {}) {
return proxyRequest('PUT', url, params, headers);
}
function proxyDelete (url, params, headers = {}) {
return proxyRequest('DELETE', url, params, headers);
}
function proxyHead(url, headers = {}) {
return proxyRequest('HEAD', url, {}, headers);
}
function proxyOptions(url, headers = {}) {
return proxyRequest('OPTIONS', url, {}, headers);
}
function directGet (url, params, headers = {}) {
return directRequest('GET', url, params, headers);
}
function directPost (url, params, headers = {}) {
return directRequest('POST', url, params, headers);
}
function directPut (url, params, headers = {}) {
return directRequest('PUT', url, params, headers);
}
function directDelete (url, params, headers = {}) {
return directRequest('DELETE', url, params, headers);
}
function directHead (url, headers = {}) {
return directRequest('HEAD', url, {} , headers);
}
function directOptions (url, headers ={}) {
return directRequest('OPTIONS', url, {}, headers);
}
function proxyWs (url) {
return doWebSocket(url, true);
}
function directWs (url) {
return doWebSocket(url);
}
/**
* generate the final url based on protocol and path
*
*/
function generateUrl (protocol, urlPath) {
return protocol === 'http' ? HTTP_SERVER_BASE + urlPath : HTTPS_SERVER_BASE + urlPath;
}
function generateWsUrl (protocol, urlPath) {
return protocol === 'wss' ? WSS_SERVER_BASE + urlPath : WS_SERVER_BASE + urlPath;
}
module.exports = {
proxyGet,
proxyPost,
directGet,
directPost,
directUpload,
proxyUpload,
generateUrl,
proxyWs,
directWs,
generateWsUrl,
directPut,
proxyPut,
directDelete,
proxyDelete,
directHead,
proxyHead,
directOptions,
proxyOptions,
directPutUpload,
proxyPutUpload
};

View File

@ -0,0 +1,65 @@
/*
* Utility class for creating proxy server, used to create specfied proxy server
*
*/
let proxy = require('../../proxy.js');
const util = require('../../lib/util.js');
const DEFAULT_OPTIONS = {
type: "http",
port: 8001,
hostname: "localhost",
dbFile: null, // optional, save request data to a specified file, will use in-memory db if not specified
webPort: 8002, // optional, port for web interface
socketPort: 8003, // optional, internal port for web socket, replace this when it is conflict with your own service
throttle: 10000, // optional, speed limit in kb/s
disableWebInterface: false, //optional, set it when you don't want to use the web interface
setAsGlobalProxy: false, //set anyproxy as your system proxy
interceptHttps: true, // intercept https as well
silent: false //optional, do not print anything into terminal. do not set it when you are still debugging.
};
/**
*
* @return An instance of proxy, could be closed by calling `instance.close()`
*/
function defaultProxyServer () {
proxy = util.freshRequire('../proxy.js');
const options = util.merge({}, DEFAULT_OPTIONS);
options.rule = util.freshRequire('./rule_default.js');
return new proxy.proxyServer(options);
}
/*
* Create proxy server with rule
* @param rules
Object, the rule object which contains required intercept method
@return An instance of proxy, could be closed by calling `instance.close()`
*/
function proxyServerWithRule (rule) {
proxy = util.freshRequire('../proxy.js');
const options = util.merge({}, DEFAULT_OPTIONS);
options.rule = rule;
return new proxy.proxyServer(options);
}
function proxyServerWithoutHttpsIntercept (rule) {
proxy = util.freshRequire('../proxy.js');
const options = util.merge({}, DEFAULT_OPTIONS);
if (rule) {
options.rule = rule;
}
options.interceptHttps = false;
return new proxy.proxyServer(options);
}
module.exports = {
defaultProxyServer,
proxyServerWithoutHttpsIntercept,
proxyServerWithRule
};