Skip to content

Commit edb1958

Browse files
authored
Merge pull request #100 from supabase-community/feat/db-sharing-pg-gateway-web-api
chore: port browser-proxy to pg-gateway Web API
2 parents 8adbc16 + 0779d7f commit edb1958

File tree

8 files changed

+162
-54
lines changed

8 files changed

+162
-54
lines changed

apps/browser-proxy/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Browser Proxy
2+
3+
This app is a proxy that sits between the browser and a PostgreSQL client.
4+
5+
It is using a WebSocket server and a TCP server to make the communication between the PGlite instance in the browser and a standard PostgreSQL client possible.
6+
7+
## Development
8+
9+
Copy the `.env.example` file to `.env` and set the correct environment variables.
10+
11+
Install dependencies:
12+
13+
```sh
14+
npm install
15+
```
16+
17+
Start the proxy in development mode:
18+
19+
```sh
20+
npm run dev
21+
```
22+
23+
## Deployment
24+
25+
Create a new app on Fly.io, for example `database-build-browser-proxy`.
26+
27+
Fill the app's secrets with the correct environment variables based on the `.env.example` file.
28+
29+
Deploy the app:
30+
31+
```sh
32+
fly deploy --app database-build-browser-proxy
33+
```

apps/browser-proxy/fly.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
app = "postgres-new-browser-proxy"
2-
31
primary_region = 'iad'
42

53
[[services]]

