@ibgib/space-gib 0.0.1 → 0.0.2
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/client/bootstrap.mjs +1 -1
- package/dist/client/bootstrap.mjs.map +1 -1
- package/dist/client/chunk-BL2SGXS4.mjs +18994 -0
- package/dist/client/chunk-RDTAT5G4.mjs +235 -0
- package/dist/client/chunk-RDTAT5G4.mjs.map +7 -0
- package/dist/client/chunk-RE7XSMHH.mjs +31 -0
- package/dist/client/chunk-RE7XSMHH.mjs.map +7 -0
- package/dist/client/chunk-YUSGN3J4.mjs +23119 -0
- package/dist/client/index.html +44 -8
- package/dist/client/index.mjs +1 -1
- package/dist/client/script.mjs +1 -1
- package/dist/client/style.css +26 -0
- package/dist/server/server.mjs +5323 -1011
- package/dist/server/server.mjs.map +4 -4
- package/package.json +1 -1
- package/src/client/AUTO-GENERATED-version.mts +1 -1
- package/src/client/api/space-gib-api-bridge.mts +84 -8
- package/src/client/dev-tools.mts +609 -24
- package/src/client/index.html +44 -8
- package/src/client/style.css +26 -0
- package/src/common/keystone-policies.json +64 -0
- package/src/common/keystone-policies.mts +39 -86
- package/src/server/serve-gib/handlers/api/debug/ws-echo.handler.mts +13 -12
- package/src/server/serve-gib/handlers/api/keystone/keystone-evolve.handler.mts +14 -167
- package/src/server/serve-gib/handlers/api/keystone/keystone-genesis.handler.mts +6 -6
- package/src/server/serve-gib/handlers/api/keystone/keystone-post.handler.mts +10 -25
- package/src/server/serve-gib/handlers/ws/sync-upgrade-handler-base.mts +201 -0
- package/src/server/serve-gib/handlers/ws/sync-upgrade.handler.mts +13 -487
- package/src/server/serve-gib/handlers/ws/ws-helper.mts +80 -3
- package/dist/client/chunk-CT47Z5WU.mjs +0 -21
- package/dist/client/chunk-CT47Z5WU.mjs.map +0 -7
- package/dist/client/chunk-RHEDTRKF.mjs +0 -235
- package/dist/client/chunk-RHEDTRKF.mjs.map +0 -7
- package/dist/respec-gib.node.mjs +0 -5
|
@@ -1,498 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module handlers/ws/sync-upgrade.handler
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Concrete implementation of the serve-gib WebSocket Sync Peer wrapper.
|
|
5
|
+
* Decoupled from core protocols by extending SyncUpgradeHandlerBase.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import { Socket } from 'node:net';
|
|
10
|
-
|
|
11
|
-
import { extractErrorMsg, getUUID } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
|
|
12
|
-
import { IbGibAddr } from '@ibgib/ts-gib/dist/types.mjs';
|
|
13
|
-
import { getIbGibAddr, getIbAndGib } from '@ibgib/ts-gib/dist/helper.mjs';
|
|
14
|
-
import { validateIbGibAddr } from '@ibgib/ts-gib/dist/V1/validate-helper.mjs';
|
|
15
|
-
import { KeystoneService_V1 } from '@ibgib/core-gib/dist/keystone/keystone-service-v1.mjs';
|
|
16
|
-
import { KeystoneIbGib_V1 } from '@ibgib/core-gib/dist/keystone/keystone-types.mjs';
|
|
17
|
-
import { parseKeystoneIb } from '@ibgib/core-gib/dist/keystone/keystone-helpers.mjs';
|
|
18
|
-
import { IbGib_V1 } from '@ibgib/ts-gib/dist/V1/types.mjs';
|
|
19
|
-
import { IbGibSpaceResultIbGib, IbGibSpaceResultData, IbGibSpaceResultRel8ns } from '@ibgib/core-gib/dist/witness/space/space-types.mjs';
|
|
20
|
-
|
|
21
|
-
import { GLOBAL_LOG_A_LOT } from '../../constants.mjs';
|
|
22
|
-
import {
|
|
23
|
-
encodeTextFrame, decodeTextFrame, encodeCloseFrame, performHandshake
|
|
24
|
-
} from './ws-helper.mjs';
|
|
8
|
+
import { SyncUpgradeHandlerBase } from './sync-upgrade-handler-base.mjs';
|
|
25
9
|
import { API_PATH_REGEXES } from '../../../path-constants.mjs';
|
|
26
|
-
import {
|
|
27
|
-
import { ParamsWithDomain, RequestContext, ResponseResult, ServeGibHttpMethod } from '../../types.mjs';
|
|
28
|
-
import { SESSION_KEYSTONE_POLICY, checkHandshakeSolution } from '../../../../common/keystone-policies.mjs';
|
|
29
|
-
import { AuthChallengeMessage, AuthFailMessage, AuthInitMessage, AuthOkMessage, AuthProofMessage, HandshakeMessage } from './ws-types.mjs';
|
|
30
|
-
|
|
31
|
-
const logalot = GLOBAL_LOG_A_LOT || true;
|
|
10
|
+
import { ServeGibHttpMethod } from '../../types.mjs';
|
|
32
11
|
|
|
33
12
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* initialization from the request context.
|
|
13
|
+
* SyncChannelHandler manages the WebSocket Connect and Transport channel.
|
|
14
|
+
* Delegates 100% of connect handshake, signature verify, and sync turns to
|
|
15
|
+
* SyncPeerWebSocketReceiver_V1.
|
|
38
16
|
*/
|
|
39
|
-
export
|
|
40
|
-
|
|
41
|
-
solution: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface SyncUpgradeRequestContext extends RequestContext<ParamsWithDomain, SyncUpgradeQueryParams> {
|
|
45
|
-
sAddr_tjp?: IbGibAddr;
|
|
46
|
-
demandedIds?: string[];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export class SyncUpgradeHandler extends ServeGibHandlerWithMetaspaceBase<ParamsWithDomain, SyncUpgradeQueryParams> {
|
|
50
|
-
protected override lc: string = `[${SyncUpgradeHandler.name}]`;
|
|
17
|
+
export class SyncChannelHandler extends SyncUpgradeHandlerBase {
|
|
18
|
+
protected override lc: string = `[${SyncChannelHandler.name}]`;
|
|
51
19
|
protected override method: ServeGibHttpMethod = 'GET';
|
|
52
|
-
protected override regex = API_PATH_REGEXES.SYNC_WS;
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Entry point for WebSocket upgrades.
|
|
56
|
-
* Orchestrates the upgrade, handshake, and keystone authorization.
|
|
57
|
-
* @returns true if the upgrade was handled (even if it failed auth), false if path mismatch.
|
|
58
|
-
*/
|
|
59
|
-
async handleUpgrade(reqCtx: RequestContext<ParamsWithDomain>, socket: Socket, head: Buffer): Promise<boolean> {
|
|
60
|
-
const lc_fn = `${this.lc}[handleUpgrade]`;
|
|
61
|
-
try {
|
|
62
|
-
if (logalot) { console.log(`${lc_fn} starting... (I: d772c676239f1f0a823c14a8063d4826)`); }
|
|
63
|
-
|
|
64
|
-
// 1. Initial validation
|
|
65
|
-
if (!this.canHandleRoute(reqCtx)) {
|
|
66
|
-
if (logalot) { console.log(`${lc_fn} path/method mismatch. (I: 234198c0f649dfdf0e2d21f86b3ff826)`); }
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const ctx = reqCtx as SyncUpgradeRequestContext;
|
|
71
|
-
|
|
72
|
-
// Populate params (e.g. domain info) from the request context
|
|
73
|
-
ctx.params = await this.parseParams(ctx);
|
|
74
|
-
ctx.queryParams = await this.parseQueryParams(ctx);
|
|
75
|
-
|
|
76
|
-
// 2. Initialize Metaspace Context (bootstrapDomainMetaspace)
|
|
77
|
-
await this.initConcreteContext(ctx);
|
|
78
|
-
|
|
79
|
-
// 2b. Perform upfront picket-fence pre-filter validation
|
|
80
|
-
await this.validateUpfrontHandshake(ctx);
|
|
81
|
-
|
|
82
|
-
// 3. RFC 6455 Handshake
|
|
83
|
-
const ok = performHandshake(ctx as any as IncomingMessage, socket);
|
|
84
|
-
if (!ok) { return true; }
|
|
85
|
-
|
|
86
|
-
// 4. Setup Auth Challenge
|
|
87
|
-
const challengeUuid = await getUUID();
|
|
88
|
-
|
|
89
|
-
socket.write(encodeTextFrame(JSON.stringify({
|
|
90
|
-
type: 'auth-challenge-init',
|
|
91
|
-
challengeUuid
|
|
92
|
-
})));
|
|
93
|
-
|
|
94
|
-
socket.on('data', async (data: Buffer) => {
|
|
95
|
-
await this.handleUpgrade_socketDataHandler({
|
|
96
|
-
reqCtx: ctx,
|
|
97
|
-
challengeUuid,
|
|
98
|
-
socket,
|
|
99
|
-
data,
|
|
100
|
-
})
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
return true;
|
|
104
|
-
} catch (error) {
|
|
105
|
-
console.error(`${lc_fn} fatal: ${extractErrorMsg(error)}`);
|
|
106
|
-
socket.destroy();
|
|
107
|
-
return true;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
protected async handleUpgrade_socketDataHandler({
|
|
112
|
-
reqCtx,
|
|
113
|
-
challengeUuid,
|
|
114
|
-
socket,
|
|
115
|
-
data,
|
|
116
|
-
}: {
|
|
117
|
-
reqCtx: SyncUpgradeRequestContext,
|
|
118
|
-
challengeUuid: string,
|
|
119
|
-
socket: Socket,
|
|
120
|
-
data: Buffer,
|
|
121
|
-
}): Promise<any> {
|
|
122
|
-
const lc = `[${this.handleUpgrade_socketDataHandler.name}]`;
|
|
123
|
-
if (logalot) { console.log(`${lc} starting... (I: 3ba9a857899f0479030e323b755fe826)`); }
|
|
124
|
-
try {
|
|
125
|
-
debugger; //
|
|
126
|
-
const text = decodeTextFrame(data);
|
|
127
|
-
if (text === null) {
|
|
128
|
-
socket.write(encodeCloseFrame());
|
|
129
|
-
socket.end();
|
|
130
|
-
return; /* <<<< returns early */
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const msg = JSON.parse(text) as HandshakeMessage;
|
|
134
|
-
switch (msg.type) {
|
|
135
|
-
case 'auth-init':
|
|
136
|
-
await this.handleUpgrade_socketDataHandler_authInit({
|
|
137
|
-
reqCtx,
|
|
138
|
-
challengeUuid,
|
|
139
|
-
socket,
|
|
140
|
-
msg,
|
|
141
|
-
})
|
|
142
|
-
break;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
case 'auth-proof':
|
|
146
|
-
await this.handleUpgrade_socketDataHandler_authProof({
|
|
147
|
-
reqCtx,
|
|
148
|
-
challengeUuid,
|
|
149
|
-
socket,
|
|
150
|
-
msg,
|
|
151
|
-
});
|
|
152
|
-
break;
|
|
153
|
-
|
|
154
|
-
default:
|
|
155
|
-
if (logalot) { console.log(`${lc} unexpected message type: ${msg.type}`); }
|
|
156
|
-
throw new Error(`unknown msg.type ${msg.type} (E: 8143084554d848f48801fcd5dc5fc826)`);
|
|
157
|
-
}
|
|
158
|
-
} catch (error) {
|
|
159
|
-
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
160
|
-
socket.write(encodeTextFrame(JSON.stringify({
|
|
161
|
-
type: 'auth-fail',
|
|
162
|
-
message: extractErrorMsg(error)
|
|
163
|
-
} as AuthFailMessage)));
|
|
164
|
-
} finally {
|
|
165
|
-
if (logalot) { console.log(`${lc} complete.`); }
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
protected async handleUpgrade_socketDataHandler_authInit({
|
|
170
|
-
reqCtx,
|
|
171
|
-
challengeUuid,
|
|
172
|
-
socket,
|
|
173
|
-
msg,
|
|
174
|
-
}: {
|
|
175
|
-
reqCtx: SyncUpgradeRequestContext,
|
|
176
|
-
challengeUuid: string,
|
|
177
|
-
socket: Socket,
|
|
178
|
-
msg: AuthInitMessage,
|
|
179
|
-
}): Promise<any> {
|
|
180
|
-
const lc = `${this.lc}[${this.handleUpgrade_socketDataHandler_authInit.name}]`;
|
|
181
|
-
try {
|
|
182
|
-
if (logalot) { console.log(`${lc} starting... (I: 9ef8c8b892f82c7f98e34168f89c4826)`); }
|
|
183
|
-
|
|
184
|
-
const { sAddr } = msg;
|
|
185
|
-
if (logalot) { console.log(`${lc} auth-init for ${sAddr}`); }
|
|
186
|
-
|
|
187
|
-
// Load authorized S frame from context's metaspace
|
|
188
|
-
const metaspace = reqCtx.metaspace!;
|
|
189
|
-
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
190
|
-
if (!space) { throw new Error("No space."); }
|
|
191
|
-
|
|
192
|
-
let authorizedS: KeystoneIbGib_V1;
|
|
193
|
-
try {
|
|
194
|
-
authorizedS = await this.getSessionKeystone({ metaspace, space, sAddr });
|
|
195
|
-
if (logalot) { console.log(`${lc} loaded authorizedS:`, JSON.stringify(authorizedS).slice(0, 200) + '...'); }
|
|
196
|
-
} catch (error) {
|
|
197
|
-
console.warn(`${lc} session keystone not found: ${extractErrorMsg(error)}`);
|
|
198
|
-
socket.write(encodeTextFrame(JSON.stringify({
|
|
199
|
-
type: 'auth-fail',
|
|
200
|
-
message: 'Session keystone not found on server. Did you complete Step 4?'
|
|
201
|
-
} as AuthFailMessage)));
|
|
202
|
-
return; /* <<<< returns early */
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Identify handshake pool and select demanded IDs
|
|
206
|
-
const handshakePool = (authorizedS.data?.challengePools ?? []).find(p => p.id === SESSION_KEYSTONE_POLICY.HANDSHAKE_POOL.ID);
|
|
207
|
-
if (!handshakePool) {
|
|
208
|
-
socket.write(encodeTextFrame(JSON.stringify({
|
|
209
|
-
type: 'auth-fail',
|
|
210
|
-
message: `Session keystone missing "${SESSION_KEYSTONE_POLICY.HANDSHAKE_POOL.ID}" pool`
|
|
211
|
-
} as AuthFailMessage)));
|
|
212
|
-
return; /* <<<< returns early */
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const challengeIds = Object.keys(handshakePool.challenges);
|
|
216
|
-
const demandedIds = challengeIds.sort(() => 0.5 - Math.random()).slice(0, SESSION_KEYSTONE_POLICY.HANDSHAKE_POOL.SERVER_DEMAND_COUNT);
|
|
217
|
-
reqCtx.demandedIds = demandedIds;
|
|
218
|
-
|
|
219
|
-
socket.write(encodeTextFrame(JSON.stringify({
|
|
220
|
-
type: 'auth-challenge',
|
|
221
|
-
challengeUuid,
|
|
222
|
-
demandedIds
|
|
223
|
-
} as AuthChallengeMessage)));
|
|
224
|
-
|
|
225
|
-
} catch (error) {
|
|
226
|
-
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
227
|
-
throw error;
|
|
228
|
-
} finally {
|
|
229
|
-
if (logalot) { console.log(`${lc} complete.`); }
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* At this point, the session keystone (S^Stjp) according to the server only
|
|
236
|
-
* has the first frame (tjp) persisted. But the client has successfully
|
|
237
|
-
* initiated the handshake, so it provided the one manual challenge.
|
|
238
|
-
*
|
|
239
|
-
* We (the server) then sent the client some challenge ids and said to the
|
|
240
|
-
* client "Hey, solve the keystone with these challenges included."
|
|
241
|
-
*
|
|
242
|
-
* So now, the client has sent the next evolution of the session keystone,
|
|
243
|
-
* S^S1.Stjp, with those challenge ids included.
|
|
244
|
-
*/
|
|
245
|
-
protected async handleUpgrade_socketDataHandler_authProof({
|
|
246
|
-
reqCtx,
|
|
247
|
-
challengeUuid,
|
|
248
|
-
socket,
|
|
249
|
-
msg,
|
|
250
|
-
}: {
|
|
251
|
-
reqCtx: SyncUpgradeRequestContext,
|
|
252
|
-
challengeUuid: string,
|
|
253
|
-
socket: Socket,
|
|
254
|
-
msg: AuthProofMessage,
|
|
255
|
-
}): Promise<any> {
|
|
256
|
-
const lc = `${this.lc}[${this.handleUpgrade_socketDataHandler_authProof.name}]`;
|
|
257
|
-
try {
|
|
258
|
-
if (logalot) { console.log(`${lc} starting... (I: b0c185793602f4e4d862785e906cdd26)`); }
|
|
259
|
-
const { proofFrame } = msg;
|
|
260
|
-
if (logalot) { console.log(`${lc} verifying auth-proof...`); }
|
|
261
|
-
|
|
262
|
-
const sAddr_proofFrame = getIbGibAddr({ ibGib: proofFrame });
|
|
263
|
-
|
|
264
|
-
// need to get the tip of session keystone
|
|
265
|
-
const metaspace = reqCtx.metaspace!;
|
|
266
|
-
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
267
|
-
if (!space) { throw new Error(`(UNEXPECTED) no default local user space? (E: 1443da451007921b2da9d2c8b15a4826)`); }
|
|
268
|
-
const sAddr_tjp = reqCtx.sAddr_tjp;
|
|
269
|
-
if (!sAddr_tjp) {
|
|
270
|
-
throw new Error("Missing upfront verified session keystone TJP address (E: cb1998fa7c4a11f5ee87b001a4e126a1)");
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* this is the latest/tip addr for session keystone that the server
|
|
274
|
-
* is currently aware of.
|
|
275
|
-
*
|
|
276
|
-
* NOTE: We do NOT (!!!) trust the incoming session keystone to
|
|
277
|
-
* drive what the client thinks it the latest addr. Not only is this
|
|
278
|
-
* in the control of a would-be attacker, the client could just be
|
|
279
|
-
* out of date with a race condition.
|
|
280
|
-
*/
|
|
281
|
-
const sAddr_latest = await metaspace.getLatestAddr({ tjpAddr: sAddr_tjp, space });
|
|
282
|
-
if (!sAddr_latest) {
|
|
283
|
-
socket.write(encodeTextFrame(JSON.stringify({
|
|
284
|
-
type: 'auth-fail',
|
|
285
|
-
message: 'Authorized session keystone tip not found'
|
|
286
|
-
} as AuthFailMessage)));
|
|
287
|
-
return; /* <<<< returns early */
|
|
288
|
-
}
|
|
289
|
-
const sKeystone_latest = await this.getSessionKeystone({
|
|
290
|
-
sAddr: sAddr_latest,
|
|
291
|
-
metaspace,
|
|
292
|
-
space,
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
const keystoneService = new KeystoneService_V1();
|
|
296
|
-
const validationErrors = await keystoneService.validate({
|
|
297
|
-
prevIbGib: sKeystone_latest,
|
|
298
|
-
currentIbGib: proofFrame,
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
if (validationErrors && validationErrors.length > 0) {
|
|
302
|
-
socket.write(encodeTextFrame(JSON.stringify({
|
|
303
|
-
type: 'auth-fail',
|
|
304
|
-
message: 'Proof validation failed',
|
|
305
|
-
errors: validationErrors
|
|
306
|
-
} as AuthFailMessage)));
|
|
307
|
-
return; /* <<<< returns early */
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const proofs = proofFrame.data.proofs ?? [];
|
|
311
|
-
const proof = proofs.find(p => p.claim?.verb === SESSION_KEYSTONE_POLICY.HANDSHAKE_POOL.VERB);
|
|
312
|
-
if (!proof || proof.claim?.target !== challengeUuid) {
|
|
313
|
-
socket.write(encodeTextFrame(JSON.stringify({
|
|
314
|
-
type: 'auth-fail',
|
|
315
|
-
message: 'Proof target mismatch or missing handshake claim'
|
|
316
|
-
} as AuthFailMessage)));
|
|
317
|
-
return; /* <<<< returns early */
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Ensure that all of our demanded challenge ids are present in the proof solutions
|
|
321
|
-
const demandedIds = reqCtx.demandedIds;
|
|
322
|
-
if (!demandedIds || demandedIds.length === 0) {
|
|
323
|
-
socket.write(encodeTextFrame(JSON.stringify({
|
|
324
|
-
type: 'auth-fail',
|
|
325
|
-
message: 'Server missing active handshake challenge state'
|
|
326
|
-
} as AuthFailMessage)));
|
|
327
|
-
return; /* <<<< returns early */
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const solvedIds = new Set(proof.solutions?.map(s => s.challengeId) ?? []);
|
|
331
|
-
const missingIds = demandedIds.filter(id => !solvedIds.has(id));
|
|
332
|
-
if (missingIds.length > 0) {
|
|
333
|
-
socket.write(encodeTextFrame(JSON.stringify({
|
|
334
|
-
type: 'auth-fail',
|
|
335
|
-
message: `Proof is missing demanded solutions: ${missingIds.join(', ')}`
|
|
336
|
-
} as AuthFailMessage)));
|
|
337
|
-
return; /* <<<< returns early */
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Persist the newly validated evolved session keystone tip to the server space
|
|
341
|
-
await metaspace.put({ ibGibs: [proofFrame], space });
|
|
342
|
-
|
|
343
|
-
if (logalot) { console.log(`${lc} auth successful for ${sAddr_proofFrame} (I: 6d02c649f55c81cfe8f11988f44bca26)`); }
|
|
344
|
-
socket.write(encodeTextFrame(JSON.stringify({ type: 'auth-ok' } as AuthOkMessage)));
|
|
345
|
-
} catch (error) {
|
|
346
|
-
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
347
|
-
throw error;
|
|
348
|
-
} finally {
|
|
349
|
-
if (logalot) { console.log(`${lc} complete.`); }
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
protected async getSessionKeystone({
|
|
354
|
-
metaspace,
|
|
355
|
-
space,
|
|
356
|
-
sAddr,
|
|
357
|
-
}: {
|
|
358
|
-
metaspace: NonNullable<RequestContext['metaspace']>,
|
|
359
|
-
space: any,
|
|
360
|
-
sAddr: string,
|
|
361
|
-
}): Promise<KeystoneIbGib_V1> {
|
|
362
|
-
const lc = `${this.lc}[${this.getSessionKeystone.name}]`;
|
|
363
|
-
try {
|
|
364
|
-
if (logalot) { console.log(`${lc} starting... (I: cb9af0e14d3345bfbc0be35728de7c26)`); }
|
|
365
|
-
|
|
366
|
-
const resGet = await metaspace.get({ addrs: [sAddr], space });
|
|
367
|
-
|
|
368
|
-
// Validate result
|
|
369
|
-
if (resGet.success && resGet.ibGibs && resGet.ibGibs.length === 1) {
|
|
370
|
-
return resGet.ibGibs[0] as KeystoneIbGib_V1;
|
|
371
|
-
} else {
|
|
372
|
-
// Robust error handling using rawResultIbGib
|
|
373
|
-
const resIbGib = resGet.rawResultIbGib as IbGibSpaceResultIbGib<IbGib_V1, IbGibSpaceResultData, IbGibSpaceResultRel8ns>;
|
|
374
|
-
const addrsNotFound = resIbGib?.data?.addrsNotFound ?? 'unknown';
|
|
375
|
-
throw new Error(`couldn't find all addrs. addrsNotFound: ${addrsNotFound}? resGet.errorMsg: ${resGet.errorMsg}. space.ib: ${space.ib} (E: f3c87e2b19794358a9e701cd59b8eb26)`);
|
|
376
|
-
}
|
|
377
|
-
} catch (error) {
|
|
378
|
-
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
379
|
-
throw error;
|
|
380
|
-
} finally {
|
|
381
|
-
if (logalot) { console.log(`${lc} complete.`); }
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
protected async validateUpfrontHandshake(reqCtx: SyncUpgradeRequestContext): Promise<void> {
|
|
386
|
-
const lc = `${this.lc}[${this.validateUpfrontHandshake.name}]`;
|
|
387
|
-
const sAddr = reqCtx.queryParams?.sAddr;
|
|
388
|
-
const solution = reqCtx.queryParams?.solution;
|
|
389
|
-
|
|
390
|
-
if (!sAddr || !solution) {
|
|
391
|
-
throw new Error(`Missing upfront handshake parameters (sAddr, solution) (E: 8a4c9a724ddf59998199ec87a9ee126)`);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
const metaspace = reqCtx.metaspace!;
|
|
395
|
-
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
396
|
-
if (!space) { throw new Error("No space."); }
|
|
397
|
-
|
|
398
|
-
const authorizedS = await this.getSessionKeystone({ metaspace, space, sAddr });
|
|
399
|
-
const isValid = await checkHandshakeSolution(authorizedS, solution);
|
|
400
|
-
if (!isValid) {
|
|
401
|
-
throw new Error(`Upfront handshake solution verification failed (E: 804c98a724ddf59998199ec87a9ee127)`);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Store secure TJP address of the verified session keystone in reqCtx
|
|
405
|
-
const past = authorizedS.rel8ns?.past;
|
|
406
|
-
const tjpAddr = (past && past.length > 0) ? past[0] : getIbGibAddr({ ibGib: authorizedS });
|
|
407
|
-
reqCtx.sAddr_tjp = tjpAddr;
|
|
408
|
-
|
|
409
|
-
if (logalot) { console.log(`${lc} Upfront picket-fence verification succeeded for ${sAddr}!`); }
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
protected override async parseParamsImpl(reqCtx: RequestContext<ParamsWithDomain>): Promise<ParamsWithDomain | undefined> {
|
|
413
|
-
const lc = `${this.lc}[${this.parseParamsImpl.name}]`;
|
|
414
|
-
try {
|
|
415
|
-
const match = reqCtx.pathname.match(this.regex);
|
|
416
|
-
if (!match) return undefined;
|
|
417
|
-
const domainIb = decodeURIComponent(match[1]);
|
|
418
|
-
const domainGib = decodeURIComponent(match[2]);
|
|
419
|
-
const domainAddr = getIbGibAddr({ ib: domainIb, gib: domainGib });
|
|
420
|
-
|
|
421
|
-
return {
|
|
422
|
-
domainInfo: this.getDomainInfo({ domainAddr })
|
|
423
|
-
};
|
|
424
|
-
} catch (error) {
|
|
425
|
-
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
426
|
-
throw error;
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
protected override async parseQueryParams(
|
|
431
|
-
reqCtx: RequestContext<ParamsWithDomain, SyncUpgradeQueryParams>
|
|
432
|
-
): Promise<SyncUpgradeQueryParams | undefined> {
|
|
433
|
-
const lc = `${this.lc}[${this.parseQueryParams.name}]`;
|
|
434
|
-
try {
|
|
435
|
-
if (logalot) { console.log(`${lc} starting... (I: 3ed5ba8d47af31f538fbdb484cfdb826)`); }
|
|
436
|
-
|
|
437
|
-
const sAddrRaw = reqCtx.url.searchParams.get('sAddr');
|
|
438
|
-
const sAddr = sAddrRaw ? decodeURIComponent(sAddrRaw) : undefined;
|
|
439
|
-
const solution = reqCtx.url.searchParams.get('solution') ?? undefined;
|
|
440
|
-
|
|
441
|
-
const queryParams: Partial<SyncUpgradeQueryParams> = {};
|
|
442
|
-
if (sAddr !== undefined) { queryParams.sAddr = sAddr; }
|
|
443
|
-
if (solution !== undefined) { queryParams.solution = solution; }
|
|
444
|
-
|
|
445
|
-
if (Object.keys(queryParams).length > 0) {
|
|
446
|
-
const validationErrors = await this.validateQueryParams({ queryParams });
|
|
447
|
-
if (validationErrors.length === 0) {
|
|
448
|
-
return queryParams as SyncUpgradeQueryParams;
|
|
449
|
-
} else {
|
|
450
|
-
throw new Error(`invalid query params. validationErrors: ${validationErrors.join(', ')} (E: 9355f8a9643803f52f2241f1c7f39826)`);
|
|
451
|
-
}
|
|
452
|
-
} else {
|
|
453
|
-
return undefined;
|
|
454
|
-
}
|
|
455
|
-
} catch (error) {
|
|
456
|
-
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
457
|
-
throw error;
|
|
458
|
-
} finally {
|
|
459
|
-
if (logalot) { console.log(`${lc} complete.`); }
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
protected override async validateQueryParams({ queryParams }: { queryParams: any }): Promise<string[]> {
|
|
464
|
-
const errors: string[] = [];
|
|
465
|
-
if (!queryParams) {
|
|
466
|
-
return ["Missing query parameters: sAddr, solution"];
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
if (!queryParams.sAddr) {
|
|
470
|
-
errors.push("Missing sAddr");
|
|
471
|
-
} else {
|
|
472
|
-
try {
|
|
473
|
-
const decodedSAddr = queryParams.sAddr;
|
|
474
|
-
|
|
475
|
-
// use validateIbGibAddr to ensure that it's a valid address.
|
|
476
|
-
const addrErrors = validateIbGibAddr({ addr: decodedSAddr }) ?? [];
|
|
477
|
-
if (addrErrors.length > 0) {
|
|
478
|
-
errors.push(`Invalid sAddr: ${addrErrors.join(', ')}`);
|
|
479
|
-
} else {
|
|
480
|
-
// get the ib and ensure that it is a keystone ib using parseKeystoneIb (will throw if invalid)
|
|
481
|
-
const { ib } = getIbAndGib({ ibGibAddr: decodedSAddr });
|
|
482
|
-
parseKeystoneIb({ ib });
|
|
483
|
-
}
|
|
484
|
-
} catch (error) {
|
|
485
|
-
errors.push(`Invalid sAddr: ${extractErrorMsg(error)}`);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
if (!queryParams.solution) {
|
|
490
|
-
errors.push("Missing solution");
|
|
491
|
-
}
|
|
492
|
-
return errors;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
protected override async handleRouteImpl(reqCtx: RequestContext<ParamsWithDomain>): Promise<ResponseResult | undefined> {
|
|
496
|
-
return this.error(400, 'WS endpoint requires upgrade');
|
|
497
|
-
}
|
|
20
|
+
protected override regex: RegExp = API_PATH_REGEXES.SYNC_WS;
|
|
498
21
|
}
|
|
22
|
+
|
|
23
|
+
/** Keep naming backward-compatible for server registrations */
|
|
24
|
+
export const SyncUpgradeHandler = SyncChannelHandler;
|
|
@@ -79,8 +79,8 @@ export function encodeCloseFrame(code = 1000): Buffer {
|
|
|
79
79
|
return frame;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
/** Performs the RFC 6455 opening handshake. */
|
|
83
|
-
export function
|
|
82
|
+
/** Performs the RFC 6455 opening connect handshake. */
|
|
83
|
+
export function performConnect(req: IncomingMessage, socket: Socket): boolean {
|
|
84
84
|
try {
|
|
85
85
|
const key = req.headers['sec-websocket-key'];
|
|
86
86
|
if (!key) {
|
|
@@ -104,8 +104,85 @@ export function performHandshake(req: IncomingMessage, socket: Socket): boolean
|
|
|
104
104
|
socket.write(response);
|
|
105
105
|
return true;
|
|
106
106
|
} catch (error) {
|
|
107
|
-
console.error(`[ws-helper]
|
|
107
|
+
console.error(`[ws-helper] connect error: ${extractErrorMsg(error)}`);
|
|
108
108
|
socket.destroy();
|
|
109
109
|
return false;
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Stateful WebSocket text frame decoder that aggregates incoming TCP chunks
|
|
115
|
+
* and decodes full RFC 6455 frames.
|
|
116
|
+
*/
|
|
117
|
+
export class WebSocketFrameDecoder {
|
|
118
|
+
private buffer: Buffer = Buffer.alloc(0);
|
|
119
|
+
|
|
120
|
+
/** Adds new data chunk from TCP socket. */
|
|
121
|
+
addChunk(chunk: Buffer): void {
|
|
122
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Parses the next complete text frame from the accumulated buffer.
|
|
127
|
+
* Returns:
|
|
128
|
+
* - `string` if a complete text frame was decoded successfully (and consumes those bytes).
|
|
129
|
+
* - `null` if the buffer is incomplete or we need more data.
|
|
130
|
+
* - `throws Error` on protocol errors or close frames.
|
|
131
|
+
*/
|
|
132
|
+
nextFrame(): string | null {
|
|
133
|
+
if (this.buffer.length < 2) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const opcode = this.buffer[0] & 0x0f;
|
|
138
|
+
if (opcode === 0x8) {
|
|
139
|
+
throw new Error('Close frame received');
|
|
140
|
+
}
|
|
141
|
+
if (opcode !== 0x1) {
|
|
142
|
+
throw new Error(`Unsupported WebSocket opcode: 0x${opcode.toString(16)}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const secondByte = this.buffer[1];
|
|
146
|
+
const masked = (secondByte & 0x80) !== 0;
|
|
147
|
+
let payloadLen = secondByte & 0x7f;
|
|
148
|
+
let headerLen = 2;
|
|
149
|
+
|
|
150
|
+
if (payloadLen === 126) {
|
|
151
|
+
if (this.buffer.length < 4) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
payloadLen = this.buffer.readUInt16BE(2);
|
|
155
|
+
headerLen = 4;
|
|
156
|
+
} else if (payloadLen === 127) {
|
|
157
|
+
if (this.buffer.length < 10) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
payloadLen = Number(this.buffer.readBigUInt64BE(2));
|
|
161
|
+
headerLen = 10;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const maskLen = masked ? 4 : 0;
|
|
165
|
+
const totalFrameLen = headerLen + maskLen + payloadLen;
|
|
166
|
+
|
|
167
|
+
if (this.buffer.length < totalFrameLen) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const frameData = this.buffer.subarray(0, totalFrameLen);
|
|
172
|
+
this.buffer = this.buffer.subarray(totalFrameLen);
|
|
173
|
+
|
|
174
|
+
let payloadStart = headerLen;
|
|
175
|
+
if (masked) {
|
|
176
|
+
const mask = frameData.subarray(payloadStart, payloadStart + 4);
|
|
177
|
+
payloadStart += 4;
|
|
178
|
+
const payload = Buffer.from(frameData.subarray(payloadStart));
|
|
179
|
+
for (let i = 0; i < payload.length; i++) {
|
|
180
|
+
payload[i] ^= mask[i % 4];
|
|
181
|
+
}
|
|
182
|
+
return payload.toString('utf-8');
|
|
183
|
+
} else {
|
|
184
|
+
const payload = frameData.subarray(payloadStart);
|
|
185
|
+
return payload.toString('utf-8');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|