@ibgib/space-gib 0.0.1
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/CHANGELOG.md +31 -0
- package/Dockerfile +14 -0
- package/IMPLEMENTATION.md +484 -0
- package/README.md +46 -0
- package/dist/client/bootstrap.mjs +58 -0
- package/dist/client/bootstrap.mjs.map +7 -0
- package/dist/client/chunk-CT47Z5WU.mjs +21 -0
- package/dist/client/chunk-CT47Z5WU.mjs.map +7 -0
- package/dist/client/chunk-RHEDTRKF.mjs +235 -0
- package/dist/client/chunk-RHEDTRKF.mjs.map +7 -0
- package/dist/client/index.html +147 -0
- package/dist/client/index.mjs +2 -0
- package/dist/client/index.mjs.map +7 -0
- package/dist/client/script.mjs +2 -0
- package/dist/client/script.mjs.map +7 -0
- package/dist/client/style.css +605 -0
- package/dist/respec-gib.node.mjs +5 -0
- package/dist/server/server.mjs +20157 -0
- package/dist/server/server.mjs.map +7 -0
- package/generate-version-file.js +35 -0
- package/package.json +27 -0
- package/src/client/AUTO-GENERATED-version.mts +11 -0
- package/src/client/README.md +19 -0
- package/src/client/api/function-infos.web.mts +38 -0
- package/src/client/api/space-gib-api-bridge.mts +85 -0
- package/src/client/bootstrap.mts +49 -0
- package/src/client/components/keystone-creator/keystone-creator.css +139 -0
- package/src/client/components/keystone-creator/keystone-creator.html +26 -0
- package/src/client/components/keystone-creator/keystone-creator.mts +229 -0
- package/src/client/constants.mts +76 -0
- package/src/client/custom.d.ts +11 -0
- package/src/client/dev-tools.mts +540 -0
- package/src/client/helpers.web.mts +178 -0
- package/src/client/index.html +147 -0
- package/src/client/index.mts +59 -0
- package/src/client/script.mts +13 -0
- package/src/client/style.css +605 -0
- package/src/client/types.mts +85 -0
- package/src/client/ui/shell/space-gib-shell-constants.mts +24 -0
- package/src/client/ui/shell/space-gib-shell-service.mts +233 -0
- package/src/client/ui/shell/space-gib-shell-types.mts +5 -0
- package/src/client/witness/app/space-gib/space-gib-app-v1.mts +160 -0
- package/src/client/witness/app/space-gib/space-gib-constants.mts +38 -0
- package/src/client/witness/app/space-gib/space-gib-helper.mts +72 -0
- package/src/client/witness/app/space-gib/space-gib-types.mts +47 -0
- package/src/common/keystone-policies.mts +159 -0
- package/src/respec-gib.node.mts +6 -0
- package/src/server/README.md +18 -0
- package/src/server/bootstrap-helper.mts +141 -0
- package/src/server/bootstrap-helper.respec.mts +100 -0
- package/src/server/metaspace-nodeindexedspace/metaspace-nodeindexedspace.mts +85 -0
- package/src/server/path-constants.mts +89 -0
- package/src/server/path-helper.mts +101 -0
- package/src/server/path-helper.respec.mts +94 -0
- package/src/server/serve-gib/CHANGELOG.md +29 -0
- package/src/server/serve-gib/README.md +34 -0
- package/src/server/serve-gib/constants.mts +1 -0
- package/src/server/serve-gib/handlers/api/debug/ws-echo.handler.mts +104 -0
- package/src/server/serve-gib/handlers/api/health.handler.mts +23 -0
- package/src/server/serve-gib/handlers/api/health.respec.mts +51 -0
- package/src/server/serve-gib/handlers/api/ibgib/ibgib-handler-types.mts +49 -0
- package/src/server/serve-gib/handlers/api/ibgib/ibgib.handler.mts +176 -0
- package/src/server/serve-gib/handlers/api/keystone/keystone-evolve.handler.mts +261 -0
- package/src/server/serve-gib/handlers/api/keystone/keystone-genesis.handler.mts +146 -0
- package/src/server/serve-gib/handlers/api/keystone/keystone-get.handler.mts +198 -0
- package/src/server/serve-gib/handlers/api/keystone/keystone-get.respec.mts +107 -0
- package/src/server/serve-gib/handlers/api/keystone/keystone-handler-types.mts +29 -0
- package/src/server/serve-gib/handlers/api/keystone/keystone-post.handler.mts +70 -0
- package/src/server/serve-gib/handlers/api/keystone/keystone-post.respec.mts +130 -0
- package/src/server/serve-gib/handlers/error-handler.mts +36 -0
- package/src/server/serve-gib/handlers/handler-base.mts +383 -0
- package/src/server/serve-gib/handlers/static-handler.mts +82 -0
- package/src/server/serve-gib/handlers/ws/sync-upgrade.handler.mts +498 -0
- package/src/server/serve-gib/handlers/ws/ws-helper.mts +111 -0
- package/src/server/serve-gib/handlers/ws/ws-types.mts +53 -0
- package/src/server/serve-gib/serve-gib-helpers.mts +32 -0
- package/src/server/serve-gib/serve-gib-v1.mts +172 -0
- package/src/server/serve-gib/serve-gib.respec.mts +90 -0
- package/src/server/serve-gib/types.mts +102 -0
- package/src/server/server-constants.mts +2 -0
- package/src/server/server.mts +96 -0
- package/tsconfig.json +29 -0
- package/tsconfig.server.json +29 -0
- package/tsconfig.test.json +27 -0
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module handlers/ws/sync-upgrade.handler
|
|
3
|
+
*
|
|
4
|
+
* Handles WebSocket upgrades for the sync protocol.
|
|
5
|
+
* Implements a challenge/response handshake using Keystone identity.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { IncomingMessage } from 'node:http';
|
|
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';
|
|
25
|
+
import { API_PATH_REGEXES } from '../../../path-constants.mjs';
|
|
26
|
+
import { ServeGibHandlerWithMetaspaceBase } from '../handler-base.mjs';
|
|
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;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Handles WebSocket upgrades for sync by validating against Keystone identity.
|
|
35
|
+
*
|
|
36
|
+
* Reuses ServeGibHandlerWithMetaspaceBase to ensure consistent metaspace
|
|
37
|
+
* initialization from the request context.
|
|
38
|
+
*/
|
|
39
|
+
export interface SyncUpgradeQueryParams {
|
|
40
|
+
sAddr: string;
|
|
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}]`;
|
|
51
|
+
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
|
+
}
|
|
498
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { IncomingMessage } from 'node:http';
|
|
3
|
+
import { Socket } from 'node:net';
|
|
4
|
+
|
|
5
|
+
import { extractErrorMsg } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
|
|
6
|
+
|
|
7
|
+
const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
|
8
|
+
|
|
9
|
+
/** Encodes a UTF-8 string as a minimal unmasked WebSocket text frame. */
|
|
10
|
+
export function encodeTextFrame(text: string): Buffer {
|
|
11
|
+
const payload = Buffer.from(text, 'utf-8');
|
|
12
|
+
const payloadLen = payload.length;
|
|
13
|
+
|
|
14
|
+
let header: Buffer;
|
|
15
|
+
if (payloadLen < 126) {
|
|
16
|
+
header = Buffer.alloc(2);
|
|
17
|
+
header[0] = 0x81; // FIN + opcode 1 (text)
|
|
18
|
+
header[1] = payloadLen;
|
|
19
|
+
} else if (payloadLen < 65536) {
|
|
20
|
+
header = Buffer.alloc(4);
|
|
21
|
+
header[0] = 0x81;
|
|
22
|
+
header[1] = 126;
|
|
23
|
+
header.writeUInt16BE(payloadLen, 2);
|
|
24
|
+
} else {
|
|
25
|
+
header = Buffer.alloc(10);
|
|
26
|
+
header[0] = 0x81;
|
|
27
|
+
header[1] = 127;
|
|
28
|
+
header.writeBigUInt64BE(BigInt(payloadLen), 2);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return Buffer.concat([header, payload]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Decodes a masked client WebSocket text frame. Returns null for non-text or incomplete frames. */
|
|
35
|
+
export function decodeTextFrame(data: Buffer): string | null {
|
|
36
|
+
try {
|
|
37
|
+
if (data.length < 2) { return null; }
|
|
38
|
+
|
|
39
|
+
const opcode = data[0] & 0x0f;
|
|
40
|
+
if (opcode === 0x8) { return null; } // Close
|
|
41
|
+
if (opcode !== 0x1) { return null; } // Text only
|
|
42
|
+
|
|
43
|
+
const masked = (data[1] & 0x80) !== 0;
|
|
44
|
+
let payloadStart = 2;
|
|
45
|
+
let payloadLen = data[1] & 0x7f;
|
|
46
|
+
|
|
47
|
+
if (payloadLen === 126) {
|
|
48
|
+
payloadLen = data.readUInt16BE(2);
|
|
49
|
+
payloadStart = 4;
|
|
50
|
+
} else if (payloadLen === 127) {
|
|
51
|
+
payloadLen = Number(data.readBigUInt64BE(2));
|
|
52
|
+
payloadStart = 10;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (masked) {
|
|
56
|
+
const maskStart = payloadStart;
|
|
57
|
+
payloadStart += 4;
|
|
58
|
+
const mask = data.slice(maskStart, maskStart + 4);
|
|
59
|
+
const payload = data.slice(payloadStart, payloadStart + payloadLen);
|
|
60
|
+
for (let i = 0; i < payload.length; i++) {
|
|
61
|
+
payload[i] ^= mask[i % 4];
|
|
62
|
+
}
|
|
63
|
+
return payload.toString('utf-8');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return data.slice(payloadStart, payloadStart + payloadLen).toString('utf-8');
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error(`[ws-helper] decode error: ${extractErrorMsg(error)}`);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Encodes a WebSocket close frame (opcode 0x8) with optional status code. */
|
|
74
|
+
export function encodeCloseFrame(code = 1000): Buffer {
|
|
75
|
+
const frame = Buffer.alloc(4);
|
|
76
|
+
frame[0] = 0x88;
|
|
77
|
+
frame[1] = 2;
|
|
78
|
+
frame.writeUInt16BE(code, 2);
|
|
79
|
+
return frame;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Performs the RFC 6455 opening handshake. */
|
|
83
|
+
export function performHandshake(req: IncomingMessage, socket: Socket): boolean {
|
|
84
|
+
try {
|
|
85
|
+
const key = req.headers['sec-websocket-key'];
|
|
86
|
+
if (!key) {
|
|
87
|
+
socket.write('HTTP/1.1 400 Bad Request\r\n\r\nMissing Sec-WebSocket-Key');
|
|
88
|
+
socket.destroy();
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const acceptKey = createHash('sha1')
|
|
93
|
+
.update(key + WS_MAGIC)
|
|
94
|
+
.digest('base64');
|
|
95
|
+
|
|
96
|
+
const response = [
|
|
97
|
+
'HTTP/1.1 101 Switching Protocols',
|
|
98
|
+
'Upgrade: websocket',
|
|
99
|
+
'Connection: Upgrade',
|
|
100
|
+
`Sec-WebSocket-Accept: ${acceptKey}`,
|
|
101
|
+
'\r\n',
|
|
102
|
+
].join('\r\n');
|
|
103
|
+
|
|
104
|
+
socket.write(response);
|
|
105
|
+
return true;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error(`[ws-helper] handshake error: ${extractErrorMsg(error)}`);
|
|
108
|
+
socket.destroy();
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { KeystoneIbGib_V1 } from "@ibgib/core-gib/dist/keystone/keystone-types.mjs";
|
|
2
|
+
|
|
3
|
+
export type HandshakeMessageType =
|
|
4
|
+
'auth-challenge-init' |
|
|
5
|
+
'auth-init' |
|
|
6
|
+
'auth-challenge' |
|
|
7
|
+
'auth-proof' |
|
|
8
|
+
'auth-ok' |
|
|
9
|
+
'auth-fail';
|
|
10
|
+
|
|
11
|
+
export interface HandshakeMessageBase {
|
|
12
|
+
type: HandshakeMessageType;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface AuthChallengeInitMessage extends HandshakeMessageBase {
|
|
16
|
+
type: 'auth-challenge-init';
|
|
17
|
+
challengeUuid: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AuthInitMessage extends HandshakeMessageBase {
|
|
21
|
+
type: 'auth-init';
|
|
22
|
+
sAddr: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AuthChallengeMessage extends HandshakeMessageBase {
|
|
26
|
+
type: 'auth-challenge';
|
|
27
|
+
challengeUuid: string;
|
|
28
|
+
demandedIds: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AuthProofMessage extends HandshakeMessageBase {
|
|
32
|
+
type: 'auth-proof';
|
|
33
|
+
proofFrame: KeystoneIbGib_V1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AuthOkMessage extends HandshakeMessageBase {
|
|
37
|
+
type: 'auth-ok';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AuthFailMessage extends HandshakeMessageBase {
|
|
41
|
+
type: 'auth-fail';
|
|
42
|
+
message: string;
|
|
43
|
+
errors?: string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type HandshakeMessage =
|
|
47
|
+
AuthChallengeInitMessage |
|
|
48
|
+
AuthInitMessage |
|
|
49
|
+
AuthChallengeMessage |
|
|
50
|
+
AuthProofMessage |
|
|
51
|
+
AuthOkMessage |
|
|
52
|
+
AuthFailMessage;
|
|
53
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module serve-gib/serve-gib-helpers
|
|
3
|
+
*
|
|
4
|
+
* General purpose helper functions for the serve-gib framework.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Validates and coerces a boolean query parameter in place on the queryParams object.
|
|
9
|
+
*
|
|
10
|
+
* @param queryParams The parsed query parameters object
|
|
11
|
+
* @param key The specific key to validate and coerce
|
|
12
|
+
* @param errors Array to push error messages into if validation fails
|
|
13
|
+
*/
|
|
14
|
+
export function validateAndCoerceBooleanQueryParam(
|
|
15
|
+
queryParams: any,
|
|
16
|
+
key: string,
|
|
17
|
+
errors: string[]
|
|
18
|
+
): void {
|
|
19
|
+
const rawVal = queryParams[key];
|
|
20
|
+
if (typeof rawVal === 'string' &&
|
|
21
|
+
(rawVal.toLowerCase() === 'true' || rawVal.toLowerCase() === 'false')
|
|
22
|
+
) {
|
|
23
|
+
queryParams[key] = rawVal.toLowerCase() === 'true';
|
|
24
|
+
} else if (typeof rawVal === 'boolean') {
|
|
25
|
+
// do nothing, it's already a boolean
|
|
26
|
+
} else if (rawVal !== undefined) {
|
|
27
|
+
errors.push(`invalid value for "${key}". typeof should either be string or boolean. (E: c28ef87a2d4b4a6faef9a2c8db05b637)`);
|
|
28
|
+
} else {
|
|
29
|
+
// It's undefined, which means it wasn't provided in the URL.
|
|
30
|
+
// We do not error here because default parameters apply later or it may be optional.
|
|
31
|
+
}
|
|
32
|
+
}
|