Skip to content

Commit 3493237

Browse files
committed
wip: added authentication timeout and other fixes
1 parent 4c2d5d8 commit 3493237

File tree

3 files changed

+155
-31
lines changed

3 files changed

+155
-31
lines changed

src/nodes/NodeManager.ts

Lines changed: 80 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import type Sigchain from '../sigchain/Sigchain';
77
import type TaskManager from '../tasks/TaskManager';
88
import type GestaltGraph from '../gestalts/GestaltGraph';
99
import type {
10+
Task,
1011
TaskHandler,
1112
TaskHandlerId,
12-
Task,
1313
TaskInfo,
1414
} from '../tasks/types';
1515
import type { SignedTokenEncoded } from '../tokens/types';
@@ -23,32 +23,33 @@ import type {
2323
import type { ClaimLinkNode } from '../claims/payloads';
2424
import type NodeConnection from '../nodes/NodeConnection';
2525
import type {
26+
AgentClaimMessage,
2627
AgentRPCRequestParams,
2728
AgentRPCResponseResult,
28-
AgentClaimMessage,
2929
NodesAuthenticateConnectionMessage,
3030
} from './agent/types';
3131
import type {
32-
NodeId,
32+
AuthenticateNetworkForwardCallback,
33+
AuthenticateNetworkReverseCallback,
3334
NodeAddress,
3435
NodeBucket,
3536
NodeBucketIndex,
3637
NodeContactAddressData,
38+
NodeId,
3739
NodeIdEncoded,
38-
AuthenticateNetworkForwardCallback,
39-
AuthenticateNetworkReverseCallback,
4040
NodeIdString,
4141
} from './types';
4242
import type NodeConnectionManager from './NodeConnectionManager';
4343
import type NodeGraph from './NodeGraph';
4444
import type { ServicePOJO } from '@matrixai/mdns';
45+
import { withF } from '@matrixai/resources';
46+
import { events as mdnsEvents, MDNS, utils as mdnsUtils } from '@matrixai/mdns';
4547
import Logger from '@matrixai/logger';
46-
import { StartStop, ready } from '@matrixai/async-init/dist/StartStop';
47-
import { Semaphore, Lock } from '@matrixai/async-locks';
48+
import { ready, StartStop } from '@matrixai/async-init/dist/StartStop';
49+
import { Lock, Semaphore } from '@matrixai/async-locks';
4850
import { IdInternal } from '@matrixai/id';
49-
import { timedCancellable, context } from '@matrixai/contexts/dist/decorators';
50-
import { withF } from '@matrixai/resources';
51-
import { MDNS, events as mdnsEvents, utils as mdnsUtils } from '@matrixai/mdns';
51+
import { context, timedCancellable } from '@matrixai/contexts/dist/decorators';
52+
import { Timer } from '@matrixai/timer';
5253
import * as nodesUtils from './utils';
5354
import * as nodesEvents from './events';
5455
import * as nodesErrors from './errors';
@@ -77,12 +78,13 @@ enum AuthenticatingState {
7778
}
7879
type AuthenticationEntry = {
7980
authenticatedForward: AuthenticatingState;
80-
reasonForward?: any;
81+
reasonForward?: Error;
8182
authenticatedReverse: AuthenticatingState;
82-
reasonReverse?: any;
83+
reasonReverse?: Error;
8384
authenticatedP: Promise<void>;
8485
authenticatedResolveP: (value: void) => void;
85-
authenticatedRejectP: (reason?: any) => void;
86+
authenticatedRejectP: (reason?: Error) => void;
87+
timeout?: Timer;
8688
};
8789

8890
const activeForwardAuthenticateCancellationReason = Symbol(
@@ -1182,10 +1184,14 @@ class NodeManager {
11821184
});
11831185
connectionsEntry.authenticatedForward = AuthenticatingState.SUCCESS;
11841186
} catch (e) {
1187+
const err = new nodesErrors.ErrorNodeManagerForwardAuthenticationFailed(
1188+
undefined,
1189+
{ cause: e },
1190+
);
11851191
connectionsEntry.authenticatedForward = AuthenticatingState.FAIL;
1186-
connectionsEntry.reasonForward = e;
1192+
connectionsEntry.reasonForward = err;
11871193
await this.authenticateFail(nodeId);
1188-
throw e;
1194+
return;
11891195
}
11901196
this.logger.warn(
11911197
`Node ${nodesUtils.encodeNodeId(nodeId)} has been forward authenticated`,
@@ -1270,9 +1276,6 @@ class NodeManager {
12701276
}
12711277
}
12721278

