diff --git a/.travis.yml b/.travis.yml index 47cc542..4732996 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,5 +7,4 @@ install: - npm i npminstall && npminstall script: - npm run ci -after_script: - - npminstall codecov && codecov +after_script: "npm install coveralls && cat ./coverage/lcov.info | coveralls" diff --git a/History.md b/History.md new file mode 100644 index 0000000..5b4163d --- /dev/null +++ b/History.md @@ -0,0 +1,6 @@ + +1.1.0 / 2017-09-21 +================== + + * test: add unittest + * feat: add debug module diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2f27fe3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Axes + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index a56196f..60fc08c 100644 --- a/README.md +++ b/README.md @@ -3,24 +3,18 @@ a simple tcp proxy [![NPM version][npm-image]][npm-url] -[![build status][travis-image]][travis-url] -[![Test coverage][codecov-image]][codecov-url] -[![David deps][david-image]][david-url] -[![Known Vulnerabilities][snyk-image]][snyk-url] -[![NPM download][download-image]][download-url] +[![Build Status][travis-image]][travis-url] +[![Appveyor status][appveyor-image]][appveyor-url] +[![Coverage Status][coveralls-image]][coveralls-url] [npm-image]: https://img.shields.io/npm/v/tcp-proxy.js.svg?style=flat-square [npm-url]: https://npmjs.org/package/tcp-proxy.js -[travis-image]: https://img.shields.io/travis/{{org}}/tcp-proxy.js.svg?style=flat-square -[travis-url]: https://travis-ci.org/{{org}}/tcp-proxy.js -[codecov-image]: https://codecov.io/gh/{{org}}/tcp-proxy.js/branch/master/graph/badge.svg -[codecov-url]: https://codecov.io/gh/{{org}}/tcp-proxy.js -[david-image]: https://img.shields.io/david/{{org}}/tcp-proxy.js.svg?style=flat-square -[david-url]: https://david-dm.org/{{org}}/tcp-proxy.js -[snyk-image]: https://snyk.io/test/npm/tcp-proxy.js/badge.svg?style=flat-square -[snyk-url]: https://snyk.io/test/npm/tcp-proxy.js -[download-image]: https://img.shields.io/npm/dm/tcp-proxy.js.svg?style=flat-square -[download-url]: https://npmjs.org/package/tcp-proxy.js +[travis-url]: https://travis-ci.org/whxaxes/tcp-proxy.js +[travis-image]: http://img.shields.io/travis/whxaxes/tcp-proxy.js.svg +[appveyor-url]: https://ci.appveyor.com/project/whxaxes/tcp-proxy.js/branch/master +[appveyor-image]: https://ci.appveyor.com/api/projects/status/github/whxaxes/tcp-proxy.js?branch=master&svg=true +[coveralls-url]: https://coveralls.io/r/whxaxes/tcp-proxy.js +[coveralls-image]: https://img.shields.io/coveralls/whxaxes/tcp-proxy.js.svg ## Usage @@ -86,3 +80,7 @@ proxy.createProxy({ }, }); ``` + +## License + +MIT \ No newline at end of file diff --git a/index.js b/index.js index 75e7fb1..2d07c6b 100644 --- a/index.js +++ b/index.js @@ -2,6 +2,7 @@ const net = require('net'); const through = require('through2'); +const debug = require('debug')('tcp-proxy'); const EventEmitter = require('events').EventEmitter; function genThrough(interceptor) { @@ -31,13 +32,15 @@ function genThrough(interceptor) { } module.exports = class TCPProxy extends EventEmitter { - constructor(options) { + constructor(options = {}) { super(); this.port = options.port; + this.clients = []; } createProxy({ port, forwardPort, forwardHost, interceptor }) { const proxyPort = port || this.port; + forwardHost = forwardHost || '127.0.0.1'; interceptor = interceptor || {}; if (this.server) { @@ -47,58 +50,61 @@ module.exports = class TCPProxy extends EventEmitter { }); } - const onClose = () => { - this.proxyClient && this.proxyClient.destroy(); - this.proxyServer && this.proxyServer.destroy(); - this.proxyClient = null; - this.proxyServer = null; - }; - return new Promise((resolve, reject) => { this.server = net .createServer(client => { - const server = net.connect( - { - port: forwardPort, - host: forwardHost, - }, - () => { - let _client = client; - let _server = server; - - // client interceptor - if (interceptor.client) { - _client = _client.pipe(genThrough(interceptor.client)); - } - - // server interceptor - if (interceptor.server) { - _server = _server.pipe(genThrough(interceptor.server)); - } - - _client.pipe(server); - _server.pipe(client); + let interceptorClient; + let interceptorServer; + debug('new connection from %o', client.address()); + const server = net.connect({ + port: forwardPort, + host: forwardHost, + }, () => { + let _client = client; + let _server = server; + + // client interceptor + if (interceptor.client) { + interceptorClient = genThrough(interceptor.client); + _client = _client.pipe(interceptorClient); } - ); - this.proxyClient = client; - this.proxyServer = server; + // server interceptor + if (interceptor.server) { + interceptorServer = genThrough(interceptor.server); + _server = _server.pipe(interceptorServer); + } + + _client.pipe(server); + _server.pipe(client); + debug(`proxy 127.0.0.1:${proxyPort} connect to ${forwardHost}:${forwardPort}`); + this.emit('connection', _client, _server); + }); + + const onClose = err => { + client.destroy(); + server.destroy(); + interceptorClient && interceptorClient.end(); + interceptorServer && interceptorServer.end(); + debug(`${forwardHost}:${forwardPort} disconnect [${err ? `error: ${err.message}` : 'close'}]`); + this.removeListener('close', onClose); + }; server.once('close', onClose); server.once('error', onClose); client.once('close', onClose); client.once('error', onClose); - this.emit('connection'); + this.once('close', onClose); }) .listen(proxyPort); - this.server.once('close', () => { - this.server = null; - onClose(); + this.server.once('error', e => { + debug(`proxy server error: ${e.message}`); + reject(e); }); - this.server.once('error', reject); this.server.once('listening', () => { + debug(`proxy server listening on ${proxyPort}`); this.server.removeListener('error', reject); resolve(this.server); }); @@ -106,8 +112,17 @@ module.exports = class TCPProxy extends EventEmitter { } end() { + if (!this.server) { + return Promise.resolve(); + } + return new Promise(resolve => { - this.server.close(resolve); + this.emit('close'); + this.server.close(() => { + debug('proxy server closed'); + this.server = null; + resolve(); + }); }); } }; diff --git a/package.json b/package.json index c3fd47e..b861433 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,9 @@ { "name": "tcp-proxy.js", - "version": "1.0.0", + "version": "1.1.1", "description": "simple tcp proxy", "dependencies": { + "debug": "^3.0.1", "through2": "^2.0.3" }, "devDependencies": { diff --git a/test/index.test.js b/test/index.test.js index 16306b4..3f1098c 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -34,6 +34,23 @@ describe('test/index.test.js', () => { assert(response.data.toString() === `hello world ${data.port}`); }); + it('proxy should work correctly without options', function* () { + const newProxy = new TCPProxy(); + data = yield createServer(); + yield newProxy.createProxy({ port: 9800, forwardPort: data.port }); + const response = yield urllib.request('http://localhost:9800/'); + assert(response.data.toString() === `hello world ${data.port}`); + yield newProxy.end(); + }); + + it('proxy should not throw error with executing end function multiple times', function* () { + data = yield createServer(); + yield proxy.createProxy({ forwardPort: data.port }); + yield proxy.end(); + yield proxy.end(); + yield proxy.end(); + }); + it('proxy should start only one server', done => { co(function* () { data = yield createServer(); @@ -72,11 +89,33 @@ describe('test/index.test.js', () => { }, }, }); - const response = yield urllib.request(`http://localhost:${proxyPort}/`); - assert(response.data.toString() === `bello tom ${data.port}`); - const response2 = yield urllib.request(`http://localhost:${proxyPort}/123`); - assert(response2.data.toString() === `hello world ${data.port}`); + const response = yield { + result: urllib.request(`http://localhost:${proxyPort}/`), + result2: urllib.request(`http://localhost:${proxyPort}/123`), + }; + + assert(response.result.data.toString() === `bello tom ${data.port}`); + assert(response.result2.data.toString() === `hello world ${data.port}`); + }); + + it('should reject while proxy server error occurs', done => { + const httpServer = require('http') + .createServer() + .listen(proxyPort, () => { + proxy.createProxy({ forwardPort: 1234 }) + .then(() => { + end(new Error('no error occurs')); + }) + .catch(() => { + end(); + }); + }); + + function end(err) { + httpServer.close(); + done(err); + } }); it('should support async interceptor', function* () {