@mason-data/cli 0.7.0 → 0.9.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/README.md +101 -73
- package/dist/src/api/error.js +3 -0
- package/dist/src/api/postgres/catalog-worker.js +49 -2
- package/dist/src/api/postgres/catalog.js +368 -25
- package/dist/src/api/postgres/encode.js +3 -1
- package/dist/src/api/postgres/explain.js +3 -2
- package/dist/src/api/postgres/listener.js +11 -3
- package/dist/src/api/postgres/protocol.js +39 -23
- package/dist/src/api/postgres/proxy.js +283 -0
- package/dist/src/api/postgres/schema.js +6 -0
- package/dist/src/api/postgres/server.js +1617 -245
- package/dist/src/api/postgres/translate.js +127 -10
- package/dist/src/api/request-log.js +18 -5
- package/dist/src/cdc/poll.js +5 -5
- package/dist/src/cli/background.js +47 -25
- package/dist/src/cli/bridge-stdin.js +101 -0
- package/dist/src/cli/cancel.js +75 -0
- package/dist/src/cli/command.js +170 -11
- package/dist/src/cli/commands.js +804 -209
- package/dist/src/cli/control.js +59 -0
- package/dist/src/cli/credentials.js +182 -0
- package/dist/src/cli/demo.js +20 -11
- package/dist/src/cli/deploy.js +574 -0
- package/dist/src/cli/dev.js +491 -130
- package/dist/src/cli/doctor.js +131 -38
- package/dist/src/cli/generated-files.js +143 -0
- package/dist/src/cli/guides.js +175 -36
- package/dist/src/cli/hosted.js +850 -0
- package/dist/src/cli/init.js +162 -65
- package/dist/src/cli/login.js +289 -0
- package/dist/src/cli/machine.js +13 -12
- package/dist/src/cli/main.js +11 -5
- package/dist/src/cli/mason.cjs +17 -0
- package/dist/src/cli/mcp.js +31 -22
- package/dist/src/cli/metrics.js +79 -0
- package/dist/src/cli/output-gate.js +176 -0
- package/dist/src/cli/page-share.js +96 -0
- package/dist/src/cli/pg-tls.js +14 -4
- package/dist/src/cli/pg-url.js +4 -0
- package/dist/src/cli/pin.js +19 -14
- package/dist/src/cli/pool-size.cjs +85 -0
- package/dist/src/cli/ports.js +2 -0
- package/dist/src/cli/preflight.js +138 -46
- package/dist/src/cli/project.js +142 -4
- package/dist/src/cli/readiness.js +76 -6
- package/dist/src/cli/registry.js +65 -38
- package/dist/src/cli/replica-hosted.js +665 -0
- package/dist/src/cli/replica.js +76 -0
- package/dist/src/cli/replication-checks.js +7 -5
- package/dist/src/cli/replication-mode.js +105 -26
- package/dist/src/cli/request-filter.js +134 -0
- package/dist/src/cli/roles.js +585 -0
- package/dist/src/cli/run-server.js +637 -48
- package/dist/src/cli/runs.js +1 -1
- package/dist/src/cli/security.js +67 -18
- package/dist/src/cli/serve.js +204 -46
- package/dist/src/cli/share-run.js +329 -0
- package/dist/src/cli/share.js +286 -0
- package/dist/src/cli/source-edit.js +75 -17
- package/dist/src/cli/source-role.js +344 -0
- package/dist/src/cli/sources.js +306 -47
- package/dist/src/cli/style.js +2 -1
- package/dist/src/cli/supervise.js +27 -24
- package/dist/src/cli/sync.js +55 -25
- package/dist/src/cli/token-hosted.js +337 -0
- package/dist/src/cli/tunnel.js +200 -0
- package/dist/src/cli/ui-ssh.js +460 -0
- package/dist/src/cli/ui.js +124 -17
- package/dist/src/cli/usage.js +128 -0
- package/dist/src/cli/user-hosted.js +299 -0
- package/dist/src/cli/user.js +573 -65
- package/dist/src/cli/version.js +9 -7
- package/dist/src/demo/seed.js +29 -0
- package/dist/src/duck/analyze.js +36 -20
- package/dist/src/duck/cancel.js +10 -1
- package/dist/src/duck/collation.js +266 -0
- package/dist/src/duck/constant-key.js +94 -0
- package/dist/src/duck/distinct-key.js +283 -26
- package/dist/src/duck/events.js +165 -0
- package/dist/src/duck/executor-worker.js +20 -11
- package/dist/src/duck/executor.js +47 -33
- package/dist/src/duck/explain.js +60 -46
- package/dist/src/duck/extensions.js +160 -0
- package/dist/src/duck/functions.js +461 -63
- package/dist/src/duck/gaps.js +48 -21
- package/dist/src/duck/generated-plan.js +1070 -0
- package/dist/src/duck/generated.js +906 -0
- package/dist/src/duck/grants.js +176 -0
- package/dist/src/duck/held-types.js +64 -0
- package/dist/src/duck/history.js +127 -0
- package/dist/src/duck/jsonb-order.js +9 -12
- package/dist/src/duck/kept-route.js +6 -5
- package/dist/src/duck/kept.js +15 -13
- package/dist/src/duck/late.js +1469 -40
- package/dist/src/duck/libc-bound.js +341 -0
- package/dist/src/duck/libc-order.js +892 -126
- package/dist/src/duck/pg-eval-worker.js +7 -1
- package/dist/src/duck/pg-eval.js +6 -4
- package/dist/src/duck/pgtree.js +774 -23
- package/dist/src/duck/plan.js +369 -47
- package/dist/src/duck/publication.js +20 -6
- package/dist/src/duck/requests.js +269 -93
- package/dist/src/duck/resolved.js +117 -2
- package/dist/src/duck/rewrite.js +4584 -580
- package/dist/src/duck/row-filter.js +655 -0
- package/dist/src/duck/statement.js +19 -0
- package/dist/src/duck/store.js +439 -177
- package/dist/src/duck/stream.js +19 -10
- package/dist/src/duck/sync.js +1411 -368
- package/dist/src/duck/token.js +4 -1
- package/dist/src/duck/types.js +69 -12
- package/dist/src/duck/usage.js +326 -0
- package/dist/src/duck/users.js +69 -15
- package/dist/src/duck/verified.js +161 -1
- package/dist/src/duck/views.js +5 -5
- package/dist/src/lib/database-name.js +62 -0
- package/dist/src/lib/database-url.js +55 -10
- package/dist/src/lib/dotenv.js +17 -4
- package/dist/src/lib/instant.js +50 -0
- package/dist/src/lib/log-buffer.js +77 -0
- package/dist/src/lib/log-read.js +196 -0
- package/dist/src/lib/pg-numeric.js +390 -49
- package/dist/src/lib/project-version.js +23 -23
- package/dist/src/lib/secret-mask.js +142 -0
- package/dist/src/lib/table-sync.js +282 -0
- package/dist/src/web/app.css +706 -66
- package/dist/src/web/app.js +331 -409
- package/dist/src/web/charts.js +229 -0
- package/dist/src/web/index.html +82 -18
- package/dist/src/web/pages/queries.js +192 -0
- package/dist/src/web/pages/status.js +611 -0
- package/dist/src/web/pages/syncs.js +155 -0
- package/dist/src/web/pages/tables.js +439 -0
- package/dist/src/web/pages/usage.js +397 -0
- package/dist/src/web/pages/users.js +219 -0
- package/dist/src/web/shared.js +584 -0
- package/dist/src/web/vendor/README.md +25 -0
- package/dist/src/web/vendor/geist-1.7.2/Geist-Variable.woff2 +0 -0
- package/dist/src/web/vendor/geist-1.7.2/GeistMono-Variable.woff2 +0 -0
- package/dist/src/web/vendor/geist-1.7.2/LICENSE.txt +92 -0
- package/dist/src/web/vendor/lucide-1.56.0/LICENSE.txt +43 -0
- package/dist/src/web/vendor/lucide-1.56.0/chart-column.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/layout-dashboard.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/logs.mjs +34 -0
- package/dist/src/web/vendor/lucide-1.56.0/menu.mjs +28 -0
- package/dist/src/web/vendor/lucide-1.56.0/square-terminal.mjs +28 -0
- package/dist/src/web/vendor/lucide-1.56.0/table.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/users.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/x.mjs +27 -0
- package/dist/src/web/vendor/uplot-1.6.32/LICENSE.txt +21 -0
- package/dist/src/web/vendor/uplot-1.6.32/uPlot.esm.js +6140 -0
- package/dist/src/web/vendor/uplot-1.6.32/uPlot.min.css +1 -0
- package/dist/src/web/vendor/zilla-slab-5.3.0/LICENSE.txt +93 -0
- package/dist/src/web/vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2 +0 -0
- package/package.json +12 -4
|
@@ -75,12 +75,10 @@ class BodyReader {
|
|
|
75
75
|
rest() { return Buffer.from(this.#buf.subarray(this.#off)); }
|
|
76
76
|
get done() { return this.#off >= this.#buf.length; }
|
|
77
77
|
}
|
|
78
|
-
/** The
|
|
79
|
-
*
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
function frameLength(buf, headerLength, limit) {
|
|
83
|
-
const len = buf.readInt32BE(headerLength - 4);
|
|
78
|
+
/** The length field of the frame at `off` in `buf` (type byte excluded, the field itself included), checked against
|
|
79
|
+
* `limit`; `headerLength` is 5 with a type byte, 4 in the startup phase. */
|
|
80
|
+
function frameLength(buf, off, headerLength, limit) {
|
|
81
|
+
const len = buf.readInt32BE(off + headerLength - 4);
|
|
84
82
|
if (len < 4)
|
|
85
83
|
throw new ProtocolError(`malformed message: a length of ${len} (the length counts itself, so it is at least 4)`);
|
|
86
84
|
if (len > limit)
|
|
@@ -89,8 +87,10 @@ function frameLength(buf, headerLength, limit) {
|
|
|
89
87
|
}
|
|
90
88
|
/** Decodes what a frontend sends. Push every chunk the socket delivers; call `next()` until it returns null. */
|
|
91
89
|
export class FrontendDecoder {
|
|
92
|
-
/** the bytes
|
|
90
|
+
/** the chunks joined so far: consumed bytes before `#off`, then the frames not yet read, the last perhaps partial */
|
|
93
91
|
#buf = Buffer.alloc(0);
|
|
92
|
+
/** where the next frame starts in `#buf`: a frame consumed moves this, never the bytes behind it (BackendDecoder) */
|
|
93
|
+
#off = 0;
|
|
94
94
|
/** chunks received since, concatenated ONCE, when the frame they complete is whole (never per chunk: a frame in
|
|
95
95
|
* 64 KiB pieces re-copied everything received so far on every piece — review of #480) */
|
|
96
96
|
#pending = [];
|
|
@@ -106,13 +106,16 @@ export class FrontendDecoder {
|
|
|
106
106
|
this.#pending.push(Buffer.from(chunk));
|
|
107
107
|
this.#pendingLength += chunk.length;
|
|
108
108
|
}
|
|
109
|
-
/** Makes `#buf` hold at least `need` bytes when that many have arrived, moving the pending chunks in with
|
|
109
|
+
/** Makes `#buf` hold at least `need` bytes past `#off` when that many have arrived, moving the pending chunks in with
|
|
110
|
+
* one copy, which leaves the consumed bytes behind. */
|
|
110
111
|
#assemble(need) {
|
|
111
|
-
|
|
112
|
+
const held = this.#buf.length - this.#off;
|
|
113
|
+
if (held >= need)
|
|
112
114
|
return true;
|
|
113
|
-
if (
|
|
115
|
+
if (held + this.#pendingLength < need)
|
|
114
116
|
return false;
|
|
115
|
-
this.#buf = Buffer.concat([this.#buf, ...this.#pending]);
|
|
117
|
+
this.#buf = Buffer.concat([this.#buf.subarray(this.#off), ...this.#pending]);
|
|
118
|
+
this.#off = 0;
|
|
116
119
|
this.#pending = [];
|
|
117
120
|
this.#pendingLength = 0;
|
|
118
121
|
return true;
|
|
@@ -126,12 +129,18 @@ export class FrontendDecoder {
|
|
|
126
129
|
#whole(headerLength) {
|
|
127
130
|
if (!this.#assemble(headerLength))
|
|
128
131
|
return null;
|
|
129
|
-
const len = frameLength(this.#buf, headerLength, this.#startup ? Math.min(this.maxLength, MAX_STARTUP_LENGTH) : this.maxLength);
|
|
132
|
+
const len = frameLength(this.#buf, this.#off, headerLength, this.#startup ? Math.min(this.maxLength, MAX_STARTUP_LENGTH) : this.maxLength);
|
|
130
133
|
return this.#assemble(headerLength - 4 + len) ? len : null;
|
|
131
134
|
}
|
|
135
|
+
/** The frame's body, a view of `#buf` that lives as long as its BodyReader: every value a reader returns is a copy. */
|
|
132
136
|
#take(headerLength, len) {
|
|
133
|
-
const body = this.#buf.subarray(headerLength, headerLength - 4 + len);
|
|
134
|
-
this.#
|
|
137
|
+
const body = this.#buf.subarray(this.#off + headerLength, this.#off + headerLength - 4 + len);
|
|
138
|
+
this.#off += headerLength - 4 + len;
|
|
139
|
+
// wholly consumed, it is let go: a connection idle on a frame boundary holds no bytes of what it last sent
|
|
140
|
+
if (this.#off === this.#buf.length) {
|
|
141
|
+
this.#buf = Buffer.alloc(0);
|
|
142
|
+
this.#off = 0;
|
|
143
|
+
}
|
|
135
144
|
return body;
|
|
136
145
|
}
|
|
137
146
|
#nextStartup() {
|
|
@@ -165,7 +174,7 @@ export class FrontendDecoder {
|
|
|
165
174
|
const len = this.#whole(5);
|
|
166
175
|
if (len === null)
|
|
167
176
|
return null;
|
|
168
|
-
const code = String.fromCharCode(this.#buf[
|
|
177
|
+
const code = String.fromCharCode(this.#buf[this.#off]);
|
|
169
178
|
const r = new BodyReader(this.#take(5, len), `'${code}'`);
|
|
170
179
|
switch (code) {
|
|
171
180
|
case "p": return { type: "Password", password: r.cstring() };
|
|
@@ -353,22 +362,29 @@ function decodeBackendBody(code, body) {
|
|
|
353
362
|
/** Decodes what a backend sends, chunk by chunk, as FrontendDecoder does for the other direction. */
|
|
354
363
|
export class BackendDecoder {
|
|
355
364
|
#buf = Buffer.alloc(0);
|
|
365
|
+
/** WHERE THE NEXT FRAME STARTS IN `#buf`: `next()` moves this and `push` drops what is behind it. Copying the rest off
|
|
366
|
+
* after every message was quadratic in a reply: a 30 MB reply of 150,000 rows took about six minutes on CI (issue
|
|
367
|
+
* 77). A new buffer is always allocated, never written over, and no decoded message keeps a view of one (BodyReader
|
|
368
|
+
* copies). */
|
|
369
|
+
#off = 0;
|
|
356
370
|
push(chunk) {
|
|
357
|
-
|
|
371
|
+
const rest = this.#buf.subarray(this.#off);
|
|
372
|
+
this.#buf = rest.length === 0 ? Buffer.from(chunk) : Buffer.concat([rest, chunk]);
|
|
373
|
+
this.#off = 0;
|
|
358
374
|
}
|
|
359
375
|
next() {
|
|
360
|
-
if (this
|
|
376
|
+
if (this.pending < 5)
|
|
361
377
|
return null;
|
|
362
|
-
const len = frameLength(this.#buf, 5, MAX_MESSAGE_LENGTH);
|
|
363
|
-
if (this
|
|
378
|
+
const len = frameLength(this.#buf, this.#off, 5, MAX_MESSAGE_LENGTH);
|
|
379
|
+
if (this.pending < 1 + len)
|
|
364
380
|
return null;
|
|
365
|
-
const code = String.fromCharCode(this.#buf[
|
|
366
|
-
const body = this.#buf.subarray(5, 1 + len);
|
|
367
|
-
this.#
|
|
381
|
+
const code = String.fromCharCode(this.#buf[this.#off]);
|
|
382
|
+
const body = this.#buf.subarray(this.#off + 5, this.#off + 1 + len);
|
|
383
|
+
this.#off += 1 + len;
|
|
368
384
|
return decodeBackendBody(code, body);
|
|
369
385
|
}
|
|
370
386
|
/** bytes pushed and not yet consumed by a whole message */
|
|
371
|
-
get pending() { return this.#buf.length; }
|
|
387
|
+
get pending() { return this.#buf.length - this.#off; }
|
|
372
388
|
}
|
|
373
389
|
/** Every message of a buffer that holds whole messages only (a `execProtocolRaw` reply, a builder's output). A
|
|
374
390
|
* trailing partial message throws: the caller promised a complete reply. */
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE POSTGRES API BEHIND A TCP PROXY (`MASON_PG_PROXY_FROM`; docs/POSTGRES-API.md "Behind a TCP proxy"). Behind a TCP
|
|
3
|
+
* load balancer or proxy (an AWS NLB, HAProxy, an edge of one's own) every connection's peer is the proxy, so the
|
|
4
|
+
* request log could not say who sent what. A proxy that speaks the PROXY protocol v2 (haproxy's proxy-protocol.txt,
|
|
5
|
+
* section 2.2) begins each connection with a binary header naming the client it carries. A header is a CLAIM about
|
|
6
|
+
* where a connection came from, so it is believed only from an address the operator named:
|
|
7
|
+
* - a connection from a named address MUST begin with a valid v2 header, or it is refused;
|
|
8
|
+
* - a connection from any other address is read as before, and a PROXY header from it is refused, never believed;
|
|
9
|
+
* - unset, nothing is named and nothing changes.
|
|
10
|
+
* The header is read whole before anything else on the connection is looked at (an SSLRequest, a direct TLS start, a
|
|
11
|
+
* startup packet, a CancelRequest), and bounded in size (`MAX_PROXY_BODY`) and in time (`PROXY_HEADER_MS`).
|
|
12
|
+
*
|
|
13
|
+
* THE CLIENT'S ADDRESS HAS ONE READER (`clientAddress`): the source a named proxy's header carries, else the socket's
|
|
14
|
+
* peer, a v4-mapped IPv6 address shown as IPv4 (`shownAddress`). The request log's `client_addr` and the session's log
|
|
15
|
+
* line read it, never the socket.
|
|
16
|
+
*/
|
|
17
|
+
/** the v2 header's first 12 bytes (section 2.2) */
|
|
18
|
+
export const PROXY_V2_SIGNATURE = Buffer.from([0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, 0x54, 0x0a]);
|
|
19
|
+
/** how a v1 (text) header begins */
|
|
20
|
+
const PROXY_V1_START = Buffer.from("PROXY ", "latin1");
|
|
21
|
+
/** The most a header may carry past its 16 fixed bytes: the largest address block of the spec (two UNIX paths, 216
|
|
22
|
+
* bytes), which holds an IPv6 pair (36) and 180 bytes of TLVs. A header that says more is refused. */
|
|
23
|
+
export const MAX_PROXY_BODY = 216;
|
|
24
|
+
/** How long a named proxy has to send its whole header once it has connected. */
|
|
25
|
+
export const PROXY_HEADER_MS = 5_000;
|
|
26
|
+
/** the setting, as every message names it */
|
|
27
|
+
export const PROXY_SETTING = "MASON_PG_PROXY_FROM";
|
|
28
|
+
/**
|
|
29
|
+
* The PROXY v2 header at the start of `b`, read as section 2.2 says: the signature, version 2, the LOCAL or PROXY
|
|
30
|
+
* command, TCP over IPv4 (0x11) or IPv6 (0x21) for a PROXY command, its length at most MAX_PROXY_BODY, and its TLVs
|
|
31
|
+
* skipped, each held inside the block. Asked again as bytes arrive: a header split across reads is `more` until it is
|
|
32
|
+
* whole, and a wrong byte is refused as soon as it arrives. A v1 (text) header is refused by name.
|
|
33
|
+
*/
|
|
34
|
+
export function readProxyHeader(b) {
|
|
35
|
+
for (let i = 0; i < Math.min(b.length, PROXY_V2_SIGNATURE.length); i++) {
|
|
36
|
+
if (b[i] === PROXY_V2_SIGNATURE[i])
|
|
37
|
+
continue;
|
|
38
|
+
const v1 = Math.min(b.length, PROXY_V1_START.length);
|
|
39
|
+
if (b.subarray(0, v1).every((c, j) => c === PROXY_V1_START[j])) {
|
|
40
|
+
return v1 < PROXY_V1_START.length ? { more: true } : { refused: "a PROXY protocol v1 (text) header: Mason reads version 2 alone (HAProxy's send-proxy-v2)" };
|
|
41
|
+
}
|
|
42
|
+
return { refused: "no PROXY protocol v2 header: its first 12 bytes are not the signature" };
|
|
43
|
+
}
|
|
44
|
+
if (b.length < 16)
|
|
45
|
+
return { more: true };
|
|
46
|
+
const version = b[12] >> 4, command = b[12] & 0x0f;
|
|
47
|
+
if (version !== 2)
|
|
48
|
+
return { refused: `a PROXY header of version ${version}: Mason reads version 2` };
|
|
49
|
+
if (command > 1)
|
|
50
|
+
return { refused: `a PROXY header with the command 0x${command.toString(16)}: only LOCAL (0x0) and PROXY (0x1) are defined` };
|
|
51
|
+
const family = b[13], length = (b[14] << 8) | b[15];
|
|
52
|
+
if (length > MAX_PROXY_BODY)
|
|
53
|
+
return { refused: `a PROXY header of ${length} bytes past its first 16: Mason reads at most ${MAX_PROXY_BODY}` };
|
|
54
|
+
if (b.length < 16 + length)
|
|
55
|
+
return { more: true };
|
|
56
|
+
// LOCAL: the proxy's own connection (a health check); its address block is discarded, as the spec says
|
|
57
|
+
if (command === 0)
|
|
58
|
+
return { header: { command: "LOCAL", source: null }, length: 16 + length };
|
|
59
|
+
const body = b.subarray(16, 16 + length);
|
|
60
|
+
const addresses = family === 0x11 ? 12 : family === 0x21 ? 36 : null;
|
|
61
|
+
if (addresses === null) {
|
|
62
|
+
return { refused: `a PROXY header for family and transport 0x${family.toString(16).padStart(2, "0")}: Mason reads TCP over IPv4 (0x11) or IPv6 (0x21), and an UNSPEC one with LOCAL alone` };
|
|
63
|
+
}
|
|
64
|
+
if (length < addresses)
|
|
65
|
+
return { refused: `a PROXY header of ${length} bytes past its first 16, too few for its addresses (${addresses})` };
|
|
66
|
+
const source = family === 0x11
|
|
67
|
+
? { address: Array.from(body.subarray(0, 4)).join("."), port: (body[8] << 8) | body[9] }
|
|
68
|
+
: { address: ipv6Text(body.subarray(0, 16)), port: (body[32] << 8) | body[33] };
|
|
69
|
+
// the TLVs (type, a 2-byte length, the value): skipped, each held inside the block
|
|
70
|
+
for (let at = addresses; at < length;) {
|
|
71
|
+
if (length - at < 3)
|
|
72
|
+
return { refused: "a PROXY header whose last TLV is cut short" };
|
|
73
|
+
at += 3 + ((body[at + 1] << 8) | body[at + 2]);
|
|
74
|
+
if (at > length)
|
|
75
|
+
return { refused: "a PROXY header with a TLV past its end" };
|
|
76
|
+
}
|
|
77
|
+
return { header: { command: "PROXY", source }, length: 16 + length };
|
|
78
|
+
}
|
|
79
|
+
/** Whether a connection's first byte is a PROXY header's, v2 or v1: never a startup packet's (its length's first byte is
|
|
80
|
+
* 0) nor a TLS ClientHello's (0x16). */
|
|
81
|
+
export const startsProxyHeader = (first) => first === PROXY_V2_SIGNATURE[0] || first === PROXY_V1_START[0];
|
|
82
|
+
/**
|
|
83
|
+
* THE CLIENT'S ADDRESS, THE ONE READER OF IT: the source the header of a named proxy carries, else the socket's peer
|
|
84
|
+
* (a LOCAL header's connection is the proxy's own), shown as Mason shows an address (`shownAddress`); null when the
|
|
85
|
+
* peer cannot be read.
|
|
86
|
+
*/
|
|
87
|
+
export function clientAddress(peer, header) {
|
|
88
|
+
return header?.source?.address ?? shownAddress(peer);
|
|
89
|
+
}
|
|
90
|
+
/** An address as Mason shows it: IPv4 dotted, IPv6 in RFC 5952's form, a v4-mapped IPv6 one as IPv4; null when it is no
|
|
91
|
+
* address (undefined, a zone index, a name). */
|
|
92
|
+
export function shownAddress(text) {
|
|
93
|
+
if (text === undefined)
|
|
94
|
+
return null;
|
|
95
|
+
const v4 = ipv4Bytes(text);
|
|
96
|
+
if (v4 !== null)
|
|
97
|
+
return Array.from(v4).join(".");
|
|
98
|
+
const v6 = ipv6Bytes(text);
|
|
99
|
+
return v6 === null ? null : ipv6Text(v6);
|
|
100
|
+
}
|
|
101
|
+
/** An IPv4 address's 4 bytes, from dotted decimal with no leading zero; null for anything else. */
|
|
102
|
+
function ipv4Bytes(text) {
|
|
103
|
+
const parts = text.split(".");
|
|
104
|
+
if (parts.length !== 4 || !parts.every((p) => /^(?:0|[1-9]\d{0,2})$/.test(p) && Number(p) <= 255))
|
|
105
|
+
return null;
|
|
106
|
+
return Uint8Array.from(parts.map(Number));
|
|
107
|
+
}
|
|
108
|
+
/** An IPv6 address's 16 bytes (`::` once at most, an IPv4 tail); null for anything else, a zone index included. */
|
|
109
|
+
function ipv6Bytes(text) {
|
|
110
|
+
if (!/^[0-9A-Fa-f:.]+$/.test(text))
|
|
111
|
+
return null;
|
|
112
|
+
const halves = text.split("::");
|
|
113
|
+
if (halves.length > 2 || (halves.length === 2 && halves[0].includes(".")))
|
|
114
|
+
return null;
|
|
115
|
+
const groups = (s) => {
|
|
116
|
+
if (s === "")
|
|
117
|
+
return [];
|
|
118
|
+
const out = [];
|
|
119
|
+
const parts = s.split(":");
|
|
120
|
+
for (const [i, p] of parts.entries()) {
|
|
121
|
+
if (i === parts.length - 1 && p.includes(".")) {
|
|
122
|
+
const v4 = ipv4Bytes(p);
|
|
123
|
+
if (v4 === null)
|
|
124
|
+
return null;
|
|
125
|
+
out.push((v4[0] << 8) | v4[1], (v4[2] << 8) | v4[3]);
|
|
126
|
+
}
|
|
127
|
+
else if (/^[0-9A-Fa-f]{1,4}$/.test(p))
|
|
128
|
+
out.push(parseInt(p, 16));
|
|
129
|
+
else
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
};
|
|
134
|
+
const head = groups(halves[0]), tail = halves.length === 2 ? groups(halves[1]) : [];
|
|
135
|
+
if (head === null || tail === null)
|
|
136
|
+
return null;
|
|
137
|
+
const n = head.length + tail.length;
|
|
138
|
+
if (halves.length === 1 ? n !== 8 : n > 7)
|
|
139
|
+
return null;
|
|
140
|
+
const all = [...head, ...new Array(8 - n).fill(0), ...tail];
|
|
141
|
+
return Uint8Array.from(all.flatMap((g) => [g >> 8, g & 0xff]));
|
|
142
|
+
}
|
|
143
|
+
/** whether 16 bytes are an IPv4 address mapped into IPv6 (::ffff:a.b.c.d) */
|
|
144
|
+
const v4Mapped = (b) => b.subarray(0, 10).every((x) => x === 0) && b[10] === 0xff && b[11] === 0xff;
|
|
145
|
+
/** 16 bytes as Mason shows an IPv6 address: RFC 5952 (lower case, the longest run of two or more zero groups as `::`,
|
|
146
|
+
* the first of equal runs), and a v4-mapped one as its IPv4 address. */
|
|
147
|
+
function ipv6Text(b) {
|
|
148
|
+
if (v4Mapped(b))
|
|
149
|
+
return Array.from(b.subarray(12, 16)).join(".");
|
|
150
|
+
const g = Array.from({ length: 8 }, (_, i) => (b[2 * i] << 8) | b[2 * i + 1]);
|
|
151
|
+
let at = -1, run = 0;
|
|
152
|
+
for (let i = 0; i < 8;) {
|
|
153
|
+
if (g[i] !== 0) {
|
|
154
|
+
i++;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
let j = i;
|
|
158
|
+
while (j < 8 && g[j] === 0)
|
|
159
|
+
j++;
|
|
160
|
+
if (j - i > run) {
|
|
161
|
+
at = i;
|
|
162
|
+
run = j - i;
|
|
163
|
+
}
|
|
164
|
+
i = j;
|
|
165
|
+
}
|
|
166
|
+
const hex = g.map((x) => x.toString(16));
|
|
167
|
+
return run < 2 ? hex.join(":") : `${hex.slice(0, at).join(":")}::${hex.slice(at + run).join(":")}`;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* EVERY ADDRESS IN ONE SPACE, as Mason means one: IPv6's 128 bits, an IPv4 address at its v4-mapped place
|
|
171
|
+
* (::ffff:a.b.c.d), since a v4-mapped peer or header source IS that IPv4 client (`shownAddress`). What an entry names and
|
|
172
|
+
* what the list covers are both read here, never from how an entry is spelt: `0.0.0.0/1,128.0.0.0/1` is every IPv4
|
|
173
|
+
* address, as `0.0.0.0/0` is, and `::/64` holds every v4-mapped address, so every IPv4 one too.
|
|
174
|
+
*/
|
|
175
|
+
const V4_START = 0xffffn << 32n;
|
|
176
|
+
const V4_END = V4_START + (1n << 32n) - 1n;
|
|
177
|
+
const ALL_END = (1n << 128n) - 1n;
|
|
178
|
+
const bigOf = (b) => b.reduce((n, x) => (n << 8n) | BigInt(x), 0n);
|
|
179
|
+
/** an address's place (`V4_START`), or null when it is no address */
|
|
180
|
+
function placeOf(address) {
|
|
181
|
+
const v4 = ipv4Bytes(address);
|
|
182
|
+
if (v4 !== null)
|
|
183
|
+
return V4_START | bigOf(v4);
|
|
184
|
+
const v6 = ipv6Bytes(address);
|
|
185
|
+
return v6 === null ? null : bigOf(v6);
|
|
186
|
+
}
|
|
187
|
+
/** the places an entry names */
|
|
188
|
+
function rangeOf(c) {
|
|
189
|
+
const start = c.family === 4 ? V4_START | bigOf(c.bytes) : bigOf(c.bytes);
|
|
190
|
+
return { start, end: start + (1n << BigInt((c.family === 4 ? 32 : 128) - c.prefix)) - 1n };
|
|
191
|
+
}
|
|
192
|
+
/** Whether `address` (as `shownAddress` shows one, or in any spelling of the same address) is among those `from` names. */
|
|
193
|
+
export function proxyNames(from, address) {
|
|
194
|
+
const at = placeOf(address);
|
|
195
|
+
return at !== null && from.ranges.some((r) => r.start <= at && at <= r.end);
|
|
196
|
+
}
|
|
197
|
+
/** whether `ranges` together hold every place from `lo` to `hi` */
|
|
198
|
+
function covers(ranges, lo, hi) {
|
|
199
|
+
let next = lo;
|
|
200
|
+
for (const r of [...ranges].sort((a, b) => (a.start < b.start ? -1 : a.start > b.start ? 1 : 0))) {
|
|
201
|
+
if (r.start > next)
|
|
202
|
+
return false;
|
|
203
|
+
if (r.end >= next)
|
|
204
|
+
next = r.end + 1n;
|
|
205
|
+
if (next > hi)
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
return next > hi;
|
|
209
|
+
}
|
|
210
|
+
function prefixEqual(a, b, bits) {
|
|
211
|
+
for (let i = 0; i * 8 < bits; i++) {
|
|
212
|
+
const mask = bits - i * 8 >= 8 ? 0xff : (0xff << (8 - (bits - i * 8))) & 0xff;
|
|
213
|
+
if ((a[i] & mask) !== (b[i] & mask))
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
/** the network `bytes` / `prefix` names, its host bits cleared */
|
|
219
|
+
function network(bytes, prefix) {
|
|
220
|
+
return bytes.map((x, i) => i * 8 >= prefix ? 0 : prefix - i * 8 >= 8 ? x : x & ((0xff << (8 - (prefix - i * 8))) & 0xff));
|
|
221
|
+
}
|
|
222
|
+
const text = (family, bytes) => family === 4 ? Array.from(bytes).join(".") : ipv6Text(bytes);
|
|
223
|
+
/** One entry read, or what is wrong with it, with a did-you-mean where one is a fair guess: a guess is offered only
|
|
224
|
+
* once it is read as an entry itself, so it is never one that would be refused in turn. */
|
|
225
|
+
function cidrOf(entry) {
|
|
226
|
+
const quoted = JSON.stringify(entry);
|
|
227
|
+
const m = /^([^/]*)(?:\/(\d{1,3}))?$/.exec(entry);
|
|
228
|
+
const v4 = m === null ? null : ipv4Bytes(m[1]), v6 = m === null || v4 !== null ? null : ipv6Bytes(m[1]);
|
|
229
|
+
if (m === null || (v4 === null && v6 === null)) {
|
|
230
|
+
return { problem: `${quoted} is not an address or a CIDR (10.0.0.0/16, 2001:db8::/32, or one address such as 10.0.4.7; a host name is not looked up)` };
|
|
231
|
+
}
|
|
232
|
+
const family = v4 !== null ? 4 : 6, bytes = (v4 ?? v6), max = family === 4 ? 32 : 128;
|
|
233
|
+
const prefix = m[2] === undefined ? max : Number(m[2]);
|
|
234
|
+
if (prefix > max)
|
|
235
|
+
return { problem: `${quoted}: an IPv${family} prefix is at most /${max}` };
|
|
236
|
+
if (family === 6 && v4Mapped(bytes) && prefix >= 96) {
|
|
237
|
+
const p4 = prefix - 96, net4 = network(bytes.subarray(12, 16), p4);
|
|
238
|
+
const meant = `${text(4, net4)}${p4 === 32 ? "" : `/${p4}`}`, read = cidrOf(meant);
|
|
239
|
+
return { problem: "problem" in read
|
|
240
|
+
? `${quoted} is IPv4 written as IPv6, which Mason reads as ${meant}, and ${read.problem}`
|
|
241
|
+
: `${quoted} is IPv4 written as IPv6, which Mason reads as IPv4: did you mean ${meant}?` };
|
|
242
|
+
}
|
|
243
|
+
if (prefix === 0)
|
|
244
|
+
return { problem: `${quoted} names every address, so any client could claim to be any other: name the proxy's own addresses` };
|
|
245
|
+
const net = network(bytes, prefix);
|
|
246
|
+
if (!prefixEqual(net, bytes, max))
|
|
247
|
+
return { problem: `${quoted} has bits set past its prefix: did you mean ${text(family, net)}/${prefix}?` };
|
|
248
|
+
return { family, bytes: net, prefix };
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* `MASON_PG_PROXY_FROM` read: null when it is unset or empty (no header is believed from anyone, as before), the
|
|
252
|
+
* addresses it names, or why not: every entry that is not an address or a CIDR, each said with what is wrong, or a list
|
|
253
|
+
* whose entries TOGETHER name every IPv4 or every IPv6 address (read as places, never spellings), under which any
|
|
254
|
+
* client could claim to be any other.
|
|
255
|
+
*/
|
|
256
|
+
export function parseProxyFrom(value) {
|
|
257
|
+
if (value === undefined || value.trim() === "")
|
|
258
|
+
return null;
|
|
259
|
+
const entries = value.split(",").map((e) => e.trim());
|
|
260
|
+
const ranges = [], wrong = [];
|
|
261
|
+
for (const e of entries) {
|
|
262
|
+
if (e === "") {
|
|
263
|
+
wrong.push("an empty entry between two commas");
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const c = cidrOf(e);
|
|
267
|
+
if ("problem" in c)
|
|
268
|
+
wrong.push(c.problem);
|
|
269
|
+
else
|
|
270
|
+
ranges.push(rangeOf(c));
|
|
271
|
+
}
|
|
272
|
+
const setting = `${PROXY_SETTING}=${JSON.stringify(value)}`;
|
|
273
|
+
if (wrong.length)
|
|
274
|
+
return { problem: `${setting}: ${wrong.join("; ")}` };
|
|
275
|
+
const every = [
|
|
276
|
+
...(covers(ranges, V4_START, V4_END) ? ["every IPv4 address (0.0.0.0/0)"] : []),
|
|
277
|
+
// IPv6's every address but the v4-mapped ones, which are IPv4's and judged above
|
|
278
|
+
...(covers(ranges, 0n, V4_START - 1n) && covers(ranges, V4_END + 1n, ALL_END) ? ["every IPv6 address (::/0)"] : []),
|
|
279
|
+
];
|
|
280
|
+
if (every.length)
|
|
281
|
+
return { problem: `${setting}: its entries together name ${every.join(" and ")}, so any client could claim to be any other: name the proxy's own addresses` };
|
|
282
|
+
return { entries, ranges };
|
|
283
|
+
}
|
|
@@ -16,6 +16,12 @@ export const PG_API_TYPE_SPELLING = {
|
|
|
16
16
|
int2: "smallint", int4: "integer", int8: "bigint", float4: "real", float8: "double precision", numeric: "numeric", text: "text",
|
|
17
17
|
bool: "boolean", uuid: "uuid", date: "date", timestamptz: "timestamp with time zone", jsonb: "jsonb",
|
|
18
18
|
};
|
|
19
|
+
/** the input function's name of a type the catalog declares for a `made` column: what the planner tells one by */
|
|
20
|
+
export const HELD_TYPE_INPUT = "mason_held_in";
|
|
21
|
+
/** a composite's: its input function's name holds `HELD_TYPE_INPUT` too */
|
|
22
|
+
export const HELD_ROW_INPUT = "row_mason_held_in";
|
|
23
|
+
/** the name a `made` type is declared under, its typmod and `[]` left off (`vector(3)`, `pair[]`) */
|
|
24
|
+
export const madeTypeName = (spelled) => spelled.replace(/\[\]$/, "").replace(/\(\d+(,\d+)?\)$/, "");
|
|
19
25
|
/**
|
|
20
26
|
* A type as format_type spells it (`mood`, `other.level`, `"My Mood"`, `"a.b"."c"`, an array's `[]` left off): its
|
|
21
27
|
* schema as written (null when it is not qualified) and its name as pg_type.typname holds it (unquoted).
|