1273-
// TODO: There's a race condition with this and awaiting authentication. There are cases where authentication can complete before we can even start awaiting authenctication. We need to handle this better.
1274-
// It's better handled by starting the wait for authentication and then initiating it but the ordering of this sucks.
1275-
// best thing for now is to allow awaiting the authentication before even triggering the connections.
12761279
/**
12771280
* Will initiate a forward authentication call and coalesce
12781281
*/
@@ -1286,18 +1289,61 @@ class NodeManager {
12861289
resolveP: authenticatedResolveP,
12871290
rejectP: authenticatedRejectP,
12881291
} = utils.promise<void>();
1289-
// Prevent unhandled rejections
1290-
authenticatedP.then(
1291-
() => {},
1292-
() => {},
1293-
);
1292+
const timeoutDelay = 1000;
1293+
const timeout = new Timer({ delay: timeoutDelay });
1294+
// Prevent cancellation leak when cleaning up
1295+
void timeout.catch(() => {});
12941296
authenticationEntry = {
12951297
authenticatedForward: AuthenticatingState.PENDING,
12961298
authenticatedReverse: AuthenticatingState.PENDING,
12971299
authenticatedP,
12981300
authenticatedResolveP,
12991301
authenticatedRejectP,
1302+
timeout,
13001303
};
1304+
void timeout.then(
1305+
async () => {
1306+
this.logger.warn(`TIMED OUT!`);
1307+
const error = new nodesErrors.ErrorNodeManagerAuthenticatonTimedOut(
1308+
`Authentication timed out after ${timeoutDelay}ms`,
1309+
);
1310+
if (
1311+
authenticationEntry!.authenticatedForward ===
1312+
AuthenticatingState.PENDING
1313+
) {
1314+
authenticationEntry!.authenticatedForward =
1315+
AuthenticatingState.FAIL;
1316+
authenticationEntry!.reasonForward = error;
1317+
}
1318+
if (
1319+
authenticationEntry!.authenticatedReverse ===
1320+
AuthenticatingState.PENDING
1321+
) {
1322+
authenticationEntry!.authenticatedReverse =
1323+
AuthenticatingState.FAIL;
1324+
authenticationEntry!.reasonReverse = error;
1325+
}
1326+
if (
1327+
authenticationEntry!.authenticatedForward ===
1328+
AuthenticatingState.FAIL ||
1329+
authenticationEntry!.authenticatedReverse ===
1330+
AuthenticatingState.FAIL
1331+
) {
1332+
await this.authenticateFail(nodeId);
1333+
}
1334+
delete authenticationEntry!.timeout;
1335+
},
1336+
() => {},
1337+
);
1338+
const authenticationWithCleanupP = authenticatedP.finally(() => {
1339+
timeout.cancel(Error('reason'));
1340+
});
1341+
// Prevent unhandled rejections
1342+
authenticationWithCleanupP.then(
1343+
() => {},
1344+
() => {},
1345+
);
1346+
authenticationEntry.authenticatedP = authenticationWithCleanupP;
13011347
this.authenticationMap.set(nodeIdString, authenticationEntry);
13021348
}
13031349
const existingAuthenticate =
@@ -1349,10 +1395,14 @@ class NodeManager {
13491395
throw new nodesErrors.ErrorNodeConnectionManagerConnectionNotFound();
13501396
}
13511397
try {
1398+
this.logger.warn('waiting');
13521399
return await connectionsEntry.authenticatedP;
13531400
} catch (e) {
1401+
this.logger.warn('failed waiting');
13541402
Error.captureStackTrace(e);
13551403
throw e;
1404+
} finally {
1405+
this.logger.warn('done');
13561406
}
13571407
}
13581408

