@slates-integrations/postgresql 0.2.0-rc.6 → 0.2.0-rc.9
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/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/package.json +3 -0
- package/dist/sourcemap-register.cjs +1 -0
- package/package.json +9 -3
- package/src/auth.ts +0 -134
- package/src/config.ts +0 -16
- package/src/index.ts +0 -36
- package/src/lib/client.ts +0 -799
- package/src/lib/errors.ts +0 -55
- package/src/lib/helpers.ts +0 -48
- package/src/lib/protocol.ts +0 -336
- package/src/spec.ts +0 -13
- package/src/tools/delete-rows.ts +0 -84
- package/src/tools/describe-table.ts +0 -231
- package/src/tools/execute-query.ts +0 -95
- package/src/tools/index.ts +0 -12
- package/src/tools/insert-rows.ts +0 -133
- package/src/tools/list-schemas.ts +0 -80
- package/src/tools/list-tables.ts +0 -119
- package/src/tools/manage-indexes.ts +0 -99
- package/src/tools/manage-roles.ts +0 -214
- package/src/tools/manage-schemas.ts +0 -121
- package/src/tools/manage-table.ts +0 -267
- package/src/tools/manage-views.ts +0 -193
- package/src/tools/update-rows.ts +0 -98
- package/src/triggers/inbound-webhook.ts +0 -67
- package/src/triggers/index.ts +0 -2
- package/src/triggers/table-changes.ts +0 -123
- package/tsconfig.json +0 -23
package/src/lib/client.ts
DELETED
|
@@ -1,799 +0,0 @@
|
|
|
1
|
-
import * as crypto from 'crypto';
|
|
2
|
-
import * as net from 'net';
|
|
3
|
-
import * as tls from 'tls';
|
|
4
|
-
import {
|
|
5
|
-
AuthenticationTypes,
|
|
6
|
-
MessageTypes,
|
|
7
|
-
MessageWriter,
|
|
8
|
-
buildMessage,
|
|
9
|
-
buildPasswordMessage,
|
|
10
|
-
buildQueryMessage,
|
|
11
|
-
buildStartupMessage,
|
|
12
|
-
buildTerminateMessage,
|
|
13
|
-
oidToTypeName,
|
|
14
|
-
parseCommandComplete,
|
|
15
|
-
parseDataRow,
|
|
16
|
-
parseErrorFields,
|
|
17
|
-
parseMessages,
|
|
18
|
-
parseRowDescription,
|
|
19
|
-
type ColumnDescription,
|
|
20
|
-
type ParsedMessage
|
|
21
|
-
} from './protocol';
|
|
22
|
-
import {
|
|
23
|
-
postgresFieldsError,
|
|
24
|
-
postgresServiceError,
|
|
25
|
-
postgresUpstreamError,
|
|
26
|
-
toPostgresServiceError
|
|
27
|
-
} from './errors';
|
|
28
|
-
|
|
29
|
-
export interface ConnectionConfig {
|
|
30
|
-
host: string;
|
|
31
|
-
port: number;
|
|
32
|
-
database: string;
|
|
33
|
-
username: string;
|
|
34
|
-
password: string;
|
|
35
|
-
sslMode: 'disable' | 'require' | 'verify-ca' | 'verify-full';
|
|
36
|
-
queryTimeout?: number;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface QueryResult {
|
|
40
|
-
columns: { name: string; type: string; typeOid: number }[];
|
|
41
|
-
rows: Record<string, any>[];
|
|
42
|
-
rowCount: number | null;
|
|
43
|
-
command: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export class PostgresClient {
|
|
47
|
-
private config: ConnectionConfig;
|
|
48
|
-
|
|
49
|
-
constructor(config: ConnectionConfig) {
|
|
50
|
-
this.config = config;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async query(sql: string, timeoutMs?: number): Promise<QueryResult> {
|
|
54
|
-
let timeout = timeoutMs || this.config.queryTimeout || 30000;
|
|
55
|
-
let socket = await this.connect(timeout);
|
|
56
|
-
|
|
57
|
-
try {
|
|
58
|
-
let result = await this.executeQuery(socket, sql, timeout);
|
|
59
|
-
await this.terminate(socket);
|
|
60
|
-
return result;
|
|
61
|
-
} catch (err) {
|
|
62
|
-
socket.destroy();
|
|
63
|
-
throw toPostgresServiceError(err, 'PostgreSQL query failed');
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async multiQuery(statements: string[], timeoutMs?: number): Promise<QueryResult[]> {
|
|
68
|
-
let timeout = timeoutMs || this.config.queryTimeout || 30000;
|
|
69
|
-
let socket = await this.connect(timeout);
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
let results: QueryResult[] = [];
|
|
73
|
-
for (let sql of statements) {
|
|
74
|
-
let result = await this.executeQuery(socket, sql, timeout);
|
|
75
|
-
results.push(result);
|
|
76
|
-
}
|
|
77
|
-
await this.terminate(socket);
|
|
78
|
-
return results;
|
|
79
|
-
} catch (err) {
|
|
80
|
-
socket.destroy();
|
|
81
|
-
throw toPostgresServiceError(err, 'PostgreSQL query failed');
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
private connect(timeoutMs: number): Promise<net.Socket> {
|
|
86
|
-
return new Promise((resolve, reject) => {
|
|
87
|
-
let socket: net.Socket;
|
|
88
|
-
|
|
89
|
-
let timer = setTimeout(() => {
|
|
90
|
-
socket.destroy();
|
|
91
|
-
reject(
|
|
92
|
-
postgresUpstreamError(`PostgreSQL connection timeout after ${timeoutMs}ms`, {
|
|
93
|
-
reason: 'postgresql_connection_timeout'
|
|
94
|
-
})
|
|
95
|
-
);
|
|
96
|
-
}, timeoutMs);
|
|
97
|
-
|
|
98
|
-
let handleConnection = async (sock: net.Socket) => {
|
|
99
|
-
try {
|
|
100
|
-
await this.authenticate(sock, timeoutMs);
|
|
101
|
-
clearTimeout(timer);
|
|
102
|
-
resolve(sock);
|
|
103
|
-
} catch (err) {
|
|
104
|
-
clearTimeout(timer);
|
|
105
|
-
sock.destroy();
|
|
106
|
-
reject(toPostgresServiceError(err, 'PostgreSQL authentication failed'));
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
if (this.config.sslMode !== 'disable') {
|
|
111
|
-
// First establish a plain TCP connection, then upgrade to TLS
|
|
112
|
-
socket = net.createConnection({
|
|
113
|
-
host: this.config.host,
|
|
114
|
-
port: this.config.port
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
socket.once('error', err => {
|
|
118
|
-
clearTimeout(timer);
|
|
119
|
-
reject(
|
|
120
|
-
postgresUpstreamError(`PostgreSQL connection failed: ${err.message}`, {
|
|
121
|
-
reason: 'postgresql_connection_failed',
|
|
122
|
-
parent: err
|
|
123
|
-
})
|
|
124
|
-
);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
socket.once('connect', () => {
|
|
128
|
-
// Send SSL request
|
|
129
|
-
let sslRequest = new Uint8Array(8);
|
|
130
|
-
let view = new DataView(sslRequest.buffer);
|
|
131
|
-
view.setInt32(0, 8, false); // message length
|
|
132
|
-
view.setInt32(4, 80877103, false); // SSL request code
|
|
133
|
-
socket.write(sslRequest);
|
|
134
|
-
|
|
135
|
-
socket.once('data', response => {
|
|
136
|
-
let responseChar = String.fromCharCode(response[0]!);
|
|
137
|
-
if (responseChar === 'S') {
|
|
138
|
-
// Server supports SSL, upgrade connection
|
|
139
|
-
let tlsOptions: tls.ConnectionOptions = {
|
|
140
|
-
socket: socket,
|
|
141
|
-
rejectUnauthorized:
|
|
142
|
-
this.config.sslMode === 'verify-ca' || this.config.sslMode === 'verify-full',
|
|
143
|
-
servername:
|
|
144
|
-
this.config.sslMode === 'verify-full' ? this.config.host : undefined
|
|
145
|
-
};
|
|
146
|
-
let tlsSocket = tls.connect(tlsOptions);
|
|
147
|
-
tlsSocket.once('secureConnect', () => {
|
|
148
|
-
handleConnection(tlsSocket as any);
|
|
149
|
-
});
|
|
150
|
-
tlsSocket.once('error', err => {
|
|
151
|
-
clearTimeout(timer);
|
|
152
|
-
reject(
|
|
153
|
-
postgresUpstreamError(`PostgreSQL SSL connection failed: ${err.message}`, {
|
|
154
|
-
reason: 'postgresql_ssl_failed',
|
|
155
|
-
parent: err
|
|
156
|
-
})
|
|
157
|
-
);
|
|
158
|
-
});
|
|
159
|
-
} else if (responseChar === 'N') {
|
|
160
|
-
clearTimeout(timer);
|
|
161
|
-
socket.destroy();
|
|
162
|
-
reject(
|
|
163
|
-
postgresUpstreamError(
|
|
164
|
-
`PostgreSQL server does not support SSL connections for sslmode=${this.config.sslMode}`,
|
|
165
|
-
{
|
|
166
|
-
reason: 'postgresql_ssl_unsupported'
|
|
167
|
-
}
|
|
168
|
-
)
|
|
169
|
-
);
|
|
170
|
-
} else {
|
|
171
|
-
clearTimeout(timer);
|
|
172
|
-
socket.destroy();
|
|
173
|
-
reject(
|
|
174
|
-
postgresUpstreamError(`Unexpected PostgreSQL SSL response: ${responseChar}`, {
|
|
175
|
-
reason: 'postgresql_ssl_unexpected_response'
|
|
176
|
-
})
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
} else {
|
|
182
|
-
socket = net.createConnection({
|
|
183
|
-
host: this.config.host,
|
|
184
|
-
port: this.config.port
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
socket.once('error', err => {
|
|
188
|
-
clearTimeout(timer);
|
|
189
|
-
reject(
|
|
190
|
-
postgresUpstreamError(`PostgreSQL connection failed: ${err.message}`, {
|
|
191
|
-
reason: 'postgresql_connection_failed',
|
|
192
|
-
parent: err
|
|
193
|
-
})
|
|
194
|
-
);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
socket.once('connect', () => {
|
|
198
|
-
handleConnection(socket);
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
private authenticate(socket: net.Socket, timeoutMs: number): Promise<void> {
|
|
205
|
-
return new Promise((resolve, reject) => {
|
|
206
|
-
let buffer: Uint8Array = new Uint8Array(0);
|
|
207
|
-
let authenticated = false;
|
|
208
|
-
|
|
209
|
-
let timer = setTimeout(() => {
|
|
210
|
-
cleanup();
|
|
211
|
-
reject(
|
|
212
|
-
postgresUpstreamError(`PostgreSQL authentication timeout after ${timeoutMs}ms`, {
|
|
213
|
-
reason: 'postgresql_auth_timeout'
|
|
214
|
-
})
|
|
215
|
-
);
|
|
216
|
-
}, timeoutMs);
|
|
217
|
-
|
|
218
|
-
let cleanup = () => {
|
|
219
|
-
clearTimeout(timer);
|
|
220
|
-
socket.removeListener('data', onData);
|
|
221
|
-
socket.removeListener('error', onError);
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
let onError = (err: Error) => {
|
|
225
|
-
cleanup();
|
|
226
|
-
reject(
|
|
227
|
-
postgresUpstreamError(`PostgreSQL authentication error: ${err.message}`, {
|
|
228
|
-
reason: 'postgresql_auth_error',
|
|
229
|
-
parent: err
|
|
230
|
-
})
|
|
231
|
-
);
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
let onData = (data: Uint8Array) => {
|
|
235
|
-
let newBuffer = new Uint8Array(buffer.length + data.length);
|
|
236
|
-
newBuffer.set(buffer);
|
|
237
|
-
newBuffer.set(data, buffer.length);
|
|
238
|
-
buffer = newBuffer;
|
|
239
|
-
|
|
240
|
-
let { messages, remaining } = parseMessages(buffer);
|
|
241
|
-
buffer = remaining;
|
|
242
|
-
|
|
243
|
-
for (let msg of messages) {
|
|
244
|
-
if (msg.type === MessageTypes.AUTHENTICATION) {
|
|
245
|
-
let view = new DataView(msg.body.buffer, msg.body.byteOffset, msg.body.byteLength);
|
|
246
|
-
let authType = view.getInt32(0, false);
|
|
247
|
-
|
|
248
|
-
if (authType === AuthenticationTypes.OK) {
|
|
249
|
-
authenticated = true;
|
|
250
|
-
} else if (authType === AuthenticationTypes.CLEARTEXT_PASSWORD) {
|
|
251
|
-
socket.write(buildPasswordMessage(this.config.password));
|
|
252
|
-
} else if (authType === AuthenticationTypes.MD5_PASSWORD) {
|
|
253
|
-
let salt = msg.body.slice(4, 8);
|
|
254
|
-
let md5Password = this.computeMd5Password(
|
|
255
|
-
this.config.username,
|
|
256
|
-
this.config.password,
|
|
257
|
-
salt
|
|
258
|
-
);
|
|
259
|
-
socket.write(buildPasswordMessage(md5Password));
|
|
260
|
-
} else if (authType === AuthenticationTypes.SASL) {
|
|
261
|
-
cleanup();
|
|
262
|
-
this.handleSaslAuth(socket, msg, buffer, timeoutMs)
|
|
263
|
-
.then(() => {
|
|
264
|
-
resolve();
|
|
265
|
-
})
|
|
266
|
-
.catch(err => {
|
|
267
|
-
reject(toPostgresServiceError(err, 'PostgreSQL SASL authentication failed'));
|
|
268
|
-
});
|
|
269
|
-
return;
|
|
270
|
-
} else {
|
|
271
|
-
cleanup();
|
|
272
|
-
reject(
|
|
273
|
-
postgresServiceError(
|
|
274
|
-
`Unsupported PostgreSQL authentication method: ${authType}`
|
|
275
|
-
)
|
|
276
|
-
);
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
} else if (msg.type === MessageTypes.ERROR_RESPONSE) {
|
|
280
|
-
let fields = parseErrorFields(msg.body);
|
|
281
|
-
cleanup();
|
|
282
|
-
reject(postgresFieldsError(fields));
|
|
283
|
-
return;
|
|
284
|
-
} else if (msg.type === MessageTypes.READY_FOR_QUERY) {
|
|
285
|
-
if (authenticated) {
|
|
286
|
-
cleanup();
|
|
287
|
-
resolve();
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
// Ignore ParameterStatus, BackendKeyData, NoticeResponse
|
|
292
|
-
}
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
socket.on('data', onData);
|
|
296
|
-
socket.on('error', onError);
|
|
297
|
-
|
|
298
|
-
// Send startup message
|
|
299
|
-
socket.write(buildStartupMessage(this.config.username, this.config.database));
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
private computeMd5Password(username: string, password: string, salt: Uint8Array): string {
|
|
304
|
-
let inner = crypto
|
|
305
|
-
.createHash('md5')
|
|
306
|
-
.update(password + username)
|
|
307
|
-
.digest('hex');
|
|
308
|
-
let outer = crypto.createHash('md5').update(inner).update(salt).digest('hex');
|
|
309
|
-
return 'md5' + outer;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
private async handleSaslAuth(
|
|
313
|
-
socket: net.Socket,
|
|
314
|
-
initialMsg: ParsedMessage,
|
|
315
|
-
currentBuffer: Uint8Array,
|
|
316
|
-
timeoutMs: number
|
|
317
|
-
): Promise<{ remaining: Uint8Array }> {
|
|
318
|
-
// Parse available SASL mechanisms
|
|
319
|
-
let decoder = new TextDecoder();
|
|
320
|
-
let mechanisms: string[] = [];
|
|
321
|
-
let offset = 4; // skip auth type int
|
|
322
|
-
while (offset < initialMsg.body.length) {
|
|
323
|
-
let end = initialMsg.body.indexOf(0, offset);
|
|
324
|
-
if (end === -1 || end === offset) break;
|
|
325
|
-
mechanisms.push(decoder.decode(initialMsg.body.slice(offset, end)));
|
|
326
|
-
offset = end + 1;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (!mechanisms.includes('SCRAM-SHA-256')) {
|
|
330
|
-
throw postgresServiceError(
|
|
331
|
-
`Unsupported SASL mechanisms: ${mechanisms.join(', ')}. Only SCRAM-SHA-256 is supported.`
|
|
332
|
-
);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// SCRAM-SHA-256 authentication
|
|
336
|
-
let nonce = crypto.randomBytes(18).toString('base64');
|
|
337
|
-
let clientFirstBare = `n=,r=${nonce}`;
|
|
338
|
-
let clientFirstMessage = `n,,${clientFirstBare}`;
|
|
339
|
-
|
|
340
|
-
// Send SASLInitialResponse
|
|
341
|
-
let encoder = new TextEncoder();
|
|
342
|
-
let mechanismBytes = encoder.encode('SCRAM-SHA-256');
|
|
343
|
-
let clientFirstBytes = encoder.encode(clientFirstMessage);
|
|
344
|
-
let writer = new MessageWriter();
|
|
345
|
-
writer.writeBytes(mechanismBytes);
|
|
346
|
-
writer.writeByte(0); // null terminator for mechanism name
|
|
347
|
-
writer.writeInt32(clientFirstBytes.length);
|
|
348
|
-
writer.writeBytes(clientFirstBytes);
|
|
349
|
-
socket.write(buildMessage(MessageTypes.PASSWORD, writer.toBuffer()));
|
|
350
|
-
|
|
351
|
-
// Wait for SASLContinue
|
|
352
|
-
let saslContinueMsg = await this.waitForMessage(
|
|
353
|
-
socket,
|
|
354
|
-
MessageTypes.AUTHENTICATION,
|
|
355
|
-
currentBuffer,
|
|
356
|
-
timeoutMs
|
|
357
|
-
);
|
|
358
|
-
let continueView = new DataView(
|
|
359
|
-
saslContinueMsg.msg.body.buffer,
|
|
360
|
-
saslContinueMsg.msg.body.byteOffset,
|
|
361
|
-
saslContinueMsg.msg.body.byteLength
|
|
362
|
-
);
|
|
363
|
-
let continueAuthType = continueView.getInt32(0, false);
|
|
364
|
-
if (continueAuthType !== AuthenticationTypes.SASL_CONTINUE) {
|
|
365
|
-
throw postgresUpstreamError(
|
|
366
|
-
`Expected SASL_CONTINUE, got auth type: ${continueAuthType}`,
|
|
367
|
-
{
|
|
368
|
-
reason: 'postgresql_sasl_protocol_error'
|
|
369
|
-
}
|
|
370
|
-
);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
let serverFirstMessage = decoder.decode(saslContinueMsg.msg.body.slice(4));
|
|
374
|
-
let serverParams = this.parseScramMessage(serverFirstMessage);
|
|
375
|
-
let serverNonce = serverParams['r'] || '';
|
|
376
|
-
let saltBase64 = serverParams['s'] || '';
|
|
377
|
-
let iterations = parseInt(serverParams['i'] || '4096', 10);
|
|
378
|
-
|
|
379
|
-
if (!serverNonce.startsWith(nonce)) {
|
|
380
|
-
throw postgresUpstreamError('Server nonce does not start with client nonce', {
|
|
381
|
-
reason: 'postgresql_sasl_protocol_error'
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Compute SCRAM proof
|
|
386
|
-
let saltBytes = this.base64Decode(saltBase64);
|
|
387
|
-
let saltedPassword = this.hi(
|
|
388
|
-
this.normalizePassword(this.config.password),
|
|
389
|
-
saltBytes,
|
|
390
|
-
iterations
|
|
391
|
-
);
|
|
392
|
-
let clientKey = this.hmacSha256(saltedPassword, 'Client Key');
|
|
393
|
-
let storedKey = new Uint8Array(crypto.createHash('sha256').update(clientKey).digest());
|
|
394
|
-
|
|
395
|
-
let channelBinding = btoa('n,,');
|
|
396
|
-
let clientFinalWithoutProof = `c=${channelBinding},r=${serverNonce}`;
|
|
397
|
-
let authMessage = `${clientFirstBare},${serverFirstMessage},${clientFinalWithoutProof}`;
|
|
398
|
-
|
|
399
|
-
let clientSignature = this.hmacSha256(storedKey, authMessage);
|
|
400
|
-
let clientProof = new Uint8Array(clientKey.length);
|
|
401
|
-
for (let i = 0; i < clientKey.length; i++) {
|
|
402
|
-
clientProof[i] = clientKey[i]! ^ clientSignature[i]!;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
let clientFinalMessage = `${clientFinalWithoutProof},p=${this.base64Encode(clientProof)}`;
|
|
406
|
-
|
|
407
|
-
// Send SASLResponse
|
|
408
|
-
let responseBytes = encoder.encode(clientFinalMessage);
|
|
409
|
-
socket.write(buildMessage(MessageTypes.PASSWORD, responseBytes));
|
|
410
|
-
|
|
411
|
-
// Wait for SASLFinal
|
|
412
|
-
let saslFinalMsg = await this.waitForMessage(
|
|
413
|
-
socket,
|
|
414
|
-
MessageTypes.AUTHENTICATION,
|
|
415
|
-
saslContinueMsg.remaining,
|
|
416
|
-
timeoutMs
|
|
417
|
-
);
|
|
418
|
-
let finalView = new DataView(
|
|
419
|
-
saslFinalMsg.msg.body.buffer,
|
|
420
|
-
saslFinalMsg.msg.body.byteOffset,
|
|
421
|
-
saslFinalMsg.msg.body.byteLength
|
|
422
|
-
);
|
|
423
|
-
let finalAuthType = finalView.getInt32(0, false);
|
|
424
|
-
|
|
425
|
-
if (finalAuthType === AuthenticationTypes.SASL_FINAL) {
|
|
426
|
-
let serverFinalMessage = decoder.decode(saslFinalMsg.msg.body.slice(4));
|
|
427
|
-
let serverFinalParams = this.parseScramMessage(serverFinalMessage);
|
|
428
|
-
let serverSignature = serverFinalParams['v'];
|
|
429
|
-
|
|
430
|
-
// Verify server signature
|
|
431
|
-
let serverKey = this.hmacSha256(saltedPassword, 'Server Key');
|
|
432
|
-
let expectedServerSignature = this.base64Encode(this.hmacSha256(serverKey, authMessage));
|
|
433
|
-
|
|
434
|
-
if (serverSignature !== expectedServerSignature) {
|
|
435
|
-
throw postgresUpstreamError('PostgreSQL server signature verification failed', {
|
|
436
|
-
reason: 'postgresql_sasl_signature_verification_failed'
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// Wait for AuthenticationOk + ReadyForQuery
|
|
442
|
-
let readyBuffer = saslFinalMsg.remaining;
|
|
443
|
-
return new Promise((resolve, reject) => {
|
|
444
|
-
let timer = setTimeout(() => {
|
|
445
|
-
cleanup();
|
|
446
|
-
reject(
|
|
447
|
-
postgresUpstreamError('PostgreSQL SASL auth completion timeout', {
|
|
448
|
-
reason: 'postgresql_sasl_timeout'
|
|
449
|
-
})
|
|
450
|
-
);
|
|
451
|
-
}, timeoutMs);
|
|
452
|
-
|
|
453
|
-
let cleanup = () => {
|
|
454
|
-
clearTimeout(timer);
|
|
455
|
-
socket.removeListener('data', onData);
|
|
456
|
-
socket.removeListener('error', onError);
|
|
457
|
-
};
|
|
458
|
-
|
|
459
|
-
let onError = (err: Error) => {
|
|
460
|
-
cleanup();
|
|
461
|
-
reject(
|
|
462
|
-
postgresUpstreamError(`PostgreSQL SASL auth error: ${err.message}`, {
|
|
463
|
-
reason: 'postgresql_sasl_error',
|
|
464
|
-
parent: err
|
|
465
|
-
})
|
|
466
|
-
);
|
|
467
|
-
};
|
|
468
|
-
|
|
469
|
-
let onData = (data: Uint8Array) => {
|
|
470
|
-
let newBuf = new Uint8Array(readyBuffer.length + data.length);
|
|
471
|
-
newBuf.set(readyBuffer);
|
|
472
|
-
newBuf.set(data, readyBuffer.length);
|
|
473
|
-
readyBuffer = newBuf;
|
|
474
|
-
|
|
475
|
-
let { messages, remaining } = parseMessages(readyBuffer);
|
|
476
|
-
readyBuffer = remaining;
|
|
477
|
-
|
|
478
|
-
for (let msg of messages) {
|
|
479
|
-
if (msg.type === MessageTypes.ERROR_RESPONSE) {
|
|
480
|
-
let fields = parseErrorFields(msg.body);
|
|
481
|
-
cleanup();
|
|
482
|
-
reject(postgresFieldsError(fields));
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
if (msg.type === MessageTypes.READY_FOR_QUERY) {
|
|
486
|
-
cleanup();
|
|
487
|
-
resolve({ remaining: readyBuffer });
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
};
|
|
492
|
-
|
|
493
|
-
// Process any data already in buffer
|
|
494
|
-
let { messages, remaining } = parseMessages(readyBuffer);
|
|
495
|
-
readyBuffer = remaining;
|
|
496
|
-
for (let msg of messages) {
|
|
497
|
-
if (msg.type === MessageTypes.READY_FOR_QUERY) {
|
|
498
|
-
cleanup();
|
|
499
|
-
resolve({ remaining: readyBuffer });
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
if (msg.type === MessageTypes.ERROR_RESPONSE) {
|
|
503
|
-
let fields = parseErrorFields(msg.body);
|
|
504
|
-
cleanup();
|
|
505
|
-
reject(postgresFieldsError(fields));
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
socket.on('data', onData);
|
|
511
|
-
socket.on('error', onError);
|
|
512
|
-
});
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
private parseScramMessage(msg: string): Record<string, string> {
|
|
516
|
-
let result: Record<string, string> = {};
|
|
517
|
-
for (let part of msg.split(',')) {
|
|
518
|
-
let eqIndex = part.indexOf('=');
|
|
519
|
-
if (eqIndex > 0) {
|
|
520
|
-
result[part.substring(0, eqIndex)] = part.substring(eqIndex + 1);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
return result;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
private normalizePassword(password: string): string {
|
|
527
|
-
return password;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
private hi(password: string, salt: Uint8Array, iterations: number): Uint8Array {
|
|
531
|
-
let encoder = new TextEncoder();
|
|
532
|
-
let passwordBytes = encoder.encode(password);
|
|
533
|
-
// PBKDF2 with HMAC-SHA-256
|
|
534
|
-
let u = new Uint8Array(
|
|
535
|
-
crypto
|
|
536
|
-
.createHmac('sha256', passwordBytes)
|
|
537
|
-
.update(salt)
|
|
538
|
-
.update(new Uint8Array([0, 0, 0, 1]))
|
|
539
|
-
.digest()
|
|
540
|
-
);
|
|
541
|
-
let result = new Uint8Array(u);
|
|
542
|
-
for (let i = 1; i < iterations; i++) {
|
|
543
|
-
u = new Uint8Array(crypto.createHmac('sha256', passwordBytes).update(u).digest());
|
|
544
|
-
for (let j = 0; j < result.length; j++) {
|
|
545
|
-
result[j] = result[j]! ^ u[j]!;
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
return result;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
private hmacSha256(key: Uint8Array | string, data: string): Uint8Array {
|
|
552
|
-
let encoder = new TextEncoder();
|
|
553
|
-
let keyBytes = typeof key === 'string' ? encoder.encode(key) : key;
|
|
554
|
-
let dataBytes = typeof data === 'string' ? encoder.encode(data) : data;
|
|
555
|
-
return new Uint8Array(crypto.createHmac('sha256', keyBytes).update(dataBytes).digest());
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
private base64Encode(data: Uint8Array): string {
|
|
559
|
-
let binary = '';
|
|
560
|
-
for (let i = 0; i < data.length; i++) {
|
|
561
|
-
binary += String.fromCharCode(data[i]!);
|
|
562
|
-
}
|
|
563
|
-
return btoa(binary);
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
private base64Decode(str: string): Uint8Array {
|
|
567
|
-
let binary = atob(str);
|
|
568
|
-
let bytes = new Uint8Array(binary.length);
|
|
569
|
-
for (let i = 0; i < binary.length; i++) {
|
|
570
|
-
bytes[i] = binary.charCodeAt(i);
|
|
571
|
-
}
|
|
572
|
-
return bytes;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
private waitForMessage(
|
|
576
|
-
socket: net.Socket,
|
|
577
|
-
expectedType: number,
|
|
578
|
-
currentBuffer: Uint8Array,
|
|
579
|
-
timeoutMs: number
|
|
580
|
-
): Promise<{ msg: ParsedMessage; remaining: Uint8Array }> {
|
|
581
|
-
return new Promise((resolve, reject) => {
|
|
582
|
-
let buffer = currentBuffer;
|
|
583
|
-
|
|
584
|
-
let timer = setTimeout(() => {
|
|
585
|
-
cleanup();
|
|
586
|
-
reject(
|
|
587
|
-
postgresUpstreamError('Timeout waiting for PostgreSQL message', {
|
|
588
|
-
reason: 'postgresql_message_timeout'
|
|
589
|
-
})
|
|
590
|
-
);
|
|
591
|
-
}, timeoutMs);
|
|
592
|
-
|
|
593
|
-
let cleanup = () => {
|
|
594
|
-
clearTimeout(timer);
|
|
595
|
-
socket.removeListener('data', onData);
|
|
596
|
-
socket.removeListener('error', onError);
|
|
597
|
-
};
|
|
598
|
-
|
|
599
|
-
let onError = (err: Error) => {
|
|
600
|
-
cleanup();
|
|
601
|
-
reject(
|
|
602
|
-
postgresUpstreamError(`PostgreSQL socket error: ${err.message}`, {
|
|
603
|
-
reason: 'postgresql_socket_error',
|
|
604
|
-
parent: err
|
|
605
|
-
})
|
|
606
|
-
);
|
|
607
|
-
};
|
|
608
|
-
|
|
609
|
-
let processBuffer = (): boolean => {
|
|
610
|
-
let { messages, remaining } = parseMessages(buffer);
|
|
611
|
-
buffer = remaining;
|
|
612
|
-
|
|
613
|
-
for (let msg of messages) {
|
|
614
|
-
if (msg.type === MessageTypes.ERROR_RESPONSE) {
|
|
615
|
-
let fields = parseErrorFields(msg.body);
|
|
616
|
-
cleanup();
|
|
617
|
-
reject(postgresFieldsError(fields));
|
|
618
|
-
return true;
|
|
619
|
-
}
|
|
620
|
-
if (msg.type === expectedType) {
|
|
621
|
-
cleanup();
|
|
622
|
-
resolve({ msg, remaining: buffer });
|
|
623
|
-
return true;
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
return false;
|
|
627
|
-
};
|
|
628
|
-
|
|
629
|
-
// Check existing buffer first
|
|
630
|
-
if (processBuffer()) return;
|
|
631
|
-
|
|
632
|
-
let onData = (data: Uint8Array) => {
|
|
633
|
-
let newBuf = new Uint8Array(buffer.length + data.length);
|
|
634
|
-
newBuf.set(buffer);
|
|
635
|
-
newBuf.set(data, buffer.length);
|
|
636
|
-
buffer = newBuf;
|
|
637
|
-
processBuffer();
|
|
638
|
-
};
|
|
639
|
-
|
|
640
|
-
socket.on('data', onData);
|
|
641
|
-
socket.on('error', onError);
|
|
642
|
-
});
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
private executeQuery(
|
|
646
|
-
socket: net.Socket,
|
|
647
|
-
sql: string,
|
|
648
|
-
timeoutMs: number
|
|
649
|
-
): Promise<QueryResult> {
|
|
650
|
-
return new Promise((resolve, reject) => {
|
|
651
|
-
let buffer: Uint8Array = new Uint8Array(0);
|
|
652
|
-
let columns: ColumnDescription[] = [];
|
|
653
|
-
let rows: (string | null)[][] = [];
|
|
654
|
-
let command = '';
|
|
655
|
-
let rowCount: number | null = null;
|
|
656
|
-
|
|
657
|
-
let timer = setTimeout(() => {
|
|
658
|
-
cleanup();
|
|
659
|
-
reject(
|
|
660
|
-
postgresUpstreamError(`PostgreSQL query timeout after ${timeoutMs}ms`, {
|
|
661
|
-
reason: 'postgresql_query_timeout'
|
|
662
|
-
})
|
|
663
|
-
);
|
|
664
|
-
}, timeoutMs);
|
|
665
|
-
|
|
666
|
-
let cleanup = () => {
|
|
667
|
-
clearTimeout(timer);
|
|
668
|
-
socket.removeListener('data', onData);
|
|
669
|
-
socket.removeListener('error', onError);
|
|
670
|
-
};
|
|
671
|
-
|
|
672
|
-
let onError = (err: Error) => {
|
|
673
|
-
cleanup();
|
|
674
|
-
reject(
|
|
675
|
-
postgresUpstreamError(`PostgreSQL query error: ${err.message}`, {
|
|
676
|
-
reason: 'postgresql_query_error',
|
|
677
|
-
parent: err
|
|
678
|
-
})
|
|
679
|
-
);
|
|
680
|
-
};
|
|
681
|
-
|
|
682
|
-
let onData = (data: Uint8Array) => {
|
|
683
|
-
let newBuf = new Uint8Array(buffer.length + data.length);
|
|
684
|
-
newBuf.set(buffer);
|
|
685
|
-
newBuf.set(data, buffer.length);
|
|
686
|
-
buffer = newBuf;
|
|
687
|
-
|
|
688
|
-
let { messages, remaining } = parseMessages(buffer);
|
|
689
|
-
buffer = remaining;
|
|
690
|
-
|
|
691
|
-
for (let msg of messages) {
|
|
692
|
-
if (msg.type === MessageTypes.ROW_DESCRIPTION) {
|
|
693
|
-
columns = parseRowDescription(msg.body);
|
|
694
|
-
} else if (msg.type === MessageTypes.DATA_ROW) {
|
|
695
|
-
rows.push(parseDataRow(msg.body));
|
|
696
|
-
} else if (msg.type === MessageTypes.COMMAND_COMPLETE) {
|
|
697
|
-
let parsed = parseCommandComplete(msg.body);
|
|
698
|
-
command = parsed.command;
|
|
699
|
-
rowCount = parsed.rowCount;
|
|
700
|
-
} else if (msg.type === MessageTypes.ERROR_RESPONSE) {
|
|
701
|
-
let fields = parseErrorFields(msg.body);
|
|
702
|
-
cleanup();
|
|
703
|
-
reject(postgresFieldsError(fields));
|
|
704
|
-
return;
|
|
705
|
-
} else if (msg.type === MessageTypes.READY_FOR_QUERY) {
|
|
706
|
-
cleanup();
|
|
707
|
-
|
|
708
|
-
let mappedColumns = columns.map(col => ({
|
|
709
|
-
name: col.name,
|
|
710
|
-
type: oidToTypeName(col.typeOid),
|
|
711
|
-
typeOid: col.typeOid
|
|
712
|
-
}));
|
|
713
|
-
|
|
714
|
-
let mappedRows = rows.map(row => {
|
|
715
|
-
let obj: Record<string, any> = {};
|
|
716
|
-
for (let i = 0; i < columns.length; i++) {
|
|
717
|
-
let col = columns[i]!;
|
|
718
|
-
let value = row[i] ?? null;
|
|
719
|
-
obj[col.name] = castValue(value, col.typeOid);
|
|
720
|
-
}
|
|
721
|
-
return obj;
|
|
722
|
-
});
|
|
723
|
-
|
|
724
|
-
resolve({
|
|
725
|
-
columns: mappedColumns,
|
|
726
|
-
rows: mappedRows,
|
|
727
|
-
rowCount,
|
|
728
|
-
command
|
|
729
|
-
});
|
|
730
|
-
return;
|
|
731
|
-
}
|
|
732
|
-
// Ignore NoticeResponse, EmptyQueryResponse
|
|
733
|
-
}
|
|
734
|
-
};
|
|
735
|
-
|
|
736
|
-
socket.on('data', onData);
|
|
737
|
-
socket.on('error', onError);
|
|
738
|
-
socket.write(buildQueryMessage(sql));
|
|
739
|
-
});
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
private terminate(socket: net.Socket): Promise<void> {
|
|
743
|
-
return new Promise(resolve => {
|
|
744
|
-
let settled = false;
|
|
745
|
-
let timeout: ReturnType<typeof setTimeout>;
|
|
746
|
-
let finish = () => {
|
|
747
|
-
if (settled) return;
|
|
748
|
-
settled = true;
|
|
749
|
-
clearTimeout(timeout);
|
|
750
|
-
socket.removeListener('close', finish);
|
|
751
|
-
socket.removeListener('error', finish);
|
|
752
|
-
resolve();
|
|
753
|
-
};
|
|
754
|
-
|
|
755
|
-
timeout = setTimeout(() => {
|
|
756
|
-
socket.destroy();
|
|
757
|
-
finish();
|
|
758
|
-
}, 1000);
|
|
759
|
-
|
|
760
|
-
socket.once('close', finish);
|
|
761
|
-
socket.once('error', finish);
|
|
762
|
-
|
|
763
|
-
try {
|
|
764
|
-
socket.end(buildTerminateMessage(), finish);
|
|
765
|
-
} catch {
|
|
766
|
-
socket.destroy();
|
|
767
|
-
finish();
|
|
768
|
-
}
|
|
769
|
-
});
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
// Cast PostgreSQL text values to appropriate JS types
|
|
774
|
-
let castValue = (value: string | null, typeOid: number): any => {
|
|
775
|
-
if (value === null) return null;
|
|
776
|
-
|
|
777
|
-
switch (typeOid) {
|
|
778
|
-
case 16: // boolean
|
|
779
|
-
return value === 't' || value === 'true';
|
|
780
|
-
case 20: // bigint
|
|
781
|
-
case 21: // smallint
|
|
782
|
-
case 23: // integer
|
|
783
|
-
case 26: // oid
|
|
784
|
-
return parseInt(value, 10);
|
|
785
|
-
case 700: // real
|
|
786
|
-
case 701: // double precision
|
|
787
|
-
case 1700: // numeric
|
|
788
|
-
return parseFloat(value);
|
|
789
|
-
case 114: // json
|
|
790
|
-
case 3802: // jsonb
|
|
791
|
-
try {
|
|
792
|
-
return JSON.parse(value);
|
|
793
|
-
} catch {
|
|
794
|
-
return value;
|
|
795
|
-
}
|
|
796
|
-
default:
|
|
797
|
-
return value;
|
|
798
|
-
}
|
|
799
|
-
};
|