Skip to content

Commit ca0f015

Browse files
authored
Merge pull request #1 from gemini-testing/dd.ws_interceptor
feat: add ability to intercept websocket messages
2 parents a3fe02d + 40d0f5b commit ca0f015

File tree

6 files changed

+701
-588
lines changed

6 files changed

+701
-588
lines changed

.travis.yml

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,5 @@
1-
sudo: false
21
language: node_js
32
node_js:
4-
- "4"
5-
- "6"
63
- "8"
74
script:
85
- npm test
9-
after_success:
10-
- bash <(curl -s https://codecov.io/bash)
11-
matrix:
12-
fast_finish: true
13-
notifications:
14-
email:
15-
16-
irc: "irc.freenode.org#nodejitsu"

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,26 @@ proxyServer.listen(8015);
395395
396396
};
397397
```
398+
* **wsInterceptServerMsg**: Is a handler which is called when a websocket message is intercepted on its way to the server. It takes two arguments: `data` - is a websocket message and flags (fin, mask, compress, binary). If falsy value is returned then nothing will be sended to the target server.
399+
```
400+
const proxy = new HttpProxy({
401+
...
402+
wsInterceptServerMsg: (data, flags) {
403+
return typeof data === 'string ? data.toUpperCase() : data;
404+
}
405+
...
406+
})
407+
```
408+
* **wsInterceptClientMsg**: Is a handler which is called when a websocket message is intercepted on its way to the client. It takes two arguments: `data` - is a websocket message and flags (fin, mask, compress, binary). If falsy value is returned then nothing will be sended to the client.
409+
```
410+
const proxy = new HttpProxy({
411+
...
412+
wsInterceptClientMsg: (data, flags) {
413+
return typeof data === 'string ? data.toUpperCase() : data;
414+
}
415+
...
416+
})
417+
```
398418
399419
**NOTE:**
400420
`options.ws` and `options.ssl` are optional.
@@ -414,6 +434,8 @@ If you are using the `proxyServer.listen` method, the following options are also
414434
* `proxyReq`: This event is emitted before the data is sent. It gives you a chance to alter the proxyReq request object. Applies to "web" connections
415435
* `proxyReqWs`: This event is emitted before the data is sent. It gives you a chance to alter the proxyReq request object. Applies to "websocket" connections
416436
* `proxyRes`: This event is emitted if the request to the target got a response.
437+
* `wsServerMsg`: This event is emitted after websocket message is sended to the server.
438+
* `wsClientMsg`: This event is emitted after webscoket mesage is sended to the client.
417439
* `open`: This event is emitted once the proxy websocket was created and piped into the target websocket.
418440
* `close`: This event is emitted once the proxy websocket was closed.
419441
* (DEPRECATED) `proxySocket`: Deprecated in favor of `open`.

lib/http-proxy/passes/ws-incoming.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
var http = require('http'),
2-
https = require('https'),
3-
common = require('../common');
1+
'use strict';
2+
3+
const http = require('http');
4+
const https = require('https');
5+
const common = require('../common');
6+
const WsInterceptor = require('../ws/interceptor');
47

58
/*!
69
* Array of passes.
@@ -142,7 +145,12 @@ module.exports = {
142145
//
143146
socket.write(createHttpHeader('HTTP/1.1 101 Switching Protocols', proxyRes.headers));
144147

145-
proxySocket.pipe(socket).pipe(proxySocket);
148+
if (options.wsInterceptServerMsg || options.wsInterceptClientMsg) {
149+
WsInterceptor.create({socket, options, proxyReq, proxyRes, proxySocket}).intercept();
150+
}
151+
else {
152+
proxySocket.pipe(socket).pipe(proxySocket);
153+
}
146154

147155
server.emit('open', proxySocket);
148156
server.emit('proxySocket', proxySocket); //DEPRECATED.

lib/http-proxy/ws/interceptor.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
'use strict';
2+
3+
const PerMessageDeflate = require('ws/lib/PerMessageDeflate');
4+
const Extensions = require('ws/lib/Extensions');
5+
const Receiver = require('ws/lib/Receiver');
6+
const Sender = require('ws/lib/Sender');
7+
8+
const acceptExtensions = ({extenstions, isServer}) => {
9+
const {extensionName} = PerMessageDeflate;
10+
const extenstion = extenstions[extensionName];
11+
12+
if (!extenstion) {
13+
return {};
14+
}
15+
16+
const perMessageDeflate = new PerMessageDeflate({}, isServer);
17+
perMessageDeflate.accept(extenstion);
18+
19+
return {[extensionName]: perMessageDeflate};
20+
};
21+
22+
const getMsgHandler = ({interceptor, dataSender, binary}) => {
23+
return (data, flags) => {
24+
if (typeof interceptor !== 'function') {
25+
dataSender({data});
26+
}
27+
28+
const modifiedData = interceptor(data, flags);
29+
30+
// if interceptor does not return data then nothing will be sended to the server
31+
if (modifiedData) {
32+
dataSender({data: modifiedData, binary});
33+
}
34+
}
35+
};
36+
37+
module.exports = class Interceptor {
38+
static create(opts = {}) {
39+
return new this(opts);
40+
}
41+
42+
constructor({socket, options, proxyReq, proxyRes, proxySocket}) {
43+
this._socket = socket;
44+
this._options = options;
45+
this._proxyReq = proxyReq;
46+
this._proxyRes = proxyRes;
47+
this._proxySocket = proxySocket;
48+
49+
this._configure();
50+
}
51+
52+
_configure() {
53+
const secWsExtensions = this._proxyRes.headers['sec-websocket-extensions'];
54+
const extenstions = Extensions.parse(secWsExtensions);
55+
this._isCompressed = secWsExtensions && secWsExtensions.indexOf('permessage-deflate') != -1;
56+
57+
// need both versions of extensions for each side of the proxy connection
58+
this._clientExtenstions = this._isCompressed ? acceptExtensions({extenstions, isServer: false}) : null;
59+
this._serverExtenstions = this._isCompressed ? acceptExtensions({extenstions, isServer: true}) : null;
60+
}
61+
62+
_getDataSender({sender, event, options}) {
63+
return ({data, binary = false}) => {
64+
const opts = Object.assign({fin: true, compress: this._isCompressed, binary}, options);
65+
sender.send(data, opts);
66+
67+
this._proxyReq.emit(event, {data, binary});
68+
};
69+
}
70+
71+
_interceptServerMessages() {
72+
const receiver = new Receiver(this._clientExtenstions);
73+
const sender = new Sender(this._proxySocket, this._serverExtenstions);
74+
75+
// frame must be masked when send from client to server - https://tools.ietf.org/html/rfc6455#section-5.3
76+
const options = {mask: true};
77+
const dataSender = this._getDataSender({sender, event: 'wsServerMsg', options});
78+
79+
receiver.ontext = getMsgHandler({interceptor: this._options.wsInterceptServerMsg, dataSender, binary: false});
80+
receiver.onbinary = getMsgHandler({interceptor: this._options.wsInterceptServerMsg, dataSender, binary: true});
81+
82+
this._socket.on('data', (data) => receiver.add(data));
83+
}
84+
85+
_interceptClientMessages() {
86+
const receiver = new Receiver(this._serverExtenstions);
87+
const sender = new Sender(this._socket, this._clientExtenstions);
88+
89+
const options = {mask: false};
90+
const dataSender = this._getDataSender({sender, event: 'wsClientMsg', options});
91+
92+
receiver.ontext = getMsgHandler({interceptor: this._options.wsInterceptClientMsg, dataSender, binary: false});
93+
receiver.onbinary = getMsgHandler({interceptor: this._options.wsInterceptClientMsg, dataSender, binary: true});
94+
95+
this._proxySocket.on('data', (data) => receiver.add(data));
96+
}
97+
98+
intercept() {
99+
this._interceptServerMessages();
100+
this._interceptClientMessages();
101+
}
102+
};

0 commit comments

Comments
 (0)