@@ -1370,10 +1420,13 @@ class NodeManager {
13701420
this.logger.warn('skipped');
13711421
return;
13721422
}
1373-
const rejectAuthentication = connectionsEntry.authenticatedRejectP;
1423+
const authenticatedRejectP = connectionsEntry.authenticatedRejectP;
13741424
// Trigger shutdown of the connections
13751425
await this.nodeConnectionManager.destroyConnection(nodeId, true);
1376-
let reason;
1426+
let reason: Error;
1427+
this.logger.warn(
1428+
`boi ${connectionsEntry.reasonForward}|${connectionsEntry.reasonReverse}`,
1429+
);
13771430
if (
13781431
connectionsEntry.reasonForward != null &&
13791432
connectionsEntry.reasonReverse != null
@@ -1393,7 +1446,8 @@ class NodeManager {
13931446
utils.never('No reason was provided');
13941447
}
13951448
this.logger.warn(`Authentication failed with: ${reason.toString()}`);
1396-
rejectAuthentication(
1449+
// Removing authentication entry
1450+
authenticatedRejectP(
13971451
new nodesErrors.ErrorNodeManagerAuthenticationFailed(undefined, {
13981452
cause: reason,
13991453
}),

src/nodes/errors.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ class ErrorNodeManagerAuthenticationFailed<T> extends ErrorNodeManager<T> {
4848
exitCode = sysexits.NOPERM;
4949
}
5050

51+
class ErrorNodeManagerForwardAuthenticationFailed<T> extends ErrorNodes<T> {
52+
static description = 'Failed to complete forward authentication';
53+
exitCode = sysexits.USAGE;
54+
}
55+
56+
class ErrorNodeManagerAuthenticatonTimedOut<T> extends ErrorNodes<T> {
57+
static description = 'Failed to complete authentication before timing out';
58+
exitCode = sysexits.USAGE;
59+
}
60+
5161
class ErrorNodeGraph<T> extends ErrorNodes<T> {}
5262

5363
class ErrorNodeGraphRunning<T> extends ErrorNodeGraph<T> {
@@ -245,6 +255,8 @@ export {
245255
ErrorNodeManagerSyncNodeGraphFailed,
246256
ErrorNodeManagerAuthenticationCallbackNotProvided,
247257
ErrorNodeManagerAuthenticationFailed,
258+
ErrorNodeManagerForwardAuthenticationFailed,
259+
ErrorNodeManagerAuthenticatonTimedOut,
248260
ErrorNodeGraph,
249261
ErrorNodeGraphRunning,
250262
ErrorNodeGraphNotRunning,

tests/nodes/NodeManager.test.ts

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2295,11 +2295,7 @@ describe(`${NodeManager.name}`, () => {
22952295
);
22962296
await expect(reverseAuthenticateP).toResolve();
22972297
});
2298-
// FIXME: the forward trigger fails so the reverse never starts. We need a timeout here.
2299-
// TODO: Forward authenticate callback throwing and error will not be a fully supported error path but
2300-
// still needs to be handled.
2301-
// TODO: We need authentication triggered and cleaned up by connection life cycle.
2302-
// TODO: we need to garbage collect authentication state
2298+
// TODO: use the connection destroyed event to clean up auth state.
23032299
test('forward authenticate fails on local', async () => {
23042300
nodeManagerLocal.setAuthenticateNetworkForwardCallback(async () => {
23052301
throw Error('Failure to generate forward authentication message');
@@ -2407,24 +2403,29 @@ describe(`${NodeManager.name}`, () => {
24072403
nodeConnectionManagerPeer.port,
24082404
);
24092405

2406+
logger.warn('step1');
24102407
const authenticationAttemptP = nodeManagerLocal.withConnF(
24112408
nodeIdPeer,
24122409
async () => {
24132410
// Do nothing
24142411
},
24152412
);
2413+
logger.warn('step2');
24162414
await expect(authenticationAttemptP).rejects.toThrow(
24172415
nodesErrors.ErrorNodeManagerAuthenticationFailed,
24182416
);
2417+
logger.warn('step3');
24192418
const forwardAuthenticateP = nodeManagerLocal.withConnF(
24202419
nodeIdPeer,
24212420
async () => {
24222421
// Do nothing
24232422
},
24242423
);
2424+
logger.warn('step4');
24252425
await expect(forwardAuthenticateP).rejects.toThrow(
24262426
nodesErrors.ErrorNodeManagerAuthenticationFailed,
24272427
);
2428+
logger.warn('step5');
24282429
const reverseAuthenticateP = nodeManagerPeer.withConnF(
24292430
nodeIdLocal,
24302431
async () => {
@@ -2434,10 +2435,67 @@ describe(`${NodeManager.name}`, () => {
24342435
await expect(reverseAuthenticateP).rejects.toThrow(
24352436
nodesErrors.ErrorNodeManagerAuthenticationFailed,
24362437
);
2438+
logger.warn('DONE');
24372439
});
24382440
test.todo('reverse authenticate fails on local');
24392441
test.todo('reverse authenticate fails on peer');
24402442
test.todo('non whitelisted RPC calls are prevented');
2443+
test.todo('connections that do not initiate authentication are handled');
2444+
test('can fail authentication with timeout', async () => {
2445+
const { p: waitP } = utils.promise();
2446+
nodeManagerLocal.setAuthenticateNetworkForwardCallback(async () => {
2447+
const message: NodesAuthenticateConnectionMessageBasicPublic = {
2448+
type: 'NodesAuthenticateConnectionMessageBasicPublic',
2449+
networkId: 'hello',
2450+
};
2451+
return message;
2452+
});
2453+
nodeManagerPeer.setAuthenticateNetworkForwardCallback(async () => {
2454+
const message: NodesAuthenticateConnectionMessageBasicPublic = {
2455+
type: 'NodesAuthenticateConnectionMessageBasicPublic',
2456+
networkId: 'hello',
2457+
};
2458+
return message;
2459+
});
2460+
nodeManagerLocal.setAuthenticateNetworkReverseCallback(
2461+
async (message) => {
2462+
if (
2463+
message.type !== 'NodesAuthenticateConnectionMessageBasicPublic'
2464+
) {
2465+
throw Error('must be basic message');
2466+
}
2467+
if (message.networkId !== 'hello') {
2468+
throw Error('network must be "hello"');
2469+
}
2470+
},
2471+
);
2472+
nodeManagerPeer.setAuthenticateNetworkReverseCallback(async (message) => {
2473+
await waitP;
2474+
if (message.type !== 'NodesAuthenticateConnectionMessageBasicPublic') {
2475+
throw Error('must be basic message');
2476+
}
2477+
if (message.networkId !== 'hello') {
2478+
throw Error('network must be "hello"');
2479+
}
2480+
});
2481+
2482+
// Creating first connection to 0;
2483+
await nodeConnectionManagerLocal.createConnection(
2484+
[nodeIdPeer],
2485+
localHost,
2486+
nodeConnectionManagerPeer.port,
2487+
);
2488+
2489+
const forwardAuthenticateP = nodeManagerLocal.withConnF(
2490+
nodeIdPeer,
2491+
async () => {
2492+
// Do nothing
2493+
},
2494+
);
2495+
await expect(forwardAuthenticateP).rejects.toThrow(
2496+
nodesErrors.ErrorNodeManagerAuthenticationFailed,
2497+
);
2498+
});
24412499
test.todo('');
24422500
});
24432501
});

0 commit comments

Comments
 (0)