1
1
'use strict'
2
2
3
3
const From = require ( 'fastify-reply-from' )
4
- const WebSocketPlugin = require ( 'fastify-websocket' )
5
4
const WebSocket = require ( 'ws' )
6
- const { pipeline } = require ( 'stream' )
7
- const nonWsMethods = [ 'DELETE' , 'HEAD' , 'PATCH' , 'POST' , 'PUT' , 'OPTIONS' ]
8
5
9
- module . exports = async function ( fastify , opts ) {
6
+ const httpMethods = [ 'DELETE' , 'GET' , 'HEAD' , 'PATCH' , 'POST' , 'PUT' , 'OPTIONS' ]
7
+
8
+ function liftErrorCode ( code ) {
9
+ if ( typeof code !== 'number' ) {
10
+ // Sometimes "close" event emits with a non-numeric value
11
+ return 1011
12
+ } else if ( code === 1004 || code === 1005 || code === 1006 ) {
13
+ // ws module forbid those error codes usage, lift to "application level" (4xxx)
14
+ return 4000 + ( code % 1000 )
15
+ } else {
16
+ return code
17
+ }
18
+ }
19
+
20
+ function closeWebSocket ( socket , code , reason ) {
21
+ if ( socket . readyState === WebSocket . OPEN ) {
22
+ socket . close ( liftErrorCode ( code ) , reason )
23
+ }
24
+ }
25
+
26
+ function waitConnection ( socket , write ) {
27
+ if ( socket . readyState === WebSocket . CONNECTING ) {
28
+ socket . once ( 'open' , write )
29
+ } else {
30
+ write ( )
31
+ }
32
+ }
33
+
34
+ function proxyWebSockets ( source , target ) {
35
+ function close ( code , reason ) {
36
+ closeWebSocket ( source , code , reason )
37
+ closeWebSocket ( target , code , reason )
38
+ }
39
+
40
+ source . on ( 'message' , data => waitConnection ( target , ( ) => target . send ( data ) ) )
41
+ source . on ( 'ping' , data => waitConnection ( target , ( ) => target . ping ( data ) ) )
42
+ source . on ( 'pong' , data => waitConnection ( target , ( ) => target . pong ( data ) ) )
43
+ source . on ( 'close' , close )
44
+ source . on ( 'error' , error => close ( 1011 , error . message ) )
45
+ source . on ( 'unexpected-response' , ( ) => close ( 1011 , 'unexpected response' ) )
46
+
47
+ // source WebSocket is already connected because it is created by ws server
48
+ target . on ( 'message' , data => source . send ( data ) )
49
+ target . on ( 'ping' , data => source . ping ( data ) )
50
+ target . on ( 'pong' , data => source . pong ( data ) )
51
+ target . on ( 'close' , close )
52
+ target . on ( 'error' , error => close ( 1011 , error . message ) )
53
+ target . on ( 'unexpected-response' , ( ) => close ( 1011 , 'unexpected response' ) )
54
+ }
55
+
56
+ function createWebSocketUrl ( options , request ) {
57
+ const source = new URL ( request . url , 'http://127.0.0.1' )
58
+
59
+ const target = new URL (
60
+ options . rewritePrefix || options . prefix || source . pathname ,
61
+ options . upstream
62
+ )
63
+
64
+ target . search = source . search
65
+
66
+ return target
67
+ }
68
+
69
+ function setupWebSocketProxy ( fastify , options ) {
70
+ const server = new WebSocket . Server ( {
71
+ path : options . prefix ,
72
+ server : fastify . server ,
73
+ ...options . wsServerOptions
74
+ } )
75
+
76
+ fastify . addHook ( 'onClose' , ( instance , done ) => server . close ( done ) )
77
+
78
+ // To be able to close the HTTP server,
79
+ // all WebSocket clients need to be disconnected.
80
+ // Fastify is missing a pre-close event, or the ability to
81
+ // add a hook before the server.close call. We need to resort
82
+ // to monkeypatching for now.
83
+ const oldClose = fastify . server . close
84
+ fastify . server . close = function ( done ) {
85
+ for ( const client of server . clients ) {
86
+ client . close ( )
87
+ }
88
+ oldClose . call ( this , done )
89
+ }
90
+
91
+ server . on ( 'connection' , ( source , request ) => {
92
+ const url = createWebSocketUrl ( options , request )
93
+
94
+ const target = new WebSocket ( url , options . wsClientOptions )
95
+
96
+ fastify . log . debug ( { url : url . href } , 'proxy websocket' )
97
+ proxyWebSockets ( source , target )
98
+ } )
99
+ }
100
+
101
+ async function httpProxy ( fastify , opts ) {
10
102
if ( ! opts . upstream ) {
11
103
throw new Error ( 'upstream must be specified' )
12
104
}
@@ -46,33 +138,16 @@ module.exports = async function (fastify, opts) {
46
138
done ( null , payload )
47
139
}
48
140
49
- if ( opts . websocket ) {
50
- fastify . register ( WebSocketPlugin , opts . websocket )
51
- }
52
-
53
- fastify . get ( '/' , {
54
- preHandler,
55
- config : opts . config || { } ,
56
- handler,
57
- wsHandler
58
- } )
59
- fastify . get ( '/*' , {
60
- preHandler,
61
- config : opts . config || { } ,
62
- handler,
63
- wsHandler
64
- } )
65
-
66
141
fastify . route ( {
67
142
url : '/' ,
68
- method : nonWsMethods ,
143
+ method : httpMethods ,
69
144
preHandler,
70
145
config : opts . config || { } ,
71
146
handler
72
147
} )
73
148
fastify . route ( {
74
149
url : '/*' ,
75
- method : nonWsMethods ,
150
+ method : httpMethods ,
76
151
preHandler,
77
152
config : opts . config || { } ,
78
153
handler
@@ -84,21 +159,14 @@ module.exports = async function (fastify, opts) {
84
159
reply . from ( dest || '/' , replyOpts )
85
160
}
86
161
87
- function wsHandler ( conn , req ) {
88
- // TODO support paths and querystrings
89
- // TODO support rewriteHeader
90
- // TODO support rewritePrefix
91
- const ws = new WebSocket ( opts . upstream )
92
- const stream = WebSocket . createWebSocketStream ( ws )
93
-
94
- // TODO fastify-websocket should create a logger for each connection
95
- fastify . log . info ( 'starting websocket tunnel' )
96
- pipeline ( conn , stream , conn , function ( err ) {
97
- if ( err ) {
98
- fastify . log . info ( { err } , 'websocket tunnel terminated with error' )
99
- return
100
- }
101
- fastify . log . info ( 'websocket tunnel terminated' )
102
- } )
162
+ if ( opts . websocket ) {
163
+ setupWebSocketProxy ( fastify , opts )
103
164
}
104
165
}
166
+
167
+ httpProxy [ Symbol . for ( 'plugin-meta' ) ] = {
168
+ fastify : '^3.0.0' ,
169
+ name : 'fastify-http-proxy'
170
+ }
171
+
172
+ module . exports = httpProxy
0 commit comments