pgserve 2.2.4 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/pgserve-wrapper.cjs +5 -4
- package/bin/postgres-server.js +142 -631
- package/config/logrotate.d/pgserve +47 -0
- package/config/pgaudit.conf +31 -0
- package/package.json +2 -2
- package/scripts/test-npx.sh +32 -10
- package/src/cli-install.cjs +147 -77
- package/src/commands/uninstall.js +241 -0
- package/src/index.js +11 -44
- package/src/lib/admin-json.js +202 -0
- package/src/lib/pm2-args.js +119 -0
- package/src/lib/socket-dir.js +69 -0
- package/src/postgres.js +64 -5
- package/src/admin-client.js +0 -223
- package/src/audit.js +0 -168
- package/src/cluster.js +0 -654
- package/src/control-db.js +0 -330
- package/src/daemon-control.js +0 -468
- package/src/daemon-shared.js +0 -18
- package/src/daemon-tcp.js +0 -297
- package/src/daemon.js +0 -709
- package/src/dashboard.js +0 -217
- package/src/fingerprint.js +0 -479
- package/src/gc.js +0 -351
- package/src/pg-wire.js +0 -869
- package/src/protocol.js +0 -389
- package/src/restore.js +0 -574
- package/src/router.js +0 -546
- package/src/sdk.js +0 -137
- package/src/stats-collector.js +0 -453
- package/src/stats-dashboard.js +0 -401
- package/src/sync.js +0 -335
- package/src/tenancy.js +0 -75
- package/src/tokens.js +0 -102
package/src/daemon-tcp.js
DELETED
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pgserve daemon — opt-in TCP accept path (Group 6).
|
|
3
|
-
*
|
|
4
|
-
* Bound only when `pgserve daemon --listen <host:port>` is set. TCP peers
|
|
5
|
-
* cannot use SO_PEERCRED, so identity is established via a bearer token
|
|
6
|
-
* presented in `application_name` shaped `?fingerprint=<12hex>&token=<bearer>`.
|
|
7
|
-
*
|
|
8
|
-
* Methods are attached to `PgserveDaemon.prototype` from `daemon.js` so
|
|
9
|
-
* the surface stays one cohesive class — the split is purely to honour
|
|
10
|
-
* the 1000-line discipline (AGENTS.md §8).
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/* global Bun */
|
|
14
|
-
import fs from 'fs';
|
|
15
|
-
import { extractApplicationName, rewriteDatabaseName } from './protocol.js';
|
|
16
|
-
import { audit, AUDIT_EVENTS } from './audit.js';
|
|
17
|
-
import { verifyToken } from './control-db.js';
|
|
18
|
-
import { parseTcpAuth, hashToken } from './tokens.js';
|
|
19
|
-
import { flushPending } from './daemon-shared.js';
|
|
20
|
-
|
|
21
|
-
const PROTOCOL_VERSION_3 = 196608;
|
|
22
|
-
const SSL_REQUEST_CODE = 80877103;
|
|
23
|
-
const GSSAPI_REQUEST_CODE = 80877104;
|
|
24
|
-
const CANCEL_REQUEST_CODE = 80877102;
|
|
25
|
-
|
|
26
|
-
const MAX_STARTUP_BUFFER_SIZE = 1024 * 1024; // 1 MiB — same bound as router.js
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Install the TCP accept handlers on PgserveDaemon.prototype.
|
|
30
|
-
* Called once from daemon.js at module load.
|
|
31
|
-
*/
|
|
32
|
-
export function attachTcpHandlers(PgserveDaemon) {
|
|
33
|
-
PgserveDaemon.prototype.bindTcpListener = bindTcpListener;
|
|
34
|
-
PgserveDaemon.prototype.handleTcpOpen = handleTcpOpen;
|
|
35
|
-
PgserveDaemon.prototype.handleTcpData = handleTcpData;
|
|
36
|
-
PgserveDaemon.prototype.processTcpStartupMessage = processTcpStartupMessage;
|
|
37
|
-
PgserveDaemon.prototype.handleTcpClose = handleTcpClose;
|
|
38
|
-
PgserveDaemon.prototype.handleTcpError = handleTcpError;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function bindTcpListener({ host, port }) {
|
|
42
|
-
const daemon = this;
|
|
43
|
-
return Bun.listen({
|
|
44
|
-
hostname: host,
|
|
45
|
-
port,
|
|
46
|
-
socket: {
|
|
47
|
-
data(socket, data) { daemon.handleTcpData(socket, data); },
|
|
48
|
-
open(socket) { daemon.handleTcpOpen(socket); },
|
|
49
|
-
close(socket) { daemon.handleTcpClose(socket); },
|
|
50
|
-
error(socket, error) { daemon.handleTcpError(socket, error); },
|
|
51
|
-
drain(socket) {
|
|
52
|
-
const state = daemon.socketState.get(socket);
|
|
53
|
-
if (!state) return;
|
|
54
|
-
if (state.pendingToClient) {
|
|
55
|
-
state.pendingToClient = flushPending(socket, state.pendingToClient);
|
|
56
|
-
}
|
|
57
|
-
if (!state.pendingToClient && state.pgSocket) {
|
|
58
|
-
state.pgSocket.resume();
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function handleTcpOpen(socket) {
|
|
66
|
-
// TCP peers cannot use SO_PEERCRED; identity is established via the
|
|
67
|
-
// application_name token in the startup message.
|
|
68
|
-
this.socketState.set(socket, {
|
|
69
|
-
transport: 'tcp',
|
|
70
|
-
buffer: null,
|
|
71
|
-
pgSocket: null,
|
|
72
|
-
dbName: null,
|
|
73
|
-
handshakeComplete: false,
|
|
74
|
-
startupInProgress: false,
|
|
75
|
-
pendingToPg: null,
|
|
76
|
-
pendingToClient: null,
|
|
77
|
-
fingerprint: null,
|
|
78
|
-
tokenId: null,
|
|
79
|
-
});
|
|
80
|
-
this.connections.add(socket);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function handleTcpData(socket, data) {
|
|
84
|
-
const state = this.socketState.get(socket);
|
|
85
|
-
if (!state) return;
|
|
86
|
-
|
|
87
|
-
if (state.handshakeComplete && state.pgSocket) {
|
|
88
|
-
if (state.pendingToPg) {
|
|
89
|
-
state.pendingToPg = Buffer.concat([state.pendingToPg, Buffer.from(data)]);
|
|
90
|
-
socket.pause();
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
const written = state.pgSocket.write(data);
|
|
94
|
-
if (written < data.byteLength) {
|
|
95
|
-
state.pendingToPg = written === 0 ? Buffer.from(data) : Buffer.from(data.subarray(written));
|
|
96
|
-
socket.pause();
|
|
97
|
-
}
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const incomingSize = state.buffer ? state.buffer.length + data.byteLength : data.byteLength;
|
|
102
|
-
if (incomingSize > MAX_STARTUP_BUFFER_SIZE) {
|
|
103
|
-
this.logger.warn?.(
|
|
104
|
-
{ incomingSize, limit: MAX_STARTUP_BUFFER_SIZE },
|
|
105
|
-
'TCP pre-handshake buffer exceeded limit — closing connection',
|
|
106
|
-
);
|
|
107
|
-
socket.end();
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
state.buffer = state.buffer ? Buffer.concat([state.buffer, Buffer.from(data)]) : Buffer.from(data);
|
|
111
|
-
|
|
112
|
-
this.processTcpStartupMessage(socket, state).catch((err) => {
|
|
113
|
-
this.logger.error?.({ err: err.message }, 'TCP processStartupMessage failed');
|
|
114
|
-
try { socket.end(); } catch { /* swallow */ }
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async function processTcpStartupMessage(socket, state) {
|
|
119
|
-
if (state.startupInProgress) return;
|
|
120
|
-
const buffer = state.buffer;
|
|
121
|
-
if (!buffer || buffer.length < 8) return;
|
|
122
|
-
const messageLength = buffer.readUInt32BE(0);
|
|
123
|
-
if (buffer.length < messageLength) return;
|
|
124
|
-
const code = buffer.readUInt32BE(4);
|
|
125
|
-
|
|
126
|
-
if (code === SSL_REQUEST_CODE || code === GSSAPI_REQUEST_CODE) {
|
|
127
|
-
socket.write(Buffer.from('N'));
|
|
128
|
-
state.buffer = buffer.length > messageLength ? buffer.subarray(messageLength) : null;
|
|
129
|
-
if (state.buffer) await processTcpStartupMessage.call(this, socket, state);
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
if (code === CANCEL_REQUEST_CODE) {
|
|
133
|
-
socket.end();
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
if (code !== PROTOCOL_VERSION_3) {
|
|
137
|
-
this.logger.warn?.({ code }, 'TCP unsupported protocol version');
|
|
138
|
-
socket.end();
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const startupMessage = buffer.subarray(0, messageLength);
|
|
143
|
-
const applicationName = extractApplicationName(startupMessage);
|
|
144
|
-
const auth = parseTcpAuth(applicationName);
|
|
145
|
-
state.startupInProgress = true;
|
|
146
|
-
|
|
147
|
-
// Validate before opening any PG socket. The denied path emits exactly
|
|
148
|
-
// one audit event then closes — the peer gets no oracle distinguishing
|
|
149
|
-
// "unknown fingerprint" from "bad token".
|
|
150
|
-
let validated = null;
|
|
151
|
-
try {
|
|
152
|
-
if (auth && this._adminClient) {
|
|
153
|
-
const tokenHash = hashToken(auth.token);
|
|
154
|
-
validated = await verifyToken(this._adminClient, {
|
|
155
|
-
fingerprint: auth.fingerprint,
|
|
156
|
-
tokenHash,
|
|
157
|
-
}, { timeoutMs: this.adminLookupTimeoutMs });
|
|
158
|
-
}
|
|
159
|
-
} catch (err) {
|
|
160
|
-
this.logger.warn?.({ err: err.message }, 'verifyToken failed');
|
|
161
|
-
validated = null;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (!validated) {
|
|
165
|
-
audit(AUDIT_EVENTS.TCP_TOKEN_DENIED, {
|
|
166
|
-
fingerprint: auth?.fingerprint || null,
|
|
167
|
-
remote_address: socket.remoteAddress || null,
|
|
168
|
-
reason: !auth ? 'missing_or_malformed_application_name' : 'token_unknown',
|
|
169
|
-
});
|
|
170
|
-
try { socket.end(); } catch { /* swallow */ }
|
|
171
|
-
state.startupInProgress = false;
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
state.fingerprint = auth.fingerprint;
|
|
176
|
-
state.tokenId = validated.tokenId;
|
|
177
|
-
state.dbName = validated.databaseName;
|
|
178
|
-
|
|
179
|
-
audit(AUDIT_EVENTS.TCP_TOKEN_USED, {
|
|
180
|
-
fingerprint: auth.fingerprint,
|
|
181
|
-
token_id: validated.tokenId,
|
|
182
|
-
database: validated.databaseName,
|
|
183
|
-
remote_address: socket.remoteAddress || null,
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// Force the peer into its fingerprint's database — even if the libpq
|
|
187
|
-
// client asked for something else. Drop application_name on the way
|
|
188
|
-
// through: the auth blob easily exceeds Postgres' 63-char NAMEDATALEN
|
|
189
|
-
// and would otherwise trigger a truncation NOTICE on every connect.
|
|
190
|
-
let outgoingStartup;
|
|
191
|
-
try {
|
|
192
|
-
outgoingStartup = rewriteDatabaseName(startupMessage, validated.databaseName, {
|
|
193
|
-
dropParams: ['application_name'],
|
|
194
|
-
});
|
|
195
|
-
} catch (err) {
|
|
196
|
-
this.logger.error?.({ err: err.message }, 'rewriteDatabaseName failed for TCP peer');
|
|
197
|
-
try { socket.end(); } catch { /* swallow */ }
|
|
198
|
-
state.startupInProgress = false;
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
try {
|
|
203
|
-
if (this.autoProvision) {
|
|
204
|
-
await this.pgManager.createDatabase(validated.databaseName);
|
|
205
|
-
}
|
|
206
|
-
const pgSocketPath = this.pgManager.getSocketPath();
|
|
207
|
-
const daemon = this;
|
|
208
|
-
const pgHandler = {
|
|
209
|
-
data(_pgSocket, pgData) {
|
|
210
|
-
if (state.pendingToClient) {
|
|
211
|
-
state.pendingToClient = Buffer.concat([state.pendingToClient, Buffer.from(pgData)]);
|
|
212
|
-
_pgSocket.pause();
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
const written = socket.write(pgData);
|
|
216
|
-
if (written < pgData.byteLength) {
|
|
217
|
-
state.pendingToClient = written === 0
|
|
218
|
-
? Buffer.from(pgData)
|
|
219
|
-
: Buffer.from(pgData.subarray(written));
|
|
220
|
-
_pgSocket.pause();
|
|
221
|
-
}
|
|
222
|
-
},
|
|
223
|
-
open(pgSocket) {
|
|
224
|
-
pgSocket.write(outgoingStartup);
|
|
225
|
-
state.handshakeComplete = true;
|
|
226
|
-
},
|
|
227
|
-
close() {
|
|
228
|
-
try { socket.end(); } catch { /* swallow */ }
|
|
229
|
-
},
|
|
230
|
-
error(_pgSocket, error) {
|
|
231
|
-
daemon.logger.error?.(
|
|
232
|
-
{ dbName: validated.databaseName, err: error?.message || String(error) },
|
|
233
|
-
'TCP-side PG proxy socket error',
|
|
234
|
-
);
|
|
235
|
-
try { socket.end(); } catch { /* swallow */ }
|
|
236
|
-
},
|
|
237
|
-
drain(_pgSocket) {
|
|
238
|
-
if (state.pendingToPg) {
|
|
239
|
-
state.pendingToPg = flushPending(_pgSocket, state.pendingToPg);
|
|
240
|
-
}
|
|
241
|
-
if (!state.pendingToPg) {
|
|
242
|
-
socket.resume();
|
|
243
|
-
}
|
|
244
|
-
},
|
|
245
|
-
};
|
|
246
|
-
|
|
247
|
-
const useUnix = pgSocketPath && fs.existsSync(pgSocketPath);
|
|
248
|
-
if (useUnix) {
|
|
249
|
-
state.pgSocket = await Bun.connect({ unix: pgSocketPath, socket: pgHandler });
|
|
250
|
-
} else {
|
|
251
|
-
state.pgSocket = await Bun.connect({
|
|
252
|
-
hostname: '127.0.0.1',
|
|
253
|
-
port: this.pgManager.port,
|
|
254
|
-
socket: pgHandler,
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
this.emit('tcp-connection', { dbName: validated.databaseName, fingerprint: auth.fingerprint });
|
|
258
|
-
} catch (err) {
|
|
259
|
-
this.logger.error?.(
|
|
260
|
-
{ dbName: validated.databaseName, err: err?.message || String(err) },
|
|
261
|
-
'TCP daemon connection error',
|
|
262
|
-
);
|
|
263
|
-
try { socket.end(); } catch { /* swallow */ }
|
|
264
|
-
this.emit('connection-error', { error: err, dbName: validated.databaseName });
|
|
265
|
-
} finally {
|
|
266
|
-
state.startupInProgress = false;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function handleTcpClose(socket) {
|
|
271
|
-
const state = this.socketState.get(socket);
|
|
272
|
-
if (state) {
|
|
273
|
-
state.pendingToPg = null;
|
|
274
|
-
state.pendingToClient = null;
|
|
275
|
-
if (state.pgSocket) {
|
|
276
|
-
try { state.pgSocket.end(); } catch { /* swallow */ }
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
this.connections.delete(socket);
|
|
280
|
-
this.socketState.delete(socket);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function handleTcpError(socket, error) {
|
|
284
|
-
if (error?.code !== 'ECONNRESET') {
|
|
285
|
-
this.logger.error?.({ err: error?.message || String(error) }, 'TCP socket error');
|
|
286
|
-
}
|
|
287
|
-
const state = this.socketState.get(socket);
|
|
288
|
-
if (state) {
|
|
289
|
-
state.pendingToPg = null;
|
|
290
|
-
state.pendingToClient = null;
|
|
291
|
-
if (state.pgSocket) {
|
|
292
|
-
try { state.pgSocket.end(); } catch { /* swallow */ }
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
this.connections.delete(socket);
|
|
296
|
-
this.socketState.delete(socket);
|
|
297
|
-
}
|