11'use strict'
22
33const From = require ( 'fastify-reply-from' )
4- const WebSocketPlugin = require ( 'fastify-websocket' )
54const WebSocket = require ( 'ws' )
6- const { pipeline } = require ( 'stream' )
7- const nonWsMethods = [ 'DELETE' , 'HEAD' , 'PATCH' , 'POST' , 'PUT' , 'OPTIONS' ]
85
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 ) {
10102 if ( ! opts . upstream ) {
11103 throw new Error ( 'upstream must be specified' )
12104 }
@@ -46,33 +138,16 @@ module.exports = async function (fastify, opts) {
46138 done ( null , payload )
47139 }
48140
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-
66141 fastify . route ( {
67142 url : '/' ,
68- method : nonWsMethods ,
143+ method : httpMethods ,
69144 preHandler,
70145 config : opts . config || { } ,
71146 handler
72147 } )
73148 fastify . route ( {
74149 url : '/*' ,
75- method : nonWsMethods ,
150+ method : httpMethods ,
76151 preHandler,
77152 config : opts . config || { } ,
78153 handler
@@ -84,21 +159,14 @@ module.exports = async function (fastify, opts) {
84159 reply . from ( dest || '/' , replyOpts )
85160 }
86161
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 )
103164 }
104165}
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