Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backport http-party/node-http-proxy#1362 #1380

Closed
wants to merge 29 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
40d0f5b
feat: add ability to intercept websocket messages
DudaGod Jun 20, 2019
ca0f015
Merge pull request #1 from gemini-testing/dd.ws_interceptor
DudaGod Jun 21, 2019
b54a495
1.18.0
DudaGod Jun 21, 2019
4b59ff1
fix: bug when one of interceptors does not specified
DudaGod Jun 21, 2019
cb1ce77
1.18.1
DudaGod Jun 21, 2019
784de0d
fix: listen "close" from client and from server
DudaGod Jun 25, 2019
046e84b
fix: intercept every client-server messages
DudaGod Jun 25, 2019
006ba47
fix: rename interceptor opts and evetns
DudaGod Jun 25, 2019
b59893d
chore: fix typo
DudaGod Jun 25, 2019
003a07e
fix: listen "close" event from socket
DudaGod Jun 25, 2019
92ca51f
Merge pull request #2 from gemini-testing/dd.correct_close_connection
DudaGod Jun 25, 2019
6a0824b
1.19.0
DudaGod Jun 25, 2019
136f4a5
feat: ability to subscribe on websocket client/server senders
DudaGod Jul 3, 2019
3f009b0
Merge pull request #4 from gemini-testing/dd.emit_ws_senders
DudaGod Jul 3, 2019
66ffb89
1.20.0
DudaGod Jul 3, 2019
3a770f8
feat: pass request object to message interceptors
DudaGod Jul 15, 2019
1acc239
Merge pull request #5 from gemini-testing/dd.pass_req_to_interceptor
DudaGod Jul 15, 2019
e9ef418
1.21.0
DudaGod Jul 15, 2019
b1247c6
fix: correctly close half-opened ws socket
DudaGod Jul 31, 2019
502711a
Merge pull request #6 from gemini-testing/dd.support_allow_half_open_…
DudaGod Jul 31, 2019
438e470
1.22.0
DudaGod Jul 31, 2019
9b20298
fix: close proxy socket on client socket "close" event
DudaGod Aug 7, 2019
eafba2a
Merge pull request #7 from gemini-testing/dd.close_proxy_socket_on_cl…
DudaGod Aug 7, 2019
9b7019e
1.22.1
DudaGod Aug 7, 2019
b4fa487
Merge commit 'cb3171abfa7944d5ddaf0911f5d21148c051bb06' into backport…
mcheshkov Sep 13, 2019
51db0ad
Merge commit '569e2ac4fb6d04b0522649027b3f7971121b464d' into backport…
mcheshkov Sep 13, 2019
b7ad362
Merge commit 'acdbec09c69c2f42f73a8ffff62e8afc5af4f080' into backport…
mcheshkov Sep 13, 2019
b55e321
Merge commit '235f0aa047ccc1cf3da0f6dfcc51995297703f95' into backport…
mcheshkov Sep 13, 2019
6d53867
Merge commit '91fee3e943dc4497e8dd4ef27116388dce091988' into backport…
mcheshkov Sep 13, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 0 additions & 11 deletions .travis.yml
@@ -1,16 +1,5 @@
sudo: false
language: node_js
node_js:
- "4"
- "6"
- "8"
script:
- npm test
after_success:
- bash <(curl -s https://codecov.io/bash)
matrix:
fast_finish: true
notifications:
email:
- travis@nodejitsu.com
irc: "irc.freenode.org#nodejitsu"
54 changes: 54 additions & 0 deletions README.md
Expand Up @@ -395,6 +395,27 @@ proxyServer.listen(8015);

};
```
* **wsInterceptClientMsg**: Is a handler which is called when a websocket message is intercepted on its way to the server from the client. It takes two arguments: `data` - is a websocket message and `options` in which exists field `req` - websocket request object and `flags` (fin, mask, compress, binary). If falsy value is returned then nothing will be sended to the client.
```
const proxy = new HttpProxy({
...
wsInterceptClientMsg: (data, options) {
return typeof data === 'string ? data.toUpperCase() : data;
}
...
})
```
* **wsInterceptServerMsg**: Is a handler which is called when a websocket message is intercepted on its way to the client from the server. It takes two arguments: `data` - is a websocket message and `options` in which exist fields `req` - websocket request object and `flags` (fin, mask, compress, binary). If falsy value is returned then nothing will be sended to the target server.
```
const proxy = new HttpProxy({
...
wsInterceptServerMsg: (data, options) {
return typeof data === 'string ? data.toUpperCase() : data;
}
...
})
```


**NOTE:**
`options.ws` and `options.ssl` are optional.
Expand Down Expand Up @@ -466,6 +487,39 @@ proxy.on('close', function (res, socket, head) {

**[Back to top](#table-of-contents)**

### Listening for websocket proxy request events

* `clientSenderInited`: This event is emitted after websocket sender from client to server is initialized.
* `serverSenderInited`: This event is emitted after websocket sender from server to client is initialized.
* `wsClientMsg`: This event is emitted after webscoket message is sended from the client to the server.
* `wsServerMsg`: This event is emitted after websocket message is sended from the server to the client.

```js
httpProxy.createServer({
target: 'ws://localhost:9014',
ws: true
}).listen(8014);

