Skip to content

Commit 1296cd0

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

3 files changed

Lines changed: 259 additions & 206 deletions

File tree

lib/handlers/ws-handler.js

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
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+
hostName: hostName,
44+
port: port,
45+
path: path
46+
};
47+
}
48+
49+
/**
50+
* When the source ws is closed, we need to close the target websocket.
51+
* If the source ws is normally closed, that is, the code is reserved, we need to transfrom them
52+
* @param {object} event CloseEvent
53+
*/
54+
const getCloseFromOriginEvent = (closeEvent) => {
55+
const code = closeEvent.code || '';
56+
const reason = closeEvent.reason || '';
57+
let targetCode = '';
58+
let targetReason = '';
59+
if (code >= 1004 && code <= 1006) {
60+
targetCode = 1000; // normal closure
61+
targetReason = `Normally closed. The origin ws is closed at code: ${code} and reason: ${reason}`;
62+
} else {
63+
targetCode = code;
64+
targetReason = reason;
65+
}
66+
67+
return {
68+
code: targetCode,
69+
reason: targetReason
70+
};
71+
}
72+
73+
/**
74+
* get a websocket event handler
75+
* @param @required {object} wsClient
76+
*/
77+
function handleWs(userRule, recorder, wsClient, wsReq) {
78+
const self = this;
79+
let resourceInfoId = -1;
80+
const resourceInfo = {
81+
wsMessages: [] // all ws messages go through AnyProxy
82+
};
83+
const clientMsgQueue = [];
84+
const serverInfo = getWsReqInfo(wsReq);
85+
// proxy-layer websocket client
86+
const proxyWs = new WebSocket(serverInfo.url, '', {
87+
rejectUnauthorized: !self.dangerouslyIgnoreUnauthorized,
88+
headers: serverInfo.noWsHeaders
89+
});
90+
91+
if (recorder) {
92+
Object.assign(resourceInfo, {
93+
host: serverInfo.hostName,
94+
method: 'WebSocket',
95+
path: serverInfo.path,
96+
url: serverInfo.url,
97+
req: wsReq,
98+
startTime: new Date().getTime()
99+
});
100+
resourceInfoId = recorder.appendRecord(resourceInfo);
101+
}
102+
103+
/**
104+
* store the messages before the proxy ws is ready
105+
*/
106+
const sendProxyMessage = (finalMsg) => {
107+
const message = finalMsg.data;
108+
if (proxyWs.readyState === 1) {
109+
// if there still are msg queue consuming, keep it going
110+
if (clientMsgQueue.length > 0) {
111+
clientMsgQueue.push(message);
112+
} else {
113+
proxyWs.send(message);
114+
}
115+
} else {
116+
clientMsgQueue.push(message);
117+
}
118+
};
119+
120+
/**
121+
* consume the message in queue when the proxy ws is not ready yet
122+
* will handle them from the first one-by-one
123+
*/
124+
const consumeMsgQueue = () => {
125+
while (clientMsgQueue.length > 0) {
126+
const message = clientMsgQueue.shift();
127+
proxyWs.send(message);
128+
}
129+
};
130+
131+
/**
132+
* consruct a message Record from message event
133+
* @param @required {object} finalMsg based on the MessageEvent from websockt.onmessage
134+
* @param @required {boolean} isToServer whether the message is to or from server
135+
*/
136+
const recordMessage = (finalMsg, isToServer) => {
137+
const message = {
138+
time: Date.now(),
139+
message: finalMsg.data,
140+
isToServer: isToServer
141+
};
142+
143+
// resourceInfo.wsMessages.push(message);
144+
recorder && recorder.updateRecordWsMessage(resourceInfoId, message);
145+
};
146+
147+
/**
148+
*
149+
* @param {*} messageEvent
150+
*/
151+
const prepareMessageDetail = (messageEvent) => {
152+
return {
153+
requestOptions: {
154+
port: serverInfo.port,
155+
host: serverInfo.hostName,
156+
path: serverInfo.path,
157+
},
158+
url: serverInfo.url,
159+
data: messageEvent.data,
160+
};
161+
};
162+
163+
proxyWs.onopen = () => {
164+
consumeMsgQueue();
165+
};
166+
167+
// this event is fired when the connection is build and headers is returned
168+
proxyWs.on('upgrade', (response) => {
169+
resourceInfo.endTime = new Date().getTime();
170+
const headers = response.headers;
171+
resourceInfo.res = { //construct a self-defined res object
172+
statusCode: response.statusCode,
173+
headers: headers,
174+
};
175+
176+
resourceInfo.statusCode = response.statusCode;
177+
resourceInfo.resHeader = headers;
178+
resourceInfo.resBody = '';
179+
resourceInfo.length = resourceInfo.resBody.length;
180+
181+
recorder && recorder.updateRecord(resourceInfoId, resourceInfo);
182+
});
183+
184+
proxyWs.onerror = (e) => {
185+
// https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes
186+
wsClient.close(1001, e.message);
187+
proxyWs.close(1001);
188+
};
189+
190+
proxyWs.onmessage = (event) => {
191+
co(function *() {
192+
const modifiedMsg = (yield userRule.beforeSendWsResponse(prepareMessageDetail(event))) || {};
193+
const finalMsg = {
194+
data: modifiedMsg.data || event.data,
195+
};
196+
recordMessage(finalMsg, false);
197+
wsClient.readyState === 1 && wsClient.send(finalMsg.data);
198+
});
199+
};
200+
201+
proxyWs.onclose = (event) => {
202+
logUtil.debug(`proxy ws closed with code: ${event.code} and reason: ${event.reason}`);
203+
const targetCloseInfo = getCloseFromOriginEvent(event);
204+
wsClient.readyState !== 3 && wsClient.close(targetCloseInfo.code, targetCloseInfo.reason);
205+
};
206+
207+
wsClient.onmessage = (event) => {
208+
co(function *() {
209+
const modifiedMsg = (yield userRule.beforeSendWsRequest(prepareMessageDetail(event))) || {};
210+
const finalMsg = {
211+
data: modifiedMsg.data || event.data,
212+
};
213+
recordMessage(finalMsg, true);
214+
sendProxyMessage(finalMsg);
215+
});
216+
};
217+
218+
wsClient.onclose = (event) => {
219+
logUtil.debug(`original ws closed with code: ${event.code} and reason: ${event.reason}`);
220+
const targetCloseInfo = getCloseFromOriginEvent(event);
221+
proxyWs.readyState !== 3 && proxyWs.close(targetCloseInfo.code, targetCloseInfo.reason);
222+
};
223+
}
224+
225+
module.exports = function getWsHandler(userRule, recorder, wsClient, wsReq) {
226+
try {
227+
handleWs.call(this, userRule, recorder, wsClient, wsReq);
228+
} catch (e) {
229+
logUtil.debug('WebSocket Proxy Error:' + e.message);
230+
logUtil.debug(e.stack);
231+
console.error(e);
232+
}
233+
}

0 commit comments

Comments
 (0)