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+ 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