1- import * as nodeNet from 'node:net'
2- import * as https from 'node:https'
3- import { BackendError , PostgresConnection } from 'pg-gateway'
4- import { fromNodeSocket } from 'pg-gateway/node'
5- import { WebSocketServer , type WebSocket } from 'ws'
6- import makeDebug from 'debug'
7- import { extractDatabaseId , isValidServername } from './servername.ts'
8- import { getTls , setSecureContext } from './tls.ts'
9- import { createStartupMessage } from './create-message.ts'
10- import { extractIP } from './extract-ip.ts'
11- import {
12- DatabaseShared ,
13- DatabaseUnshared ,
14- logEvent ,
15- UserConnected ,
16- UserDisconnected ,
17- } from './telemetry.ts'
1+ import { httpsServer } from './websocket-server.ts'
2+ import { tcpServer } from './tcp-server.ts'
183
19- const debug = makeDebug ( 'browser-proxy' )
20-
21- const tcpConnections = new Map < string , PostgresConnection > ( )
22- const websocketConnections = new Map < string , WebSocket > ( )
23-
24- const httpsServer = https . createServer ( {
25- SNICallback : ( servername , callback ) => {
26- debug ( 'SNICallback' , servername )
27- if ( isValidServername ( servername ) ) {
28- debug ( 'SNICallback' , 'valid' )
29- callback ( null )
30- } else {
31- debug ( 'SNICallback' , 'invalid' )
32- callback ( new Error ( 'invalid SNI' ) )
33- }
34- } ,
4+ process . on ( 'unhandledRejection' , ( reason , promise ) => {
5+ console . error ( { location : 'unhandledRejection' , reason, promise } )
356} )
36- await setSecureContext ( httpsServer )
37- // reset the secure context every week to pick up any new TLS certificates
38- setInterval ( ( ) => setSecureContext ( httpsServer ) , 1000 * 60 * 60 * 24 * 7 )
39-
40- const websocketServer = new WebSocketServer ( {
41- server : httpsServer ,
42- } )
43-
44- websocketServer . on ( 'error' , ( error ) => {
45- debug ( 'websocket server error' , error )
46- } )
47-
48- websocketServer . on ( 'connection' , ( socket , request ) => {
49- debug ( 'websocket connection' )
50-
51- const host = request . headers . host
52-
53- if ( ! host ) {
54- debug ( 'No host header present' )
55- socket . close ( )
56- return
57- }
587
59- const databaseId = extractDatabaseId ( host )
60-
61- if ( websocketConnections . has ( databaseId ) ) {
62- socket . send ( 'sorry, too many clients already' )
63- socket . close ( )
64- return
65- }
66-
67- websocketConnections . set ( databaseId , socket )
68-
69- logEvent ( new DatabaseShared ( { databaseId } ) )
70-
71- socket . on ( 'message' , ( data : Buffer ) => {
72- debug ( 'websocket message' , data . toString ( 'hex' ) )
73- const tcpConnection = tcpConnections . get ( databaseId )
74- tcpConnection ?. streamWriter ?. write ( data )
75- } )
76-
77- socket . on ( 'close' , ( ) => {
78- websocketConnections . delete ( databaseId )
79- logEvent ( new DatabaseUnshared ( { databaseId } ) )
80- } )
81- } )
82-
83- // we need to use proxywrap to make our tcp server to enable the PROXY protocol support
84- const net = (
85- process . env . PROXIED ? ( await import ( 'findhit-proxywrap' ) ) . default . proxy ( nodeNet ) : nodeNet
86- ) as typeof nodeNet
87-
88- const tcpServer = net . createServer ( )
89-
90- tcpServer . on ( 'connection' , async ( socket ) => {
91- let databaseId : string | undefined
92-
93- const connection = await fromNodeSocket ( socket , {
94- tls : getTls ,
95- onTlsUpgrade ( state ) {
96- if ( ! state . tlsInfo ?. serverName || ! isValidServername ( state . tlsInfo . serverName ) ) {
97- throw BackendError . create ( {
98- code : '08006' ,
99- message : 'invalid SNI' ,
100- severity : 'FATAL' ,
101- } )
102- }
103-
104- const _databaseId = extractDatabaseId ( state . tlsInfo . serverName ! )
105-
106- if ( ! websocketConnections . has ( _databaseId ! ) ) {
107- throw BackendError . create ( {
108- code : 'XX000' ,
109- message : 'the browser is not sharing the database' ,
110- severity : 'FATAL' ,
111- } )
112- }
113-
114- if ( tcpConnections . has ( _databaseId ) ) {
115- throw BackendError . create ( {
116- code : '53300' ,
117- message : 'sorry, too many clients already' ,
118- severity : 'FATAL' ,
119- } )
120- }
121-
122- // only set the databaseId after we've verified the connection
123- databaseId = _databaseId
124- tcpConnections . set ( databaseId ! , connection )
125- logEvent ( new UserConnected ( { databaseId } ) )
126- } ,
127- serverVersion ( ) {
128- return '16.3'
129- } ,
130- onAuthenticated ( ) {
131- const websocket = websocketConnections . get ( databaseId ! )
132-
133- if ( ! websocket ) {
134- throw BackendError . create ( {
135- code : 'XX000' ,
136- message : 'the browser is not sharing the database' ,
137- severity : 'FATAL' ,
138- } )
139- }
140-
141- const clientIpMessage = createStartupMessage ( 'postgres' , 'postgres' , {
142- client_ip : extractIP ( socket . remoteAddress ! ) ,
143- } )
144- websocket . send ( clientIpMessage )
145- } ,
146- onMessage ( message , state ) {
147- if ( ! state . isAuthenticated ) {
148- return
149- }
150-
151- const websocket = websocketConnections . get ( databaseId ! )
152-
153- if ( ! websocket ) {
154- throw BackendError . create ( {
155- code : 'XX000' ,
156- message : 'the browser is not sharing the database' ,
157- severity : 'FATAL' ,
158- } )
159- }
160-
161- debug ( 'tcp message' , { message } )
162- websocket . send ( message )
163-
164- // return an empty buffer to indicate that the message has been handled
165- return new Uint8Array ( )
166- } ,
167- } )
168-
169- socket . on ( 'close' , ( ) => {
170- if ( databaseId ) {
171- tcpConnections . delete ( databaseId )
172- logEvent ( new UserDisconnected ( { databaseId } ) )
173- const websocket = websocketConnections . get ( databaseId )
174- websocket ?. send ( createStartupMessage ( 'postgres' , 'postgres' , { client_ip : '' } ) )
175- }
176- } )
8+ process . on ( 'uncaughtException' , ( error ) => {
9+ console . error ( { location : 'uncaughtException' , error } )
17710} )
17811
17912httpsServer . listen ( 443 , ( ) => {
@@ -183,3 +16,22 @@ httpsServer.listen(443, () => {
18316tcpServer . listen ( 5432 , ( ) => {
18417 console . log ( 'tcp server listening on port 5432' )
18518} )
19+
20+ const shutdown = async ( ) => {
21+ await Promise . allSettled ( [
22+ new Promise < void > ( ( res ) =>
23+ httpsServer . close ( ( ) => {
24+ res ( )
25+ } )
26+ ) ,
27+ new Promise < void > ( ( res ) =>
28+ tcpServer . close ( ( ) => {
29+ res ( )
30+ } )
31+ ) ,
32+ ] )
33+ process . exit ( 0 )
34+ }
35+
36+ process . on ( 'SIGTERM' , shutdown )
37+ process . on ( 'SIGINT' , shutdown )
0 commit comments