diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..3aa0747 --- /dev/null +++ b/.babelrc @@ -0,0 +1,6 @@ +{ + "presets": [ + "es2015", + "stage-0" + ] +} \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..5d96e44 --- /dev/null +++ b/.eslintrc.js @@ -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) + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1e7469e..2fa5ecd 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,5 @@ coverage .grunt build/Release node_modules -.lock-wscript \ No newline at end of file +.lock-wscript +temp \ No newline at end of file diff --git a/jasmine.json b/jasmine.json new file mode 100644 index 0000000..3216038 --- /dev/null +++ b/jasmine.json @@ -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 +} diff --git a/lib/certMgr.js b/lib/certMgr.js index 50b12f1..711c2ed 100644 --- a/lib/certMgr.js +++ b/lib/certMgr.js @@ -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; \ No newline at end of file +module.exports = crtMgr; \ No newline at end of file diff --git a/lib/requestHandler.js b/lib/requestHandler.js index 074ea82..2f79d83 100644 --- a/lib/requestHandler.js +++ b/lib/requestHandler.js @@ -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(); diff --git a/lib/rule_default.js b/lib/rule_default.js index ccccad9..30f5a94 100644 --- a/lib/rule_default.js +++ b/lib/rule_default.js @@ -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); } }); } diff --git a/lib/util.js b/lib/util.js index e1d678a..f3c2314 100644 --- a/lib/util.js +++ b/lib/util.js @@ -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; +}; + diff --git a/package.json b/package.json index 37d6361..d58f736 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "async": "~0.9.0", "async-task-mgr": ">=1.1.0", "body-parser": "^1.13.1", + "change-case": "^3.0.0", "colorful": "^2.1.0", "commander": "~2.3.0", "compression": "^1.4.4", @@ -17,19 +18,35 @@ "iconv-lite": "^0.4.6", "ip": "^0.3.2", "juicer": "^0.6.6-stable", + "mime-types": "2.1.11", "nedb": "^0.11.0", + "node-easy-cert": "^1.0.0", "node-forge": "^0.6.39", "npm": "^2.7.0", "promise": "^7.0.4", "qrcode-npm": "0.0.3", "stream-throttle": "^0.1.3", - "ws": "^0.4.32" + "ws": "^1.1.0" }, "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": { - "test": "sh test.sh" + "test": "sh test/test.sh", + "testserver": "node test/server/startServer.js" }, "optionalDependencies": {}, "repository": { diff --git a/proxy.js b/proxy.js index 93d995f..85b9411 100644 --- a/proxy.js +++ b/proxy.js @@ -11,7 +11,6 @@ var http = require('http'), color = require('colorful'), certMgr = require("./lib/certMgr"), getPort = require("./lib/getPort"), - requestHandler = require("./lib/requestHandler"), Recorder = require("./lib/recorder"), logUtil = require("./lib/log"), wsServer = require("./lib/wsServer"), @@ -37,7 +36,8 @@ var T_TYPE_HTTP = 0, DEFAULT_HOST = "localhost", 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.type : 'http'(default) or 'https' @@ -70,22 +70,39 @@ function proxyServer(option){ 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){ - 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 var nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]); 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.blue("The WebSocket will not work properly in the https intercept mode :("), logUtil.T_TIP); } if(option.throttle){ 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; async.series( diff --git a/test/data/headers.js b/test/data/headers.js new file mode 100644 index 0000000..2e47a4e --- /dev/null +++ b/test/data/headers.js @@ -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 +}; + diff --git a/test/data/test.css b/test/data/test.css new file mode 100644 index 0000000..7345751 --- /dev/null +++ b/test/data/test.css @@ -0,0 +1,3 @@ +.test { + display: block; +} \ No newline at end of file diff --git a/test/data/test.eot b/test/data/test.eot new file mode 100755 index 0000000..c4c74f9 Binary files /dev/null and b/test/data/test.eot differ diff --git a/test/data/test.js b/test/data/test.js new file mode 100644 index 0000000..9883731 --- /dev/null +++ b/test/data/test.js @@ -0,0 +1,3 @@ +function test () { + console.info('This is nothing but a js file, to test the js download'); +} \ No newline at end of file diff --git a/test/data/test.json b/test/data/test.json new file mode 100644 index 0000000..107be9b --- /dev/null +++ b/test/data/test.json @@ -0,0 +1,3 @@ +{ + 'testkey': 'this is just a normal json file' +} \ No newline at end of file diff --git a/test/data/test.png b/test/data/test.png new file mode 100755 index 0000000..55a2aa3 Binary files /dev/null and b/test/data/test.png differ diff --git a/test/data/test.svg b/test/data/test.svg new file mode 100755 index 0000000..b3c6de3 --- /dev/null +++ b/test/data/test.svg @@ -0,0 +1,14 @@ + + + +Copyright (C) 2016 by original authors @ fontello.com + + + + + + + + + + \ No newline at end of file diff --git a/test/data/test.ttf b/test/data/test.ttf new file mode 100755 index 0000000..c3616fe Binary files /dev/null and b/test/data/test.ttf differ diff --git a/test/data/test.webp b/test/data/test.webp new file mode 100644 index 0000000..565200a Binary files /dev/null and b/test/data/test.webp differ diff --git a/test/data/test.woff b/test/data/test.woff new file mode 100755 index 0000000..6324ce3 Binary files /dev/null and b/test/data/test.woff differ diff --git a/test/data/test.woff2 b/test/data/test.woff2 new file mode 100755 index 0000000..c40c369 Binary files /dev/null and b/test/data/test.woff2 differ diff --git a/test/no_rule_spec.js b/test/no_rule_spec.js new file mode 100644 index 0000000..f474cf1 --- /dev/null +++ b/test/no_rule_spec.js @@ -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`); + // }); + // }); + // }); + }); +} diff --git a/test/no_rule_websocket_spec.js b/test/no_rule_websocket_spec.js new file mode 100644 index 0000000..bdfade2 --- /dev/null +++ b/test/no_rule_websocket_spec.js @@ -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(); + } + } + + }); + }); + +} + diff --git a/test/rule_shouldUseLocalResponse_spec.js b/test/rule_shouldUseLocalResponse_spec.js new file mode 100644 index 0000000..e540f66 --- /dev/null +++ b/test/rule_shouldUseLocalResponse_spec.js @@ -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'); + }); + + }); + }); +} diff --git a/test/server/server.js b/test/server/server.js new file mode 100644 index 0000000..74bf6d7 --- /dev/null +++ b/test/server/server.js @@ -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; \ No newline at end of file diff --git a/test/server/startServer.js b/test/server/startServer.js new file mode 100644 index 0000000..15af246 --- /dev/null +++ b/test/server/startServer.js @@ -0,0 +1,3 @@ +const Server = require('./server.js'); + +new Server(); \ No newline at end of file diff --git a/test/test.sh b/test/test.sh index 054cc28..b752548 100755 --- a/test/test.sh +++ b/test/test.sh @@ -1,5 +1,8 @@ #!/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 -nodeunit test.js \ No newline at end of file +jasmine JASMINE_CONFIG_PATH=./jasmine.json diff --git a/test/test_rules/shouldUseLocalResponseRule.js b/test/test_rules/shouldUseLocalResponseRule.js new file mode 100644 index 0000000..ac32bb1 --- /dev/null +++ b/test/test_rules/shouldUseLocalResponseRule.js @@ -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); + } +}; diff --git a/test/util/CommonUtil.js b/test/util/CommonUtil.js new file mode 100644 index 0000000..430f138 --- /dev/null +++ b/test/util/CommonUtil.js @@ -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 +}; diff --git a/test/util/HttpUtil.js b/test/util/HttpUtil.js new file mode 100644 index 0000000..4f308e6 --- /dev/null +++ b/test/util/HttpUtil.js @@ -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 +}; \ No newline at end of file diff --git a/test/util/ProxyServerUtil.js b/test/util/ProxyServerUtil.js new file mode 100644 index 0000000..0a333ec --- /dev/null +++ b/test/util/ProxyServerUtil.js @@ -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 +}; \ No newline at end of file