Skip to content

Commit a3e14a5

Browse files
committed
support hooks on websocket
1 parent 65f5300 commit a3e14a5

3 files changed

Lines changed: 262 additions & 206 deletions

File tree

lib/handlers/ws-handler.js

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
'use strict';
2+
3+
const co = require('co');
4+
const WebSocket = require('ws');
5+
const logUtil = require('../log');
6+
7+
/**
8+
* construct the request headers based on original connection,
9+
* but delete the `sec-websocket-*` headers as they are already consumed by AnyProxy
10+
*/
11+
function getNoWsHeaders(headers) {
12+
const originHeaders = Object.assign({}, headers);
13+
14+
Object.keys(originHeaders).forEach((key) => {
15+
// if the key matchs 'sec-websocket', delete it
16+
if (/sec-websocket/ig.test(key)) {
17+
delete originHeaders[key];
18+
}
19+
});
20+
21+
delete originHeaders.connection;
22+
delete originHeaders.upgrade;
23+
return originHeaders;
24+
}
25+
26+
/**
27+
* get request info from the ws client
28+
* @param @required wsClient the ws client of WebSocket
29+
*/
30+
function getWsReqInfo(wsReq) {
31+
const headers = wsReq.headers || {};
32+
const host = headers.host;
33+
const hostname = host.split(':')[0];
34+
const port = host.split(':')[1];
35+
// TODO 如果是windows机器,url是不是全路径?需要对其过滤,取出
36+
const path = wsReq.url || '/';
37+
const isEncript = wsReq.connection && wsReq.connection.encrypted;
38+
39+
return {
40+
url: `${isEncript ? 'wss' : 'ws'}://${hostname}:${port}${path}`,
41+
headers: headers, // the full headers of origin ws connection
42+
noWsHeaders: getNoWsHeaders(headers),
43+
secure: Boolean(isEncript),
44+
hostname: hostname,
45+
port: port,
46+
path: path
47+
};
48+
}
49+
50+
/**
51+
* When the source ws is closed, we need to close the target websocket.
52+
* If the source ws is normally closed, that is, the code is reserved, we need to transfrom them
53+
* @param {object} event CloseEvent
54+
*/
55+
const getCloseFromOriginEvent = (closeEvent) => {
56+
const code = closeEvent.code || '';
57+
const reason = closeEvent.reason || '';
58+
let targetCode = '';
59+
let targetReason = '';
60+
if (code >= 1004 && code <= 1006) {
61+
targetCode = 1000; // normal closure
62+
targetReason = `Normally closed. The origin ws is closed at code: ${code} and reason: ${reason}`;
63+
} else {
64+
targetCode = code;
65+
targetReason = reason;
66+
}
67+
68+
return {
69+
code: targetCode,
70+
reason: targetReason
71+
};
72+
}
73+
74+
/**
75+
* get a websocket event handler
76+
* @param @required {object} wsClient
77+
*/
78+
function handleWs(userRule, recorder, wsClient, wsReq) {
79+
const self = this;
80+
let resourceInfoId = -1;
81+
const resourceInfo = {
82+
wsMessages: [] // all ws messages go through AnyProxy
83+
};
84+
const clientMsgQueue = [];
85+
const serverInfo = getWsReqInfo(wsReq);
86+
// proxy-layer websocket client
87+
const proxyWs = new WebSocket(serverInfo.url, '', {
88+
rejectUnauthorized: !self.dangerouslyIgnoreUnauthorized,
89+
headers: serverInfo.noWsHeaders
90+
});
91+
92+
if (recorder) {
93+
Object.assign(resourceInfo, {
94+
host: serverInfo.hostname,
95+
method: 'WebSocket',
96+
path: serverInfo.path,
97+
url: serverInfo.url,
98+
req: wsReq,
99+
startTime: new Date().getTime()
100+
});
101+
resourceInfoId = recorder.appendRecord(resourceInfo);
102+
}
103+
104+
/**
105+
* store the messages before the proxy ws is ready
106+
*/
107+
const sendProxyMessage = (finalMsg) => {
108+
const message = finalMsg.data;
109+
if (proxyWs.readyState === 1) {
110+
// if there still are msg queue consuming, keep it going
111+
if (clientMsgQueue.length > 0) {
112+
clientMsgQueue.push(message);
113+
} else {
114+
proxyWs.send(message);
115+
}
116+
} else {
117+
clientMsgQueue.push(message);
118+
}
119+
};
120+
121+
/**
122+
* consume the message in queue when the proxy ws is not ready yet
123+
* will handle them from the first one-by-one
124+
*/
125+
const consumeMsgQueue = () => {
126+
while (clientMsgQueue.length > 0) {
127+
const message = clientMsgQueue.shift();
128+
proxyWs.send(message);
129+
}
130+
};
131+
132+
/**
133+
* consruct a message Record from message event
134+
* @param @required {object} finalMsg based on the MessageEvent from websockt.onmessage
135+
* @param @required {boolean} isToServer whether the message is to or from server
136+
*/
137+
const recordMessage = (finalMsg, isToServer) => {
138+
const message = {
139+
time: Date.now(),
140+
message: finalMsg.data,
141+
isToServer: isToServer
142+
};
143+
144+
// resourceInfo.wsMessages.push(message);
145+
recorder && recorder.updateRecordWsMessage(resourceInfoId, message);
146+
};
147+
148+
/**
149+
* prepare messageDetail object for intercept hooks
150+
* @param {object} messageEvent
151+
* @returns {object}
152+
*/
153+
const prepareMessageDetail = (messageEvent) => {
154+
return {
155+
requestOptions: {
156+
port: serverInfo.port,
157+
hostname: serverInfo.hostname,
158+
path: serverInfo.path,
159+
secure: serverInfo.secure,
160+
},
161+
url: serverInfo.url,
162+
data: messageEvent.data,
163+
};
164+
};
165+
166+
proxyWs.onopen = () => {
167+
consumeMsgQueue();
168+
};
169+
170+
// this event is fired when the connection is build and headers is returned
171+
proxyWs.on('upgrade', (response) => {
172+
resourceInfo.endTime = new Date().getTime();
173+
const headers = response.headers;
174+
resourceInfo.res = { //construct a self-defined res object
175+
statusCode: response.statusCode,
176+
headers: headers,
177+
};
178+
179+
resourceInfo.statusCode = response.statusCode;
180+
resourceInfo.resHeader = headers;
181+
resourceInfo.resBody = '';
182+
resourceInfo.length = resourceInfo.resBody.length;
183+
184+
recorder && recorder.updateRecord(resourceInfoId, resourceInfo);
185+
});
186+
187+
proxyWs.onerror = (e) => {
188+
// https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes
189+
wsClient.close(1001, e.message);
190+
proxyWs.close(1001);
191+
};
192+
193+
proxyWs.onmessage = (event) => {
194+
co(function *() {
195+
const modifiedMsg = (yield userRule.beforeSendWsMessageToClient(prepareMessageDetail(event))) || {};
196+
const finalMsg = {
197+
data: modifiedMsg.data || event.data,
198+
};
199+
recordMessage(finalMsg, false);
200+
wsClient.readyState === 1 && wsClient.send(finalMsg.data);
201+
});
202+
};
203+
204+
proxyWs.onclose = (event) => {
205+
logUtil.debug(`proxy ws closed with code: ${event.code} and reason: ${event.reason}`);
206+
const targetCloseInfo = getCloseFromOriginEvent(event);
207+
wsClient.readyState !== 3 && wsClient.close(targetCloseInfo.code, targetCloseInfo.reason);
208+
};
209+
210+
wsClient.onmessage = (event) => {
211+
co(function *() {
212+
const modifiedMsg = (yield userRule.beforeSendWsMessageToServer(prepareMessageDetail(event))) || {};
213+
const finalMsg = {
214+
data: modifiedMsg.data || event.data,
215+
};
216+
recordMessage(finalMsg, true);
217+
sendProxyMessage(finalMsg);
218+
});
219+
};
220+
221+
wsClient.onclose = (event) => {
222+
logUtil.debug(`original ws closed with code: ${event.code} and reason: ${event.reason}`);
223+
const targetCloseInfo = getCloseFromOriginEvent(event);
224+
proxyWs.readyState !== 3 && proxyWs.close(targetCloseInfo.code, targetCloseInfo.reason);
225+
};
226+
}
227+
228+
module.exports = function getWsHandler(userRule, recorder, wsClient, wsReq) {
229+
try {
230+
handleWs.call(this, userRule, recorder, wsClient, wsReq);
231+
} catch (e) {
232+
logUtil.debug('WebSocket Proxy Error:' + e.message);
233+
logUtil.debug(e.stack);
234+
console.error(e);
235+
}
236+
}

0 commit comments

Comments
 (0)