diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 3aa0747..0000000 --- a/.babelrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "presets": [ - "es2015", - "stage-0" - ] -} \ No newline at end of file diff --git a/.eslintrc b/.eslintrc index 1b7b3f1..e17b451 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,7 +5,7 @@ "browser": true, "node": true, "es6": true, - "jasmine": true + "jest": true }, "globals": { "React": true, diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..0466163 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: node_js +node_js: + - 12 +before_script: + - node ./bin/anyproxy-ca -g \ No newline at end of file diff --git a/README.md b/README.md index 3612f28..b845c9d 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ AnyProxy [![NPM version][npm-image]][npm-url] [![node version][node-image]][node-url] [![npm download][download-image]][download-url] +[![Build Status](https://travis-ci.org/alibaba/anyproxy.svg?branch=master)](https://travis-ci.org/alibaba/anyproxy) [npm-image]: https://img.shields.io/npm/v/anyproxy.svg?style=flat-square [npm-url]: https://npmjs.org/package/anyproxy diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..edbb252 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,11 @@ +if (process.env.NODE_ENV === 'test') { + module.exports = {}; +} else { + module.exports = { + presets: [ + 'es2015', + 'stage-0' + ] + }; +} + diff --git a/bin/anyproxy-ca b/bin/anyproxy-ca index f03a60f..85a71f7 100755 --- a/bin/anyproxy-ca +++ b/bin/anyproxy-ca @@ -20,10 +20,10 @@ program .parse(process.argv); function openFolderOfFile(filePath) { - const isWin = /^win/.test(process.platform); - if (isWin) { + const platform = process.platform; + if (/^win/.test(platform)) { exec('start .', { cwd: path.dirname(filePath) }); - } else { + } else if (/darwin/.test(platform)) { exec(`open -R ${filePath}`); } } @@ -33,7 +33,6 @@ function guideToGenrateCA() { if (!error) { const certDir = path.dirname(keyPath); console.log(`The cert is generated at ${certDir}. Please trust the ${color.bold('rootCA.crt')}.`); - // TODO: console.log('guide to install'); openFolderOfFile(crtPath); } else { console.error('failed to generate rootCA', error); @@ -44,7 +43,6 @@ function guideToGenrateCA() { function guideToTrustCA() { const certPath = AnyProxy.utils.certMgr.getRootCAFilePath(); if (certPath) { - // TODO: console.log('guide to install'); openFolderOfFile(certPath); } else { console.error('failed to get cert path'); diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..5081a7f --- /dev/null +++ b/jest.config.js @@ -0,0 +1,188 @@ +// For a detailed explanation regarding each configuration property, visit: +// https://jestjs.io/docs/en/configuration.html + +module.exports = { + // All imported modules in your tests should be mocked automatically + // automock: false, + + // Stop running tests after `n` failures + // bail: 0, + + // Respect "browser" field in package.json when resolving modules + // browser: false, + + // The directory where Jest should store its cached dependency information + // cacheDirectory: "/private/var/folders/dn/t1cpcmtx6ng82d7qf8b02w7r0000gn/T/jest_dx", + + // Automatically clear mock calls and instances between every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + // collectCoverage: false, + + // An array of glob patterns indicating a set of files for which coverage information should be collected + // collectCoverageFrom: null, + + // The directory where Jest should output its coverage files + coverageDirectory: 'coverage', + + // An array of regexp pattern strings used to skip coverage collection + // coveragePathIgnorePatterns: [ + // "/node_modules/" + // ], + + // A list of reporter names that Jest uses when writing coverage reports + // coverageReporters: [ + // "json", + // "text", + // "lcov", + // "clover" + // ], + + // An object that configures minimum threshold enforcement for coverage results + // coverageThreshold: null, + + // A path to a custom dependency extractor + // dependencyExtractor: null, + + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, + + // Force coverage collection from ignored files using an array of glob patterns + // forceCoverageMatch: [], + + // A path to a module which exports an async function that is triggered once before all test suites + // globalSetup: null, + + // A path to a module which exports an async function that is triggered once after all test suites + // globalTeardown: null, + + // A set of global variables that need to be available in all test environments + // globals: {}, + + // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. + // maxWorkers: "50%", + + // An array of directory names to be searched recursively up from the requiring module's location + // moduleDirectories: [ + // "node_modules" + // ], + + // An array of file extensions your modules use + // moduleFileExtensions: [ + // "js", + // "json", + // "jsx", + // "ts", + // "tsx", + // "node" + // ], + + // A map from regular expressions to module names that allow to stub out resources with a single module + // moduleNameMapper: {}, + + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], + + // Activates notifications for test results + // notify: false, + + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: "failure-change", + + // A preset that is used as a base for Jest's configuration + // preset: null, + + // Run tests from one or more projects + // projects: null, + + // Use this configuration option to add custom reporters to Jest + // reporters: undefined, + + // Automatically reset mock state between every test + // resetMocks: false, + + // Reset the module registry before running each individual test + // resetModules: false, + + // A path to a custom resolver + // resolver: null, + + // Automatically restore mock state between every test + // restoreMocks: false, + + // The root directory that Jest should scan for tests and modules within + // rootDir: null, + + // A list of paths to directories that Jest should use to search for files in + // roots: [ + // "" + // ], + + // Allows you to use a custom runner instead of Jest's default test runner + // runner: "jest-runner", + + // The paths to modules that run some code to configure or set up the testing environment before each test + // setupFiles: [], + + // A list of paths to modules that run some code to configure or set up the testing framework before each test + // setupFilesAfterEnv: [], + + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], + + // The test environment that will be used for testing + testEnvironment: 'node', + + // Options that will be passed to the testEnvironment + // testEnvironmentOptions: {}, + + // Adds a location field to test results + // testLocationInResults: false, + + // The glob patterns Jest uses to detect test files + // testMatch: [ + // "**/__tests__/**/*.[jt]s?(x)", + // "**/?(*.)+(spec|test).[tj]s?(x)" + // ], + + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + // testPathIgnorePatterns: [ + // "/node_modules/" + // ], + + // The regexp pattern or array of patterns that Jest uses to detect test files + // testRegex: [], + + // This option allows the use of a custom results processor + // testResultsProcessor: null, + + // This option allows use of a custom test runner + // testRunner: "jasmine2", + + // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href + // testURL: "http://localhost", + + // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" + // timers: "real", + + // A map from regular expressions to paths to transformers + // transform: null, + + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + // transformIgnorePatterns: [ + // "/node_modules/" + // ], + + // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them + // unmockedModulePathPatterns: undefined, + + // Indicates whether each individual test should be reported during the run + // verbose: null, + + // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // watchPathIgnorePatterns: [], + + // Whether to use watchman for file crawling + // watchman: true, +}; diff --git a/lib/httpsServerMgr.js b/lib/httpsServerMgr.js index e66c840..2b39db0 100644 --- a/lib/httpsServerMgr.js +++ b/lib/httpsServerMgr.js @@ -11,11 +11,11 @@ const async = require('async'), util = require('./util'), wsServerMgr = require('./wsServerMgr'), co = require('co'), + assert = require('assert'), constants = require('constants'), asyncTask = require('async-task-mgr'); const createSecureContext = tls.createSecureContext || crypto.createSecureContext; -//using sni to avoid multiple ports function SNIPrepareCert(serverName, SNICallback) { let keyContent, crtContent, @@ -59,7 +59,6 @@ function SNIPrepareCert(serverName, SNICallback) { //config.port - port to start https server //config.handler - request handler - /** * Create an https server * @@ -73,44 +72,11 @@ function createHttpsServer(config) { } return new Promise((resolve) => { - certMgr.getCertificate('anyproxy_internal_https_server', (err, keyContent, crtContent) => { - const server = https.createServer({ - secureOptions: constants.SSL_OP_NO_SSLv3 || constants.SSL_OP_NO_TLSv1, - SNICallback: SNIPrepareCert, - key: keyContent, - cert: crtContent - }, config.handler).listen(config.port); - resolve(server); - }); - }); -} - -/** -* create an https server that serving on IP address -* @param @required {object} config -* @param @required {string} config.ip the IP address of the server -* @param @required {number} config.port the port to listen on -* @param @required {function} handler the handler of each connect -*/ -function createIPHttpsServer(config) { - if (!config || !config.port || !config.handler) { - throw (new Error('please assign a port')); - } - - if (!config.ip) { - throw (new Error('please assign an IP to create the https server')); - } - - return new Promise((resolve) => { - certMgr.getCertificate(config.ip, (err, keyContent, crtContent) => { - const server = https.createServer({ - secureOptions: constants.SSL_OP_NO_SSLv3 || constants.SSL_OP_NO_TLSv1, - key: keyContent, - cert: crtContent - }, config.handler).listen(config.port); - - resolve(server); - }); + const server = https.createServer({ + secureOptions: constants.SSL_OP_NO_SSLv3 || constants.SSL_OP_NO_TLSv1, + SNICallback: SNIPrepareCert, + }, config.handler).listen(config.port); + resolve(server); }); } @@ -124,20 +90,20 @@ function createIPHttpsServer(config) { */ class httpsServerMgr { constructor(config) { - if (!config || !config.handler) { - throw new Error('handler is required'); - } - this.instanceDefaultHost = '127.0.0.1'; - this.httpsAsyncTask = new asyncTask(); + assert(config, 'config is required'); + assert(config.handler && config.wsHandler, 'handler and wsHandler are required'); + assert(config.hostname, 'hostname is required'); + this.hostname = config.hostname; this.handler = config.handler; - this.wsHandler = config.wsHandler + this.wsHandler = config.wsHandler; + this.httpsAsyncTask = new asyncTask(); + this.asyncTaskName = `https_${Math.random()}`; + this.httpsServer = null; } - getSharedHttpsServer(hostname) { - // ip address will have a unique name - const finalHost = util.isIpDomain(hostname) ? hostname : this.instanceDefaultHost; - + getSharedHttpsServer() { const self = this; + const finalHost = self.hostname; function prepareServer(callback) { let instancePort; co(util.getFreePort) @@ -145,19 +111,10 @@ class httpsServerMgr { instancePort = port; let httpsServer = null; - // if ip address passed in, will create an IP http server - if (util.isIpDomain(hostname)) { - httpsServer = yield createIPHttpsServer({ - ip: hostname, - port, - handler: self.handler - }); - } else { - httpsServer = yield createHttpsServer({ - port, - handler: self.handler - }); - } + httpsServer = yield createHttpsServer({ + port, + handler: self.handler + }); wsServerMgr.getWsServer({ server: httpsServer, @@ -168,6 +125,8 @@ class httpsServerMgr { logUtil.debug('will let WebSocket server to handle the upgrade event'); }); + self.httpsServer = httpsServer; + const result = { host: finalHost, port: instancePort, @@ -181,9 +140,7 @@ class httpsServerMgr { } return new Promise((resolve, reject) => { - // each ip address will gain a unit task name, - // while the domain address will share a common task name - self.httpsAsyncTask.addTask(`createHttpsServer-${finalHost}`, prepareServer, (error, serverInfo) => { + self.httpsAsyncTask.addTask(self.asyncTaskName, prepareServer, (error, serverInfo) => { if (error) { reject(error); } else { @@ -192,6 +149,10 @@ class httpsServerMgr { }); }); } + + close() { + return this.httpsServer && this.httpsServer.close(); + } } module.exports = httpsServerMgr; diff --git a/lib/requestHandler.js b/lib/requestHandler.js index f98e0b0..724c38f 100644 --- a/lib/requestHandler.js +++ b/lib/requestHandler.js @@ -129,7 +129,7 @@ function fetchRemoteResponse(protocol, options, reqData, config) { }); } else if (isServerDeflated && originContentLen) { refactContentEncoding(); - zlib.inflateRaw(serverResData, (err, buff) => { + zlib.inflate(serverResData, (err, buff) => { if (err) { rejectParsing(err); } else { @@ -689,7 +689,8 @@ class RequestHandler { reqHandlerCtx.httpsServerMgr = new HttpsServerMgr({ handler: reqHandlerCtx.userRequestHandler, - wsHandler: reqHandlerCtx.wsHandler // websocket + wsHandler: reqHandlerCtx.wsHandler, // websocket + hostname: '127.0.0.1', }); this.connectReqHandler = getConnectReqHandler.apply(reqHandlerCtx, [userRule, recorder, reqHandlerCtx.httpsServerMgr]); diff --git a/lib/wsServer.js b/lib/wsServer.js index beb28b2..ec40c99 100644 --- a/lib/wsServer.js +++ b/lib/wsServer.js @@ -59,6 +59,7 @@ class wsServer { const self = this; self.config = config; self.recorder = recorder; + self.checkBroadcastFlagTimer = null; } start() { @@ -78,7 +79,7 @@ class wsServer { // the flat to indicate wheter to broadcast the record let broadcastFlag = true; - setInterval(() => { + self.checkBroadcastFlagTimer = setInterval(() => { broadcastFlag = true; sendMultipleMessage(); }, 50); @@ -161,6 +162,9 @@ class wsServer { closeAll() { const self = this; + if (self.checkBroadcastFlagTimer) { + clearInterval(self.checkBroadcastFlagTimer); + } return new Promise((resolve, reject) => { self.wss.close((e) => { if (e) { diff --git a/package.json b/package.json index ce16669..694fff9 100644 --- a/package.json +++ b/package.json @@ -40,10 +40,13 @@ "ws": "^5.1.0" }, "devDependencies": { + "@babel/core": "^7.8.3", + "@babel/preset-env": "^7.8.3", "antd": "^2.5.0", "autoprefixer": "^6.4.1", "babel-core": "^6.14.0", "babel-eslint": "^7.0.0", + "babel-jest": "^24.9.0", "babel-loader": "^6.2.5", "babel-plugin-import": "^1.0.0", "babel-plugin-transform-runtime": "^6.15.0", @@ -61,11 +64,7 @@ "eslint-plugin-react": "^7.4.0", "extract-text-webpack-plugin": "^3.0.2", "file-loader": "^0.9.0", - "jasmine": "^2.5.3", - "koa": "^1.2.1", - "koa-body": "^1.4.0", - "koa-router": "^5.4.0", - "koa-send": "^3.2.0", + "jest": "^24.9.0", "less": "^2.7.1", "less-loader": "^2.2.3", "node-simhash": "^0.1.0", @@ -86,15 +85,15 @@ "svg-inline-loader": "^0.7.1", "tunnel": "^0.0.6", "url-loader": "^0.5.7", + "urllib": "^2.34.2", "webpack": "^3.10.0", "worker-loader": "^0.7.1" }, "scripts": { "prepublish": "npm run buildweb", - "test": "node test.js", + "test": "npx jest", "lint": "eslint .", "testserver": "node test/server/startServer.js", - "testOutWeb": "jasmine test/spec_outweb/test_realweb_spec.js", "buildweb": "NODE_ENV=production webpack --config web/webpack.config.js --colors", "webserver": "NODE_ENV=test webpack --config web/webpack.config.js --colors --watch", "doc:serve": "node build_scripts/prebuild-doc.js && gitbook serve ./docs-src ./docs --log debug", diff --git a/proxy.js b/proxy.js index 1230a07..76ce576 100644 --- a/proxy.js +++ b/proxy.js @@ -14,25 +14,6 @@ const http = require('http'), wsServerMgr = require('./lib/wsServerMgr'), ThrottleGroup = require('stream-throttle').ThrottleGroup; -// const memwatch = require('memwatch-next'); - -// setInterval(() => { -// console.log(process.memoryUsage()); -// const rss = Math.ceil(process.memoryUsage().rss / 1000 / 1000); -// console.log('Program is using ' + rss + ' mb of Heap.'); -// }, 1000); - -// memwatch.on('stats', (info) => { -// console.log('gc !!'); -// console.log(process.memoryUsage()); -// const rss = Math.ceil(process.memoryUsage().rss / 1000 / 1000); -// console.log('GC !! Program is using ' + rss + ' mb of Heap.'); - -// // var heapUsed = Math.ceil(process.memoryUsage().heapUsed / 1000); -// // console.log("Program is using " + heapUsed + " kb of Heap."); -// // console.log(info); -// }); - const T_TYPE_HTTP = 'http', T_TYPE_HTTPS = 'https', DEFAULT_TYPE = T_TYPE_HTTP; @@ -273,6 +254,10 @@ class ProxyCore extends events.EventEmitter { cltSocket.end(); } + if (this.requestHandler.httpsServerMgr) { + this.requestHandler.httpsServerMgr.close(); + } + if (this.socketPool) { for (const key in this.socketPool) { this.socketPool[key].destroy(); diff --git a/test.js b/test.js deleted file mode 100644 index abd6d8c..0000000 --- a/test.js +++ /dev/null @@ -1,16 +0,0 @@ -const Jasmine = require('jasmine'); - -const jasmine = new Jasmine(); -const util = require('./lib/util'); -const path = require('path'); - -const testTmpPath = path.join(__dirname, './test/temp'); -const configFilePath = path.join(__dirname, './test/jasmine.json'); -// rm - rf./test / temp / -util.deleteFolderContentsRecursive(testTmpPath); - -jasmine.loadConfigFile(configFilePath); -jasmine.configureDefaultReporter({ - showColors: false -}); -jasmine.execute(); diff --git a/test/__snapshots__/basic.spec.js.snap b/test/__snapshots__/basic.spec.js.snap new file mode 100644 index 0000000..c7dc7ef --- /dev/null +++ b/test/__snapshots__/basic.spec.js.snap @@ -0,0 +1,57 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`http - HTTP verbs DELETE: args 1`] = ` +Object { + "foo": "bar", +} +`; + +exports[`http - HTTP verbs DELETE: data 1`] = `""`; + +exports[`http - HTTP verbs GET: args 1`] = ` +Object { + "param": "param_value", +} +`; + +exports[`http - HTTP verbs GET: data 1`] = `undefined`; + +exports[`http - HTTP verbs PATCH: args 1`] = `Object {}`; + +exports[`http - HTTP verbs PATCH: data 1`] = `""`; + +exports[`http - HTTP verbs POST body and header: args 1`] = `Object {}`; + +exports[`http - HTTP verbs POST body and header: data 1`] = `"data:application/octet-stream;base64,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"`; + +exports[`http - HTTP verbs PUT: args 1`] = `Object {}`; + +exports[`http - HTTP verbs PUT: data 1`] = `"data:application/octet-stream;base64,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"`; + +exports[`https - HTTP verbs DELETE: args 1`] = ` +Object { + "foo": "bar", +} +`; + +exports[`https - HTTP verbs DELETE: data 1`] = `""`; + +exports[`https - HTTP verbs GET: args 1`] = ` +Object { + "param": "param_value", +} +`; + +exports[`https - HTTP verbs GET: data 1`] = `undefined`; + +exports[`https - HTTP verbs PATCH: args 1`] = `Object {}`; + +exports[`https - HTTP verbs PATCH: data 1`] = `""`; + +exports[`https - HTTP verbs POST body and header: args 1`] = `Object {}`; + +exports[`https - HTTP verbs POST body and header: data 1`] = `"data:application/octet-stream;base64,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"`; + +exports[`https - HTTP verbs PUT: args 1`] = `Object {}`; + +exports[`https - HTTP verbs PUT: data 1`] = `"data:application/octet-stream;base64,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"`; diff --git a/test/data/headers.js b/test/data/headers.js deleted file mode 100644 index 8062bf0..0000000 --- a/test/data/headers.js +++ /dev/null @@ -1,29 +0,0 @@ -/* -* 用于放置所有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', - some_thing: 'only_to_test_letter_case', - '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 deleted file mode 100644 index 7345751..0000000 --- a/test/data/test.css +++ /dev/null @@ -1,3 +0,0 @@ -.test { - display: block; -} \ No newline at end of file diff --git a/test/data/test.eot b/test/data/test.eot deleted file mode 100755 index c4c74f9..0000000 Binary files a/test/data/test.eot and /dev/null differ diff --git a/test/data/test.js b/test/data/test.js deleted file mode 100644 index 8021481..0000000 --- a/test/data/test.js +++ /dev/null @@ -1,4 +0,0 @@ -function test() { - console.info('This is nothing but a js file, to test the js download'); -} -test(); diff --git a/test/data/test.json b/test/data/test.json deleted file mode 100644 index 107be9b..0000000 --- a/test/data/test.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - '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 deleted file mode 100755 index 55a2aa3..0000000 Binary files a/test/data/test.png and /dev/null differ diff --git a/test/data/test.svg b/test/data/test.svg deleted file mode 100755 index b3c6de3..0000000 --- a/test/data/test.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - -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 deleted file mode 100755 index c3616fe..0000000 Binary files a/test/data/test.ttf and /dev/null differ diff --git a/test/data/test.webp b/test/data/test.webp deleted file mode 100644 index 565200a..0000000 Binary files a/test/data/test.webp and /dev/null differ diff --git a/test/data/test.woff b/test/data/test.woff deleted file mode 100755 index 6324ce3..0000000 Binary files a/test/data/test.woff and /dev/null differ diff --git a/test/data/test.woff2 b/test/data/test.woff2 deleted file mode 100755 index c40c369..0000000 Binary files a/test/data/test.woff2 and /dev/null differ diff --git a/test/fixtures/image.png b/test/fixtures/image.png new file mode 100644 index 0000000..f5416aa Binary files /dev/null and b/test/fixtures/image.png differ diff --git a/test/fixtures/someRule.js b/test/fixtures/someRule.js new file mode 100644 index 0000000..5b4c8fd --- /dev/null +++ b/test/fixtures/someRule.js @@ -0,0 +1,3 @@ +module.exports = { + foo: 'bar', +}; diff --git a/test/jasmine.json b/test/jasmine.json deleted file mode 100644 index f455f85..0000000 --- a/test/jasmine.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "spec_dir": "test", - "spec_files": [ - "spec_lib/*.js", - "spec_rule/*.js", - "spec_web/*.js" - ], - "helpers": [ - "../node_modules/babel-register/lib/node.js", - "../node_modules/babel-polyfill/dist/polyfill.js" - ], - "stopSpecOnExpectationFailure": false, - "random": false -} diff --git a/test/large_post.js b/test/large_post.js deleted file mode 100644 index 45c5865..0000000 --- a/test/large_post.js +++ /dev/null @@ -1,35 +0,0 @@ -const proxyTester = require('proxy-eval'), - Buffer = require('buffer').Buffer, - express = require('express'); - -const app = express(); - -app.post('/', (req, res) => { - const bigBody = new Buffer(1024 * 1024 * 10); - res.send(bigBody); //10 mb -}); -app.listen(3000); - -function test() { - //test the basic availibility of proxy server - setTimeout(() => { - const testParam = { - proxy: 'http://127.0.0.1:8001/', - reqTimeout: 4500, - httpGetUrl: '', - httpPostUrl: 'http://127.0.0.1:3000/', - httpPostBody: '123', - httpsGetUrl: '', - httpsPostUrl: '', - httpsPostBody: '' - }; - proxyTester.test(testParam, (results) => { - process.exit(); - }); - }, 1000); -} - -setTimeout(() => { - test(); -}, 3000); - diff --git a/test/lib/httpsServerMgr.spec.js b/test/lib/httpsServerMgr.spec.js new file mode 100644 index 0000000..c169250 --- /dev/null +++ b/test/lib/httpsServerMgr.spec.js @@ -0,0 +1,17 @@ +const httpsServerMgr = require('../../lib/httpsServerMgr'); + +describe('httpsServerMgr', () => { + it('get https server', async () => { + const serverMgr = new httpsServerMgr({ + hostname: '127.0.0.1', + handler: () => { + console.log('this is handler'); + }, + wsHandler: () => { + console.log('this is handler'); + }, + }); + await serverMgr.getSharedHttpsServer(); + serverMgr.close(); + }); +}); diff --git a/test/spec_lib/ruleLoader.js b/test/lib/ruleLoader.spec.js similarity index 56% rename from test/spec_lib/ruleLoader.js rename to test/lib/ruleLoader.spec.js index 3bf4560..5674b13 100644 --- a/test/spec_lib/ruleLoader.js +++ b/test/lib/ruleLoader.spec.js @@ -1,34 +1,25 @@ -/* -* test for rule replaceOption rule -* -*/ - const ruleLoader = require('../../lib/ruleLoader'); const fs = require('fs'); const path = require('path'); -const localModulePath = path.join(__dirname, '../util/CommonUtil.js'); -describe('rule loader', () => { - it('should successfully cache a remote file', done => { - ruleLoader.cacheRemoteFile('https://cdn.bootcss.com/lodash.js/4.16.4/lodash.min.js') +const localModulePath = path.join(__dirname, '../fixtures/someRule.js'); +describe('ruleLoader', () => { + it('should successfully cache a remote file', async () => { + await ruleLoader.cacheRemoteFile('https://cdn.bootcss.com/lodash.js/4.16.4/lodash.min.js') .then(filePath => { let content; if (filePath) { content = fs.readFileSync(filePath, { encoding: 'utf8' }); } expect(content && content.length > 100).toBe(true); - done(); - }) - .catch(done.fail); + }); }); - it('should load a local module ../util/CommonUtil', done => { - ruleLoader.loadLocalPath(localModulePath) + it('should load a local module ../util/CommonUtil', async () => { + await ruleLoader.loadLocalPath(localModulePath) .then(module => { - expect(module.printLog).not.toBeUndefined(); - done(); - }) - .catch(done.fail); + expect(module.foo).not.toBeUndefined(); + }); }); it('should smart load a remote module', done => { @@ -43,7 +34,7 @@ describe('rule loader', () => { it('should smart load a local module', done => { ruleLoader.requireModule(localModulePath) .then(module => { - expect(module.printLog).not.toBeUndefined(); + expect(module.foo).not.toBeUndefined(); done(); }) .catch(done.fail); diff --git a/test/spec_lib/util.js b/test/lib/util.spec.js similarity index 73% rename from test/spec_lib/util.js rename to test/lib/util.spec.js index 8881df3..d01afcb 100644 --- a/test/spec_lib/util.js +++ b/test/lib/util.spec.js @@ -1,17 +1,13 @@ -/* -* test for rule replaceOption rule -* -*/ const util = require('../../lib/util'); describe('utils', () => { - it('should get some free ports', done => { + it('getFreePort', async () => { const count = 100; const tasks = []; for (let i = 1; i <= count; i++) { tasks.push(util.getFreePort()); } - Promise.all(tasks) + await Promise.all(tasks) .then((results) => { // ensure ports are unique const portMap = {}; @@ -19,8 +15,6 @@ describe('utils', () => { portMap[portNumber] = true; }); expect(Object.keys(portMap).length).toEqual(count); - done(); - }) - .catch(done.fail); + }); }); }); diff --git a/test/report/README b/test/report/README deleted file mode 100644 index bb7c511..0000000 --- a/test/report/README +++ /dev/null @@ -1 +0,0 @@ -* this is a folder to save test reports * \ No newline at end of file diff --git a/test/rule/beforeDealHttpsRequest.spec.js b/test/rule/beforeDealHttpsRequest.spec.js new file mode 100644 index 0000000..d8d04e4 --- /dev/null +++ b/test/rule/beforeDealHttpsRequest.spec.js @@ -0,0 +1,50 @@ +const fs = require('fs'); +const path = require('path'); +const { basicProxyRequest, proxyServerWithRule, } = require('../util.js'); + +const RULE_PAYLOAD = 'this is something in rule'; + +const rule = { + *beforeSendRequest(requestDetail) { + const requestOptions = requestDetail.requestOptions; + return { + requestOptions, + requestData: RULE_PAYLOAD, + }; + }, + + *beforeDealHttpsRequest(requestDetail) { + return requestDetail.host.indexOf('httpbin.org') >= 0; + } +}; + +describe('Rule beforeDealHttpsRequest', () => { + let proxyServer; + let proxyPort; + let proxyHost; + + beforeAll(async () => { + proxyServer = await proxyServerWithRule(rule); + proxyPort = proxyServer.proxyPort; + proxyHost = `http://localhost:${proxyPort}`; + }); + + afterAll(() => { + return proxyServer && proxyServer.close(); + }); + it('Should replace the https request body', async () => { + const url = 'https://httpbin.org/put'; + const payloadStream = fs.createReadStream(path.resolve(__dirname, '../fixtures/image.png')); + const postHeaders = { + anyproxy_header: 'header_value', + }; + + await basicProxyRequest(proxyHost, 'PUT', url, postHeaders, {}, payloadStream).then((result) => { + const proxyRes = result.response; + const body = JSON.parse(result.body); + expect(proxyRes.statusCode).toBe(200); + expect(body.data).toEqual(RULE_PAYLOAD); + expect(body.url.indexOf('/put')).toBeGreaterThan(0); + }); + }); +}); diff --git a/test/rule/beforeSendRequest.spec.js b/test/rule/beforeSendRequest.spec.js new file mode 100644 index 0000000..965808a --- /dev/null +++ b/test/rule/beforeSendRequest.spec.js @@ -0,0 +1,93 @@ +const fs = require('fs'); +const path = require('path'); +const { basicProxyRequest, proxyServerWithRule, } = require('../util.js'); + +const RULE_PAYLOAD = 'this is something in rule'; +const RULE_REPLACE_HEADER_KEY = 'rule_replace_header_key'; +const RULE_REPLACE_HEADER_VALUE = 'rule_replace_header_value'; + +const rule = { + *beforeSendRequest(requestDetail) { + const reqUrl = requestDetail.url; + if (reqUrl.indexOf('/post') >= 0) { + const requestOptions = requestDetail.requestOptions; + requestOptions.path = '/put'; + requestOptions.method = 'PUT'; + return { + requestOptions, + requestData: RULE_PAYLOAD, + }; + } else if (reqUrl.indexOf('/status/302') >= 0) { + return { + response: { + statusCode: 404, + header: { + [RULE_REPLACE_HEADER_KEY]: RULE_REPLACE_HEADER_VALUE, + 'content-type': 'plain/text', + }, + body: RULE_PAYLOAD + } + }; + } else if (reqUrl.indexOf('/should_be_replaced') >= 0) { + const requestOptions = requestDetail.requestOptions; + requestOptions.hostname = 'httpbin.org'; + requestOptions.path = '/status/302'; + requestOptions.port = '443'; + return { + protocol: 'https', + requestOptions, + }; + } + } +}; + +describe('Rule replaceRequestData', () => { + let proxyServer; + let proxyPort; + let proxyHost; + + beforeAll(async () => { + proxyServer = await proxyServerWithRule(rule); + proxyPort = proxyServer.proxyPort; + proxyHost = `http://localhost:${proxyPort}`; + }); + + afterAll(() => { + return proxyServer && proxyServer.close(); + }); + + it('should replace the request data in proxy if the assertion is true', async () => { + const url = 'http://httpbin.org/post'; + const payloadStream = fs.createReadStream(path.resolve(__dirname, '../fixtures/image.png')); + const postHeaders = { + anyproxy_header: 'header_value', + }; + + await basicProxyRequest(proxyHost, 'POST', url, postHeaders, {}, payloadStream).then((result) => { + const proxyRes = result.response; + const body = JSON.parse(result.body); + expect(proxyRes.statusCode).toBe(200); + expect(body.data).toEqual(RULE_PAYLOAD); + expect(body.url.indexOf('/put')).toBeGreaterThan(0); + }); + }); + + it('should respond content specified in rule', async () => { + const url = 'http://httpbin.org/status/302'; + await basicProxyRequest(proxyHost, 'GET', url).then((result) => { + const proxyRes = result.response; + const body = result.body; + expect(body).toBe(RULE_PAYLOAD); + expect(proxyRes.statusCode).toBe(404); + expect(proxyRes.headers[RULE_REPLACE_HEADER_KEY]).toBe(RULE_REPLACE_HEADER_VALUE); + }); + }); + + it('should replace protocol and url', async () => { + const url = 'http://domain_not_exists.anyproxy.io/should_be_replaced'; + await basicProxyRequest(proxyHost, 'GET', url).then((result) => { + const proxyRes = result.response; + expect(proxyRes.statusCode).toBe(302); + }); + }); +}); diff --git a/test/rule/beforeSendResponse.js b/test/rule/beforeSendResponse.js new file mode 100644 index 0000000..dd4169d --- /dev/null +++ b/test/rule/beforeSendResponse.js @@ -0,0 +1,45 @@ +const { basicProxyRequest, proxyServerWithRule, } = require('../util.js'); + +const RULE_REPLACE_HEADER_KEY = 'rule_replace_header_key'; +const RULE_REPLACE_HEADER_VALUE = 'rule_replace_header_value'; +const RULE_REPLACE_BODY = 'RULE_REPLACE_BODY'; +const rule = { + *beforeSendResponse(requestDetail, responseDetail) { + if (requestDetail.url.indexOf('/uuid') >= 0) { + const newResponse = responseDetail.response; + newResponse.header[RULE_REPLACE_HEADER_KEY] = RULE_REPLACE_HEADER_VALUE; + newResponse.body = RULE_REPLACE_BODY; + newResponse.statusCode = 502; + return { + response: newResponse, + }; + } + }, +}; + +describe('Rule replaceResponseData', () => { + let proxyServer; + let proxyPort; + let proxyHost; + + beforeAll(async () => { + proxyServer = await proxyServerWithRule(rule); + proxyPort = proxyServer.proxyPort; + proxyHost = `http://localhost:${proxyPort}`; + }); + + afterAll(() => { + return proxyServer && proxyServer.close(); + }); + + it('Should replace the header and body', async () => { + const url = 'http://httpbin.org/uuid'; + await basicProxyRequest(proxyHost, 'GET', url).then((result) => { + const proxyRes = result.response; + const body = result.body; + expect(proxyRes.statusCode).toBe(502); + expect(proxyRes.headers[RULE_REPLACE_HEADER_KEY]).toBe(RULE_REPLACE_HEADER_VALUE); + expect(body).toBe(RULE_REPLACE_BODY); + }); + }); +}); diff --git a/test/rule/onError.spec.js b/test/rule/onError.spec.js new file mode 100644 index 0000000..b03c9e0 --- /dev/null +++ b/test/rule/onError.spec.js @@ -0,0 +1,60 @@ +const { basicProxyRequest, proxyServerWithRule, } = require('../util.js'); + +const jestMockErrorFn = jest.fn(); +const jestMockConnectErrorFn = jest.fn(); + +const ERROR_PAGE_IN_RULE = 'this is my error page'; +const rule = { + onConnectError: jestMockConnectErrorFn, + *onError(requestDetail, error) { + jestMockErrorFn(requestDetail, error); + return { + response: { + statusCode: '200', + header: {}, + body: ERROR_PAGE_IN_RULE, + } + }; + }, + *beforeDealHttpsRequest(requestDetail) { + return requestDetail.host.indexOf('intercept') === 0; + }, +}; + +describe('Rule replaceResponseData', () => { + let proxyServer; + let proxyPort; + let proxyHost; + + beforeAll(async () => { + proxyServer = await proxyServerWithRule(rule); + proxyPort = proxyServer.proxyPort; + proxyHost = `http://localhost:${proxyPort}`; + }); + + afterAll(() => { + return proxyServer && proxyServer.close(); + }); + + it('should get error', async () => { + const url = 'https://intercept.anyproxy_not_exists.io/some_path'; + const result = await basicProxyRequest(proxyHost, 'GET', url); + const proxyRes = result.response; + const body = result.body; + expect(proxyRes.statusCode).toBe(200); + expect(body).toBe(ERROR_PAGE_IN_RULE); + expect(jestMockErrorFn.mock.calls.length).toBe(1); + }); + + it('should get connec error', async () => { + const url = 'https://anyproxy_not_exists.io/do_not_intercept'; + let e; + try { + await basicProxyRequest(proxyHost, 'GET', url); + } catch (err) { + e = err; + } + expect(e).not.toBeUndefined(); + expect(jestMockConnectErrorFn.mock.calls.length).toBe(1); + }); +}); diff --git a/test/server/server.js b/test/server/server.js deleted file mode 100644 index 3ac9db5..0000000 --- a/test/server/server.js +++ /dev/null @@ -1,377 +0,0 @@ -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 nurl = require('url'); -const color = require('colorful'); -const WebSocketServer = require('ws').Server; -const tls = require('tls'); -const crypto = require('crypto'); -const stream = require('stream'); -const brotli = require('brotli'); -const zlib = require('zlib'); - -const createSecureContext = tls.createSecureContext || crypto.createSecureContext; - -const DEFAULT_PORT = 3000; -const HTTPS_PORT = 3001; -const HTTPS_PORT2 = 3002; // start multiple https server -const UPLOAD_DIR = path.resolve(__dirname, '../temp'); -const PROXY_KEY_PREFIX = 'proxy-'; - -function SNICertCallback(serverName, SNICallback) { - certMgr.getCertificate(serverName, (err, key, crt) => { - if (err) { - console.error('error happend in sni callback', err); - return; - } - const ctx = createSecureContext({ - key, - cert: crt - }); - - SNICallback(null, ctx); - }); -} - -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.protocol + '://' + this.request.host + nurl.parse(this.request.url).pathname; // remove param to get clean key - - // take proxy data with 'proxy-' + url - if (headers['via-proxy'] === 'true') { - key = PROXY_KEY_PREFIX + key; - } - - printLog('log request with key :' + key); - let body = this.request.body; - body = typeof body === 'object' ? JSON.stringify(body) : body; - - self.requestRecordMap[key] = { - headers, - body - }; - yield next; - }; - - this.logWsRequest = function (wsReq) { - const headers = wsReq.headers; - const host = headers.host; - const isEncript = wsReq.connection && wsReq.connection.encrypted; - const protocol = isEncript ? 'wss' : 'ws'; - let key = `${protocol}://${host}${wsReq.url}`; - // take proxy data with 'proxy-' + url - if (headers['via-proxy'] === 'true') { - key = PROXY_KEY_PREFIX + key; - } - - self.requestRecordMap[key] = { - headers: wsReq.headers, - body: '', - messages: [], - } - - return self.requestRecordMap[key]; - }; - - this.start(); -} - -KoaServer.prototype.constructRouter = function () { - const router = KoaRouter(); - router.post('/test/getuser', koaBody(), this.logRequest, function *(next) { - printLog('requesting post /test/getuser'); - this.response.set('reqbody', JSON.stringify(this.request.body)); - this.response.body = 'body_post_getuser'; - }); - - router.get('/test', this.logRequest, function *(next) { - printLog('request in get: ' + JSON.stringify(this.request)); - this.cookies.set('a1', 'a1value'); - this.cookies.set('a2', 'a2value'); - this.cookies.set('a3', 'a3value'); - this.response.set('header1', 'cookie2=headervalue2'); - - 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) { - yield send(this, `./data/test.${item}`, { - root: path.resolve(__dirname, '../') - }); - }); - }); - - router.get('/test/response/304', this.logRequest, function *(next) { - this.response.set('Content-Encoding', 'gzip'); - this.status = 304; - }); - - 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(UPLOAD_DIR)) { - try { - fs.mkdirSync(UPLOAD_DIR, '0777'); - } catch (e) { - console.log(e); - return null; - } - } - - file.name = 'test_upload_' + Date.now() + '.png'; - const 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 - } - }), - 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 - } - }), - 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.get('/test/should_not_replace_option', this.logRequest, function *(next) { - this.response.body = 'the_option_that_not_be_replaced'; - }); - - router.get('/test/should_replace_option', this.logRequest, function *(next) { - this.response.body = 'the_request_that_has_not_be_replaced'; - }); - - router.get('/test/new_replace_option', this.logRequest, function *(next) { - this.response.body = 'the_new_replaced_option_page_content'; - }); - - router.get('/test/normal_request1', this.logRequest, koaBody(), function *(next) { - printLog('requesting get /test/normal_request1'); - this.response.body = 'body_normal_request1'; - }); - - router.get('/test/normal_request2', this.logRequest, koaBody(), function *(next) { - printLog('requesting get /test/normal_request2'); - this.response.body = 'body_normal_request2'; - }); - - router.post('/test/normal_post_request1', koaBody(), this.logRequest, function *(next) { - printLog('requesting post /test/normal_post_request1'); - this.response.body = 'body_normal_post_request1'; - }); - - router.get('/big_response', this.logRequest, function *(next) { - const buf = new Buffer(1 * 1024 * 1024 * 1024); // 1GB - buf.fill(1); - printLog('request in get big response of 1GB'); - this.response.type = 'application/octet-stream'; - this.response.body = buf; - }); - - router.get('/test/brotli', this.logRequest, function *(next) { - this.status = 200; - this.response.set('Content-Encoding', 'br'); - this.response.set('Content-Type', 'application/json'); - const buf = new Buffer('{"type":"brotli","message":"This is a brotli encoding response, but it need to be a long string or the brotli module\'s compress result will be null"}'); - this.response.body = Buffer.from(brotli.compress(buf)); - }); - - router.get('/test/gzip', this.logRequest, function *(next) { - this.status = 200; - this.response.set('Content-Encoding', 'gzip'); - this.response.set('Content-Type', 'application/json'); - const bufStream = new stream.PassThrough(); - bufStream.end(new Buffer('{"type":"gzip","message":"This is a gzip encoding response"}')); - this.response.body = bufStream.pipe(zlib.createGzip()); - }); - - router.get('/test/deflate', this.logRequest, function *(next) { - this.status = 200; - this.response.set('Content-Encoding', 'deflate'); - this.response.set('Content-Type', 'application/json'); - this.response.body = zlib.deflateRawSync('{"type":"deflate","message":"This is a deflate encoding response"}'); - }); - - return router; -}; - -KoaServer.prototype.createWsServer = function (httpServer) { - const wsServer = new WebSocketServer({ - server: httpServer, - path: '/test/socket' - }); - wsServer.on('connection', (ws, wsReq) => { - const logRecord = this.logWsRequest(wsReq); - - ws.send(JSON.stringify({ - type: 'initial', - content: 'default message' - })); - - ws.on('message', message => { - printLog('message from request socket: ' + message); - this.handleRecievedMessage(ws, message); - logRecord.messages.push(message); - }); - - ws.on('error', e => console.error('error happened in websocket server', e)); - }) -}; - -KoaServer.prototype.getRequestRecord = function (key) { - return this.requestRecordMap[key] || null; -}; - -KoaServer.prototype.getProxyRequestRecord = function (key) { - key = PROXY_KEY_PREFIX + key; - return this.requestRecordMap[key] || null; -}; - -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 self = this; - const app = Koa(); - - app.use(router.routes()); - this.httpServer = app.listen(DEFAULT_PORT); - this.createWsServer(this.httpServer); - - printLog('HTTP is now listening on port :' + DEFAULT_PORT); - - certMgr.getCertificate('localhost', (error, keyContent, crtContent) => { - if (error) { - console.error('failed to create https server:', error); - } else { - self.httpsServer = https.createServer({ - SNICallback: SNICertCallback, - key: keyContent, - cert: crtContent - }, app.callback()); - - // create wss server - this.createWsServer(self.httpsServer); - - self.httpsServer.listen(HTTPS_PORT); - - self.httpsServer2 = https.createServer({ - key: keyContent, - cert: crtContent - }, app.callback()); - - self.httpsServer2.listen(HTTPS_PORT2); - - 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.httpsServer2 && this.httpsServer2.close(); - this.requestRecordMap = {}; - printLog('Server closed successfully'); -}; - - -function printLog(content) { - console.log(color.cyan('[SERVER LOG]: ' + content)); -} - -module.exports = KoaServer; diff --git a/test/server/startServer.js b/test/server/startServer.js deleted file mode 100644 index 7b861e3..0000000 --- a/test/server/startServer.js +++ /dev/null @@ -1,3 +0,0 @@ -const Server = require('./server.js'); - -new Server(); diff --git a/test/spec_lib/proxyServerModule.js b/test/spec_lib/proxyServerModule.js deleted file mode 100644 index 3a52ef2..0000000 --- a/test/spec_lib/proxyServerModule.js +++ /dev/null @@ -1,123 +0,0 @@ -/* -* test for rule replaceOption rule -* -*/ -const AnyProxy = require('../../proxy'); -const { - proxyGet, - directGet, - generateUrl, -} = require('../util/HttpUtil.js'); -const Server = require('../server/server.js'); - -describe('AnyProxy.proxyServer basic test', () => { - it('should successfully start a proxy server', done => { - const options = { - port: 8001, - rule: null, - webInterface: { - enable: true, - webPort: 8002 - }, - throttle: 10000, - forceProxyHttps: false, - silent: false - }; - const proxyServer = new AnyProxy.ProxyServer(options); - proxyServer.on('ready', () => { - proxyServer.close(); - done(); - }); - proxyServer.on('error', done.fail); - proxyServer.start(); - }); -}); - -describe('AnyProxy.proxyServer high order test', () => { - let proxyServer; - let serverInstance; - beforeAll(done => { - // jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; - serverInstance = new Server(); - - const options = { - port: 8001, - rule: null, - webInterface: { - enable: true, - webPort: 8002, - }, - throttle: 10000, - forceProxyHttps: false, - silent: false - }; - proxyServer = new AnyProxy.ProxyServer(options); - proxyServer.on('ready', done); - proxyServer.start(); - }); - - afterAll(() => { - proxyServer && proxyServer.close(); - serverInstance && serverInstance.close(); - }); - - it('should work as expected for domain host', done => { - // test if proxy server works - proxyGet('https://www.tmall.com', {}, {}) - .then(res => { - expect(res && res.statusCode && res.statusCode === 200 && res.body.length > 300).toBe(true); - done(); - }) - .catch(done); - }); - - it('should work as expected for ip host', done => { - // test if proxy server works - proxyGet(generateUrl('https', '/test'), {}, {}) - .then(res => { - expect(res && res.statusCode && res.statusCode === 200).toBe(true); - done(); - }) - .catch(done); - }); - - it('should start webinterface correctly', done => { - // test web interface - directGet('http://127.0.0.1:8002', {}, {}) - .then(res => { - expect(res && res.statusCode && res.statusCode === 200 && res.body.length > 300).toBe(true); - done(); - }) - .catch(done); - }); - - it('should deal well with the gzip encoding response', done => { - proxyGet(generateUrl('https', '/test/gzip'), {}, {}) - .then(res => { - expect(res && res.statusCode === 200).toBe(true); - expect(JSON.parse(res.body).type).toBe('gzip'); - done(); - }) - .catch(done); - }); - - it('should deal well with the deflate encoding response', done => { - proxyGet(generateUrl('https', '/test/deflate'), {}, {}) - .then(res => { - expect(res && res.statusCode === 200).toBe(true); - expect(JSON.parse(res.body).type).toBe('deflate'); - done(); - }) - .catch(done); - }); - - it('should deal well with the brotli encoding response', done => { - proxyGet(generateUrl('https', '/test/brotli'), {}, {}) - .then(res => { - expect(res && res.statusCode === 200).toBe(true); - expect(JSON.parse(res.body).type).toBe('brotli'); - done(); - }) - .catch(done); - }); -}); diff --git a/test/spec_lib/webInterface.js b/test/spec_lib/webInterface.js deleted file mode 100644 index f3f8570..0000000 --- a/test/spec_lib/webInterface.js +++ /dev/null @@ -1,30 +0,0 @@ -const WebInterface = require('../../lib/webInterface.js'); -const Recorder = require('../../lib/recorder'); -const { directGet } = require('../util/HttpUtil.js'); - -describe('WebInterface server', () => { - let webServer = null; - const webHost = 'http://127.0.0.1:8002' - - beforeAll(() => { - const recorder = new Recorder(); - webServer = new WebInterface({ - webPort: 8002, - }, recorder); - }); - - afterAll(() => { - webServer.close(); - }); - - it('should response qrcode string in /getQrCode', done => { - directGet(`${webHost}/api/getQrCode`) - .then(res => { - const body = JSON.parse(res.body); - expect(body.qrImgDom).toMatch(' { - 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 (!_isDeepEqual(directHeaders[key], proxyHeaders[key])) { - printWarn(`key "${key}" of two response headers are different in request "${requestUrl}" : - direct is: "${directHeaders[key]}", proxy is: "${proxyHeaders[key]}"`); - } - } - - return isEqual; -} - -/* -* Compare the request between direct with proxy -* -*/ -function isCommonReqEqual(url, serverInstance) { - console.info('==> trying to get the url ', url); - try { - 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']; - - directReqObj.headers['content-type'] = trimFormContentType(directReqObj.headers['content-type']); - proxyReqObj.headers['content-type'] = trimFormContentType(proxyReqObj.headers['content-type']); - - // avoid compare content-length header via proxy - delete directReqObj.headers['content-length']; - delete proxyReqObj.headers['content-length']; - delete directReqObj.headers['transfer-encoding']; - delete proxyReqObj.headers['transfer-encoding']; - - // delete the headers that should not be passed by AnyProxy - delete directReqObj.headers.connection; - delete proxyReqObj.headers.connection; - - // delete the headers related to websocket establishment - const directHeaderKeys = Object.keys(directReqObj.headers); - directHeaderKeys.forEach((key) => { - // if the key matchs 'sec-websocket', delete it - if (/sec-websocket/ig.test(key)) { - delete directReqObj.headers[key]; - } - }); - - const proxyHeaderKeys = Object.keys(proxyReqObj.headers); - proxyHeaderKeys.forEach((key) => { - // if the key matchs 'sec-websocaket', delete it - if (/sec-websocket/ig.test(key)) { - delete proxyReqObj.headers[key]; - } - }); - - 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)); -} - -function printHilite(content) { - console.log(color.yellow('==LOG==: ' + content)); -} - -function parseUrlQuery(string = '') { - const parameterArray = string.split('&'); - const parsedObj = {}; - parameterArray.forEach((parameter) => { - // 获取等号的位置 - const indexOfEqual = parameter.indexOf('='); - const name = parameter.substr(0, indexOfEqual); - const value = parameter.substr(indexOfEqual + 1); - parsedObj[name] = value; - }); - return parsedObj; -} - -function stringSimilarity(a, b, precision = 2) { - let similarity = '0%'; - let isCongruent = false; - if (a && b) { - const targetLen = Math.max(a.length, b.length); - targetLen > 1000 ? - similarity = simHasH(a, b) : - similarity = LevenshteinSimilarity(a, b); - isCongruent = similarity === 100; - similarity = similarity.toFixed(precision) + '%'; - } - return { - isCongruent, - similarity - } -} - -/** -* simhash similarity -*/ -function simHasH(a, b) { - const simhash = require('node-simhash'); - return (simhash.compare(a, b) * 100); -} - -/** -* Levenshtein Distance -*/ -function LevenshteinSimilarity(a, b) { - let cost; - const maxLen = Math.max(a.length, b.length); - const minOfThree = (numa, numb, numc) => { - if (numa > numb) { - return numb > numc ? numc : numb; - } else { - return numa > numc ? numc : numa; - } - } - if (a.length === 0) cost = b.length; - if (b.length === 0) cost = a.length; - - if (a.length > b.length) { - const tmp = a; - a = b; - b = tmp; - } - - const row = []; - for (let i = 0; i <= a.length; i++) { - row[i] = i; - } - - for (let i = 1; i <= b.length; i++) { - let prev = i; - for (let j = 1; j <= a.length; j++) { - let val; - if (b.charAt(i - 1) === a.charAt(j - 1)) { - val = row[j - 1]; - } else { - val = minOfThree(row[j - 1] + 1, prev + 1, row[j] + 1); - } - row[j - 1] = prev; - prev = val; - } - row[a.length] = prev; - } - cost = row[a.length]; - return ((maxLen - cost) / maxLen * 100); -} - -module.exports = { - isObjectEqual, - isCommonResHeaderEqual, - printLog, - printWarn, - printError, - printHilite, - isCommonReqEqual, - parseUrlQuery, - stringSimilarity, - isArrayEqual: _isDeepEqual -}; diff --git a/test/util/HttpUtil.js b/test/util/HttpUtil.js deleted file mode 100644 index 76281d1..0000000 --- a/test/util/HttpUtil.js +++ /dev/null @@ -1,373 +0,0 @@ -/* eslint prefer-arrow-callback: 0 */ -/** - * An util to make the request out - * - */ -const request = require('request'); -const fs = require('fs'); -const WebSocket = require('ws'); -const tunnel = require('tunnel'); -const stream = require('stream'); -const nodeUrl = require('url'); - -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_CHUNK_COLLECT_THRESHOLD = 20 * 1024 * 1024; // about 20 mb - -const SOCKE_PROXY_URL_OBJ = nodeUrl.parse(SOCKET_PROXY_HOST); - -class commonStream extends stream.Readable { - constructor(config) { - super({ - highWaterMark: DEFAULT_CHUNK_COLLECT_THRESHOLD * 5 - }); - } - _read(size) {} -} - -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); -} - -/** - * @param params {String} json类型或file路径 - * {Object} key-value形式 - */ -function doRequest(method = 'GET', url, params, headers = {}, isProxy) { - headers = Object.assign({}, headers); - - let reqStream = new commonStream(); - const requestData = { - headers, - followRedirect: false, - rejectUnauthorized: false - }; - - if (isProxy) { - requestData.proxy = PROXY_HOST; - requestData.headers['via-proxy'] = 'true'; - } - - const streamReq = (resolve, reject) => { - requestData.headers['content-type'] = 'text/plain'; //otherwise, koa-body could not recognize - if (typeof params === 'string') { - fs.existsSync(params) ? - reqStream = fs.createReadStream(params) : - reqStream.push(params); - } else if (typeof params === 'object') { - reqStream.push(JSON.stringify(params)); - } - reqStream.push(null); - reqStream.pipe(request[method.toLowerCase()]( - url, - requestData, - (error, response, body) => { - if (error) { - reject(error); - } else { - resolve(response); - } - } - )) - } - const commonReq = (resolve, reject) => { - requestData.url = url; - requestData.method = method; - requestData.qs = params; - request( - requestData, - (error, response, body) => { - if (error) { - reject(error); - } else { - resolve(response); - } - } - ); - } - const requestTask = new Promise((resolve, reject) => { - if (method === 'POST' || method === 'PUT') { - streamReq(resolve, reject); - } else { - commonReq(resolve, reject); - } - }); - 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, - url, - method, - headers, - json: true, - rejectUnauthorized: false - }; - - if (isProxy) { - requestData.proxy = PROXY_HOST; - requestData.headers['via-proxy'] = 'true'; - } - const requestTask = new Promise((resolve, reject) => { - request( - requestData, - (error, response, body) => { - if (error) { - reject(error); - return; - } - resolve(response); - } - ); - }); - return requestTask; -} - -function doWebSocket(url, headers = {}, isProxy) { - let ws; - if (isProxy) { - headers['via-proxy'] = 'true'; - let agent = new tunnel.httpOverHttp({ - proxy: { - hostname: SOCKE_PROXY_URL_OBJ.hostname, - port: SOCKE_PROXY_URL_OBJ.port - } - }) - - if (url.indexOf('wss') === 0) { - agent = new tunnel.httpsOverHttp({ - rejectUnauthorized: false, - proxy: { - hostname: SOCKE_PROXY_URL_OBJ.hostname, - port: SOCKE_PROXY_URL_OBJ.port - } - }) - } - - ws = new WebSocket(url, { - agent, - rejectUnauthorized: false, - headers - }); - } else { - ws = new WebSocket(url, { - rejectUnauthorized: false, - headers - }); - } - - 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, headers) { - return doWebSocket(url, headers, true); -} - -function directWs(url, headers) { - return doWebSocket(url, headers); -} - -/** -* 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; -} - -/* -* verify if the request data is a valid proxy request, by checking specified header -*/ -function isViaProxy(req) { - return req.headers['via-proxy'] === 'true'; -} - -/* -* check if url is supported by request moudle -*/ -function isSupportedProtocol(requestPath) { - return requestPath.indexOf('http://') === 0 || requestPath.indexOf('https://') === 0; -} - -/* -* collect all request data in one url -*/ -function getRequestListFromPage(pageUrl, cb) { - let _ph; - let _page; - let _outObj; - const phantom = require('phantom'); - console.log(`collecting requests from ${pageUrl}...`); - return phantom.create().then(ph => { - _ph = ph; - return _ph.createPage(); - }).then(page => { - _page = page; - _outObj = _ph.createOutObject(); - _outObj.urls = []; - page.property('onResourceRequested', function (requestData, networkRequest, out) { - out.urls.push(requestData); - }, _outObj); - return _page.open(pageUrl); - }) - .then(status => _outObj.property('urls')) - .then(urls => { - _page.close(); - _ph.exit(); - return urls; - }) - .catch((err) => { - console.log(`failed to collecting requests from ${pageUrl}`); - console.log(err); - }); -} - - -module.exports = { - getHostFromUrl, - getPathFromUrl, - getPortFromUrl, - proxyGet, - proxyPost, - directGet, - directPost, - directUpload, - proxyUpload, - generateUrl, - proxyWs, - directWs, - generateWsUrl, - directPut, - proxyPut, - directDelete, - proxyDelete, - directHead, - proxyHead, - directOptions, - proxyOptions, - directPutUpload, - proxyPutUpload, - isViaProxy, - getRequestListFromPage, - directRequest, - proxyRequest, - isSupportedProtocol -}; diff --git a/test/util/ProxyServerUtil.js b/test/util/ProxyServerUtil.js deleted file mode 100644 index abd9449..0000000 --- a/test/util/ProxyServerUtil.js +++ /dev/null @@ -1,86 +0,0 @@ -/* -* Utility class for creating proxy server, used to create specfied proxy server -* -*/ - -const util = require('../../lib/util.js'); - -const DEFAULT_OPTIONS = { - type: 'http', - port: 8001, - webInterface: { - enable: true, - webPort: 8002, // optional, port for web interface - }, - wsIntercept: true, - throttle: 10000, // optional, speed limit in kb/s - forceProxyHttps: true, // intercept https as well - dangerouslyIgnoreUnauthorized: true, - 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(webinterfaceEnable = true) { - const AnyProxy = util.freshRequire('../proxy.js'); - - const options = util.merge({}, DEFAULT_OPTIONS); - util.merge(options, { - webInterface: { - enable: webinterfaceEnable, - webPort: 8002 - } - }) - const instance = new AnyProxy.ProxyServer(options); - instance.on('error', e => { - console.log('server instance error', e); - }); - instance.start(); - return instance; -} - -/* -* 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, overrideConfig) { - const AnyProxy = util.freshRequire('../proxy.js'); - - const options = Object.assign({}, DEFAULT_OPTIONS, overrideConfig); - options.rule = rule; - - const instance = new AnyProxy.ProxyServer(options); - instance.on('error', e => { - console.log('server instance error', e); - }); - instance.start(); - - return instance; -} - -function proxyServerWithoutHttpsIntercept(rule) { - const AnyProxy = util.freshRequire('../proxy.js'); - - const options = util.merge({}, DEFAULT_OPTIONS); - if (rule) { - options.rule = rule; - } - options.forceProxyHttps = false; - - const instance = new AnyProxy.ProxyServer(options); - instance.on('error', e => { - console.log('server instance error', e); - }); - instance.start(); - return instance; -} - -module.exports = { - defaultProxyServer, - proxyServerWithoutHttpsIntercept, - proxyServerWithRule -}; diff --git a/test/util/SimHash.js b/test/util/SimHash.js deleted file mode 100644 index e69de29..0000000 diff --git a/test/spec_web/curlUtil.js b/test/web/curlUtil.spec.js similarity index 100% rename from test/spec_web/curlUtil.js rename to test/web/curlUtil.spec.js diff --git a/test/web/webInterface.spec.js b/test/web/webInterface.spec.js new file mode 100644 index 0000000..3b4297e --- /dev/null +++ b/test/web/webInterface.spec.js @@ -0,0 +1,27 @@ +const WebInterface = require('../../lib/webInterface.js'); +const Recorder = require('../../lib/recorder'); +const urllib = require('urllib'); + +describe('WebInterface server', () => { + let webServer = null; + const webHost = 'http://127.0.0.1:8002' + + beforeAll(async () => { + const recorder = new Recorder(); + webServer = new WebInterface({ + webPort: 8002, + }, recorder); + await webServer.start(); + }); + + afterAll(async () => { + await webServer.close(); + }); + + it('should response qrcode string in /getQrCode', async () => { + const response = await urllib.request(`${webHost}/api/getQrCode`); + const body = JSON.parse(response.res.data); + expect(body.qrImgDom).toMatch('