proxyServer.on('upgrade', function (req, socket, head) {
proxy.ws(req, socket, head);
});

proxyServer.on('proxyReqWs', (proxyReq, req, socket, options, head) => {
proxyReq.on('clientSenderInited', (sender) => {
sender.send('hello from client');
});

proxyReq.on('serverSenderInited', (sender) => {
sender.send('hello from server');
});

proxyReq.on('wsClientMsg', console.log);
proxyReq.on('wsServerMsg', console.log);
});
```

**[Back to top](#table-of-contents)**

### Shutdown

* When testing or running server within another program it may be necessary to close the proxy.
Expand Down
19 changes: 14 additions & 5 deletions lib/http-proxy/passes/ws-incoming.js
@@ -1,6 +1,9 @@
var http = require('http'),
https = require('https'),
common = require('../common');
'use strict';

const http = require('http');
const https = require('https');
const common = require('../common');
const WsInterceptor = require('../ws/interceptor');

/*!
* Array of passes.
Expand Down Expand Up @@ -128,10 +131,14 @@ module.exports = {
// The pipe below will end proxySocket if socket closes cleanly, but not
// if it errors (eg, vanishes from the net and starts returning
// EHOSTUNREACH). We need to do that explicitly.
socket.on('error', function () {
socket.on('error', function (err) {
console.error('ERROR: socket closed with:', err);
proxySocket.end();
});

socket.on('close', () => proxySocket.end());
socket.on('end', () => socket.end());

common.setupSocket(proxySocket);

if (proxyHead && proxyHead.length) proxySocket.unshift(proxyHead);
Expand All @@ -142,7 +149,9 @@ module.exports = {
//
socket.write(createHttpHeader('HTTP/1.1 101 Switching Protocols', proxyRes.headers));

proxySocket.pipe(socket).pipe(proxySocket);
const wsInterceptor = WsInterceptor.create({socket, options, req, proxyReq, proxyRes, proxySocket});

wsInterceptor.startDataTransfer();

server.emit('open', proxySocket);
server.emit('proxySocket', proxySocket); //DEPRECATED.
Expand Down
116 changes: 116 additions & 0 deletions lib/http-proxy/ws/interceptor.js
@@ -0,0 +1,116 @@
'use strict';

const PerMessageDeflate = require('ws/lib/PerMessageDeflate');
const Extensions = require('ws/lib/Extensions');
const Receiver = require('ws/lib/Receiver');
const Sender = require('ws/lib/Sender');

const acceptExtensions = ({extensions, isServer}) => {
const {extensionName} = PerMessageDeflate;
const extension = extensions[extensionName];

if (!extension) {
return {};
}

const perMessageDeflate = new PerMessageDeflate({}, isServer);
perMessageDeflate.accept(extension);

return {[extensionName]: perMessageDeflate};
};

module.exports = class Interceptor {
static create(opts = {}) {
return new this(opts);
}

constructor({socket, options, req, proxyReq, proxyRes, proxySocket}) {
this._socket = socket;
this._options = options;
this._req = req;
this._proxyReq = proxyReq;
this._proxyRes = proxyRes;
this._proxySocket = proxySocket;
this._isClientSocketOpen = true;
this._isServerSocketOpen = true;

this._configure();
}

_configure() {
this._proxySocket.on('close', () => this._isClientSocketOpen = false);
this._socket.on('close', () => this._isServerSocketOpen = false);

const secWsExtensions = this._proxyRes.headers['sec-websocket-extensions'];
const extensions = Extensions.parse(secWsExtensions);
this._isCompressed = secWsExtensions && secWsExtensions.includes('permessage-deflate');

// need both versions of extensions for each side of the proxy connection
this._clientExtensions = this._isCompressed ? acceptExtensions({extensions, isServer: false}) : null;
this._serverExtensions = this._isCompressed ? acceptExtensions({extensions, isServer: true}) : null;
}

_getDataSender({sender, dataSendCond, event, options}) {
return ({data, binary = false}) => {
const opts = Object.assign({fin: true, compress: this._isCompressed, binary}, options);

dataSendCond() && sender.send(data, opts);

this._proxyReq.emit(event, {data, binary});
};
}

_getMsgHandler({interceptor, dataSender, binary}) {
return (data, flags) => {
if (typeof interceptor !== 'function') {
dataSender({data});
return;
}

const modifiedData = interceptor(data, {req: this._req, flags});

// if interceptor does not return data then nothing will be sended to the server
if (modifiedData) {
dataSender({data: modifiedData, binary});
}
}
}

_interceptClientMessages() {
const receiver = new Receiver(this._clientExtensions);
const sender = new Sender(this._proxySocket, this._serverExtensions);
this._proxyReq.emit('clientSenderInited', sender);

// frame must be masked when send from client to server - https://tools.ietf.org/html/rfc6455#section-5.3
const options = {mask: true};
const dataSendCond = () => this._isClientSocketOpen;
const dataSender = this._getDataSender({sender, dataSendCond, event: 'wsClientMsg', options});

receiver.ontext = this._getMsgHandler({interceptor: this._options.wsInterceptClientMsg, dataSender, binary: false});
receiver.onbinary = this._getMsgHandler({interceptor: this._options.wsInterceptClientMsg, dataSender, binary: true});
receiver.onclose = (code, msg, {masked: mask}) => this._isClientSocketOpen && sender.close(code, msg, mask);

this._socket.on('data', (data) => receiver.add(data));
}

_interceptServerMessages() {
const receiver = new Receiver(this._serverExtensions);
const sender = new Sender(this._socket, this._clientExtensions);
this._proxyReq.emit('serverSenderInited', sender);

const options = {mask: false};
const dataSendCond = () => this._isServerSocketOpen;
const dataSender = this._getDataSender({sender, dataSendCond, event: 'wsServerMsg', options});

receiver.ontext = this._getMsgHandler({interceptor: this._options.wsInterceptServerMsg, dataSender, binary: false});
receiver.onbinary = this._getMsgHandler({interceptor: this._options.wsInterceptServerMsg, dataSender, binary: true});
receiver.onclose = (code, msg, {masked: mask}) => this._isServerSocketOpen && sender.close(code, msg, mask);

this._proxySocket.on('data', (data) => receiver.add(data));
}

startDataTransfer() {
this._interceptClientMessages();
this._interceptServerMessages();
}
};