apps/browser-proxy/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
"dependencies": {
1010
"@aws-sdk/client-s3": "^3.645.0",
1111
"debug": "^4.3.7",
12+
"expiry-map": "^2.0.0",
1213
"findhit-proxywrap": "^0.3.13",
13-
"pg-gateway": "0.3.0-alpha.7",
14+
"p-memoize": "^7.1.1",
15+
"pg-gateway": "^0.3.0-beta.3",
1416
"ws": "^8.18.0"
1517
},
1618
"devDependencies": {

apps/browser-proxy/src/index.ts

Lines changed: 21 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,34 @@
11
import * as nodeNet from 'node:net'
22
import * as https from 'node:https'
3-
import { PostgresConnection } from 'pg-gateway'
3+
import { BackendError, PostgresConnection } from 'pg-gateway'
4+
import { fromNodeSocket } from 'pg-gateway/node'
45
import { WebSocketServer, type WebSocket } from 'ws'
56
import makeDebug from 'debug'
6-
import * as tls from 'node:tls'
77
import { extractDatabaseId, isValidServername } from './servername.ts'
8-
import { getTls } from './tls.ts'
8+
import { getTls, setSecureContext } from './tls.ts'
99
import { createStartupMessage } from './create-message.ts'
1010
import { extractIP } from './extract-ip.ts'
1111

1212
const debug = makeDebug('browser-proxy')
1313

14-
const tcpConnections = new Map<string, nodeNet.Socket>()
14+
const tcpConnections = new Map<string, PostgresConnection>()
1515
const websocketConnections = new Map<string, WebSocket>()
1616

17-
let tlsOptions = await getTls()
18-
19-
// refresh the TLS certificate every week
20-
setInterval(
21-
async () => {
22-
tlsOptions = await getTls()
23-
httpsServer.setSecureContext(tlsOptions)
24-
},
25-
1000 * 60 * 60 * 24 * 7
26-
)
27-
2817
const httpsServer = https.createServer({
29-
...tlsOptions,
3018
SNICallback: (servername, callback) => {
3119
debug('SNICallback', servername)
3220
if (isValidServername(servername)) {
3321
debug('SNICallback', 'valid')
34-
callback(null, tls.createSecureContext(tlsOptions))
22+
callback(null)
3523
} else {
3624
debug('SNICallback', 'invalid')
3725
callback(new Error('invalid SNI'))
3826
}
3927
},
4028
})
29+
await setSecureContext(httpsServer)
30+
// reset the secure context every week to pick up any new TLS certificates
31+
setInterval(() => setSecureContext(httpsServer), 1000 * 60 * 60 * 24 * 7)
4132

4233
const websocketServer = new WebSocketServer({
4334
server: httpsServer,
@@ -70,8 +61,8 @@ websocketServer.on('connection', (socket, request) => {
7061

7162
socket.on('message', (data: Buffer) => {
7263
debug('websocket message', data.toString('hex'))
73-
const tcpSocket = tcpConnections.get(databaseId)
74-
tcpSocket?.write(data)
64+
const tcpConnection = tcpConnections.get(databaseId)
65+
tcpConnection?.streamWriter?.write(data)
7566
})
7667

7768
socket.on('close', () => {
@@ -86,50 +77,41 @@ const net = (
8677

8778
const tcpServer = net.createServer()
8879

89-
tcpServer.on('connection', (socket) => {
80+
tcpServer.on('connection', async (socket) => {
9081
let databaseId: string | undefined
9182

92-
const connection = new PostgresConnection(socket, {
93-
tls: tlsOptions,
83+
const connection = await fromNodeSocket(socket, {
84+
tls: getTls,
9485
onTlsUpgrade(state) {
95-
if (!state.tlsInfo?.sniServerName || !isValidServername(state.tlsInfo.sniServerName)) {
96-
// connection.detach()
97-
connection.sendError({
86+
if (!state.tlsInfo?.serverName || !isValidServername(state.tlsInfo.serverName)) {
87+
throw BackendError.create({
9888
code: '08006',
9989
message: 'invalid SNI',
10090
severity: 'FATAL',
10191
})
102-
connection.end()
103-
return
10492
}
10593

106-
const _databaseId = extractDatabaseId(state.tlsInfo.sniServerName!)
94+
const _databaseId = extractDatabaseId(state.tlsInfo.serverName!)
10795

10896
if (!websocketConnections.has(_databaseId!)) {
109-
// connection.detach()
110-
connection.sendError({
97+
throw BackendError.create({
11198
code: 'XX000',
11299
message: 'the browser is not sharing the database',
113100
severity: 'FATAL',
114101
})
115-
connection.end()
116-
return
117102
}
118103

119104
if (tcpConnections.has(_databaseId)) {
120-
// connection.detach()
121-
connection.sendError({
105+
throw BackendError.create({
122106
code: '53300',
123107
message: 'sorry, too many clients already',
124108
severity: 'FATAL',
125109
})
126-
connection.end()
127-
return
128110
}
129111

130112
// only set the databaseId after we've verified the connection
131113
databaseId = _databaseId
132-
tcpConnections.set(databaseId!, connection.socket)
114+
tcpConnections.set(databaseId!, connection)
133115
},
134116
serverVersion() {
135117
return '16.3'
@@ -138,13 +120,11 @@ tcpServer.on('connection', (socket) => {
138120
const websocket = websocketConnections.get(databaseId!)
139121

140122
if (!websocket) {
141-
connection.sendError({
123+
throw BackendError.create({
142124
code: 'XX000',
143125
message: 'the browser is not sharing the database',
144126
severity: 'FATAL',
145127
})
146-
connection.end()
147-
return
148128
}
149129

150130
const clientIpMessage = createStartupMessage('postgres', 'postgres', {
@@ -160,13 +140,11 @@ tcpServer.on('connection', (socket) => {
160140
const websocket = websocketConnections.get(databaseId!)
161141

162142
if (!websocket) {
163-
connection.sendError({
143+
throw BackendError.create({
164144
code: 'XX000',
165145
message: 'the browser is not sharing the database',
166146
severity: 'FATAL',
167147
})
168-
connection.end()
169-
return
170148
}
171149

172150
debug('tcp message', { message })

apps/browser-proxy/src/tls.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { Buffer } from 'node:buffer'
22
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3'
3+
import pMemoize from 'p-memoize'
4+
import ExpiryMap from 'expiry-map'
5+
import type { Server } from 'node:https'
36

47
const s3Client = new S3Client({ forcePathStyle: true })
58

6-
export async function getTls() {
9+
async function _getTls() {
710
const cert = await s3Client
811
.send(
912
new GetObjectCommand({
@@ -31,3 +34,12 @@ export async function getTls() {
3134
key: Buffer.from(key),
3235
}
3336
}
37+
38+
// cache the TLS certificate for 1 week
39+
const cache = new ExpiryMap(1000 * 60 * 60 * 24 * 7)
40+
export const getTls = pMemoize(_getTls, { cache })
41+
42+
export async function setSecureContext(httpsServer: Server) {
43+
const tlsOptions = await getTls()
44+
httpsServer.setSecureContext(tlsOptions)
45+
}

apps/postgres-new/components/app-provider.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,17 @@ export default function AppProvider({ children }: AppProps) {
138138
if (isStartupMessage(message)) {
139139
const parameters = parseStartupMessage(message)
140140
if ('client_ip' in parameters) {
141-
setConnectedClientIp(parameters.client_ip === '' ? null : parameters.client_ip)
141+
// client disconnected
142+
if (parameters.client_ip === '') {
143+
setConnectedClientIp(null)
144+
// we ensure we're not in a transaction block first
145+
await db.sql`rollback;`.catch()
146+
// we clean the session state, see: https://www.pgbouncer.org/faq.html#how-to-use-prepared-statements-with-session-pooling
147+
// we do this to avoid having old prepared statements in the session
148+
await db.sql`discard all;`
149+
} else {
150+
setConnectedClientIp(parameters.client_ip)
151+
}
142152
}
143153
return
144154
}

apps/postgres-new/components/live-share-icon.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ export const LiveShareIcon = (props: { size?: number; className?: string }) => (
1111
className={cx('lucide lucide-live-share', props.className)}
1212
>
1313
<path
14-
fill-rule="evenodd"
15-
clip-rule="evenodd"
14+
fillRule="evenodd"
15+
clipRule="evenodd"
1616
d="M13.735 1.694L15.178 1l8.029 6.328v1.388l-8.029 6.072-1.443-.694v-2.776h-.59c-4.06-.02-6.71.104-10.61 5.163l-1.534-.493a8.23 8.23 0 0 1 .271-2.255 11.026 11.026 0 0 1 3.92-6.793 11.339 11.339 0 0 1 7.502-2.547h1.04v-2.7zm1.804 7.917v2.776l5.676-4.281-5.648-4.545v2.664h-2.86A9.299 9.299 0 0 0 5.77 8.848a10.444 10.444 0 0 0-2.401 4.122c3.351-3.213 6.19-3.359 9.798-3.359h2.373zm-7.647 5.896a4.31 4.31 0 1 1 4.788 7.166 4.31 4.31 0 0 1-4.788-7.166zm.955 5.728a2.588 2.588 0 1 0 2.878-4.302 2.588 2.588 0 0 0-2.878 4.302z"
1717
/>
1818
</svg>

package-lock.json

Lines changed: 79 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)