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 ( / s e c - w e b s o c k e t / 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