diff --git a/package-lock.json b/package-lock.json index 7dc81821b..3c2060664 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "polykey", - "version": "1.2.1-alpha.14", + "version": "1.2.1-alpha.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "polykey", - "version": "1.2.1-alpha.14", + "version": "1.2.1-alpha.16", "license": "GPL-3.0", "dependencies": { "@matrixai/async-cancellable": "^1.1.1", diff --git a/package.json b/package.json index 4b72603ce..35afcb55d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "polykey", - "version": "1.2.1-alpha.14", + "version": "1.2.1-alpha.16", "homepage": "https://polykey.com", "author": "Matrix AI", "contributors": [ diff --git a/src/PolykeyAgent.ts b/src/PolykeyAgent.ts index 60635cbd1..8038e48d3 100644 --- a/src/PolykeyAgent.ts +++ b/src/PolykeyAgent.ts @@ -1,8 +1,8 @@ -import type { DeepPartial, FileSystem } from './types'; +import type { DeepPartial, FileSystem, ObjectEmpty } from './types'; import type { PolykeyWorkerManagerInterface } from './workers/types'; import type { TLSConfig } from './network/types'; -import type { SeedNodes, NodesOptions } from './nodes/types'; -import type { Key, KeysOptions } from './keys/types'; +import type { SeedNodes } from './nodes/types'; +import type { Key, PasswordOpsLimit, PasswordMemLimit } from './keys/types'; import path from 'path'; import process from 'process'; import Logger from '@matrixai/logger'; @@ -54,26 +54,35 @@ type PolykeyAgentOptions = { seedNodes: SeedNodes; workers: number; ipv6Only: boolean; - keys: KeysOptions; - rpc: { - callTimeoutTime: number; - parserBufferSize: number; - }; + keys: { + passwordOpsLimit: PasswordOpsLimit; + passwordMemLimit: PasswordMemLimit; + strictMemoryLock: boolean; + certDuration: number; + certRenewLeadTime: number; + recoveryCode: string; + } & ( + | ObjectEmpty + | { recoveryCode: string } + | { privateKey: Buffer } + | { privateKeyPath: string } + ); client: { - connectTimeoutTime: number; keepAliveTimeoutTime: number; keepAliveIntervalTime: number; + rpcCallTimeoutTime: number; + rpcParserBufferSize: number; + }; + nodes: { + connectionIdleTimeoutTime: number; + connectionFindConcurrencyLimit: number; + connectionConnectTimeoutTime: number; + connectionKeepAliveTimeoutTime: number; + connectionKeepAliveIntervalTime: number; + connectionHolePunchIntervalTime: number; + rpcCallTimeoutTime: number; + rpcParserBufferSize: number; }; - nodes: NodesOptions; -}; - -type PolykeyAgentStartOptions = { - clientServiceHost: string; - clientServicePort: number; - agentServiceHost: string; - agentServicePort: number; - ipv6Only: boolean; - workers: number; }; interface PolykeyAgent extends CreateDestroyStartStop {} @@ -113,9 +122,9 @@ class PolykeyAgent { }: { password: string; options?: DeepPartial; + fresh?: boolean; fs?: FileSystem; logger?: Logger; - fresh?: boolean; }): Promise { logger.info(`Creating ${this.name}`); const umask = 0o077; @@ -131,18 +140,18 @@ class PolykeyAgent { workers: config.defaultsUser.workers, ipv6Only: config.defaultsUser.ipv6Only, keys: { + passwordOpsLimit: config.defaultsUser.passwordOpsLimit, + passwordMemLimit: config.defaultsUser.passwordMemLimit, + strictMemoryLock: config.defaultsUser.strictMemoryLock, certDuration: config.defaultsUser.certDuration, certRenewLeadTime: config.defaultsUser.certRenewLeadTime, }, - rpc: { - callTimeoutTime: config.defaultsSystem.rpcCallTimeoutTime, - parserBufferSize: config.defaultsSystem.rpcParserBufferSize, - }, client: { - connectTimoutTime: config.defaultsSystem.clientConnectTimeoutTime, keepAliveTimeoutTime: config.defaultsSystem.clientKeepAliveTimeoutTime, keepAliveIntervalTime: config.defaultsSystem.clientKeepAliveIntervalTime, + rpcCallTimeoutTime: config.defaultsSystem.rpcCallTimeoutTime, + rpcParserBufferSize: config.defaultsSystem.rpcParserBufferSize, }, nodes: { connectionIdleTimeoutTime: @@ -181,7 +190,6 @@ class PolykeyAgent { const dbPath = path.join(statePath, config.paths.dbBase); const keysPath = path.join(statePath, config.paths.keysBase); const vaultsPath = path.join(statePath, config.paths.vaultsBase); - let status: Status | undefined; let schema: Schema | undefined; let keyRing: KeyRing | undefined; @@ -217,11 +225,16 @@ class PolykeyAgent { }); keyRing = await KeyRing.createKeyRing({ keysPath, - options: optionsDefaulted.keys, fs, fresh, password, logger: logger.getChild(KeyRing.name), + passwordMemLimit: optionsDefaulted.keys.passwordMemLimit, + passwordOpsLimit: optionsDefaulted.keys.passwordOpsLimit, + privateKey: optionsDefaulted.keys.privateKey, + privateKeyPath: optionsDefaulted.keys.privateKeyPath, + recoveryCode: optionsDefaulted.keys.recoveryCode, + strictMemoryLock: optionsDefaulted.keys.strictMemoryLock, }); db = await DB.createDB({ dbPath, @@ -256,7 +269,8 @@ class PolykeyAgent { db, keyRing, taskManager, - options: optionsDefaulted.keys, + certDuration: optionsDefaulted.keys.certDuration, + certRenewLeadTime: optionsDefaulted.keys.certRenewLeadTime, logger: logger.getChild(CertManager.name), fresh, }); @@ -310,7 +324,20 @@ class PolykeyAgent { nodeGraph, tlsConfig, seedNodes: optionsDefaulted.seedNodes, - options: optionsDefaulted.nodes, + connectionFindConcurrencyLimit: + optionsDefaulted.nodes.connectionFindConcurrencyLimit, + connectionIdleTimeoutTime: + optionsDefaulted.nodes.connectionIdleTimeoutTime, + connectionConnectTimeoutTime: + optionsDefaulted.nodes.connectionConnectTimeoutTime, + connectionKeepAliveTimeoutTime: + optionsDefaulted.nodes.connectionKeepAliveTimeoutTime, + connectionKeepAliveIntervalTime: + optionsDefaulted.nodes.connectionKeepAliveIntervalTime, + connectionHolePunchIntervalTime: + optionsDefaulted.nodes.connectionHolePunchIntervalTime, + rpcParserBufferSize: optionsDefaulted.nodes.rpcParserBufferSize, + rpcCallTimeoutTime: optionsDefaulted.nodes.rpcCallTimeoutTime, logger: logger.getChild(NodeConnectionManager.name), }); nodeManager = new NodeManager({ @@ -387,8 +414,8 @@ class PolykeyAgent { ), keepAliveTimeoutTime: optionsDefaulted.client.keepAliveTimeoutTime, keepAliveIntervalTime: optionsDefaulted.client.keepAliveIntervalTime, - rpcCallTimeoutTime: optionsDefaulted.rpc.callTimeoutTime, - rpcParserBufferSize: optionsDefaulted.rpc.parserBufferSize, + rpcCallTimeoutTime: optionsDefaulted.client.rpcCallTimeoutTime, + rpcParserBufferSize: optionsDefaulted.client.rpcParserBufferSize, logger: logger.getChild(ClientService.name), }); } catch (e) { @@ -469,6 +496,7 @@ class PolykeyAgent { public readonly fs: FileSystem; public readonly logger: Logger; public readonly clientService: ClientService; + public readonly privateKeyPath: string; protected workerManager: PolykeyWorkerManagerInterface | undefined; protected handleEventCertManagerCertChange = async ( @@ -585,7 +613,19 @@ class PolykeyAgent { fresh = false, }: { password: string; - options?: Partial; + options?: DeepPartial<{ + clientServiceHost: string; + clientServicePort: number; + agentServiceHost: string; + agentServicePort: number; + ipv6Only: boolean; + workers: number; + keys: + | ObjectEmpty + | { recoveryCode: string } + | { privateKey: Buffer } + | { privateKeyPath: string }; + }>; workers?: number; fresh?: boolean; }) { @@ -610,6 +650,9 @@ class PolykeyAgent { await this.keyRing.start({ password, fresh, + recoveryCode: optionsDefaulted.keys?.recoveryCode, + privateKey: optionsDefaulted.keys?.privateKey, + privateKeyPath: optionsDefaulted.keys?.privateKeyPath, }); await this.db.start({ crypto: { diff --git a/src/PolykeyClient.ts b/src/PolykeyClient.ts index 571f43b31..8d0cf0882 100644 --- a/src/PolykeyClient.ts +++ b/src/PolykeyClient.ts @@ -1,24 +1,39 @@ -import type { FileSystem } from './types'; -import type { StreamFactory } from '@matrixai/rpc'; +import type { PromiseCancellable } from '@matrixai/async-cancellable'; +import type { ContextTimed, ContextTimedInput } from '@matrixai/contexts'; +import type { DeepPartial, FileSystem } from './types'; +import type { NodeId } from './ids/types'; import path from 'path'; import Logger from '@matrixai/logger'; -import { CreateDestroyStartStop } from '@matrixai/async-init/dist/CreateDestroyStartStop'; -import { RPCClient } from '@matrixai/rpc'; -import { middleware as rpcMiddleware } from '@matrixai/rpc'; -import * as clientMiddleware from './client/middleware'; +import { + CreateDestroyStartStop, + ready, +} from '@matrixai/async-init/dist/CreateDestroyStartStop'; +import { timedCancellable, context } from '@matrixai/contexts/dist/decorators'; +import { WebSocketClient } from '@matrixai/ws'; +import { RPCClient, middleware as rpcMiddleware } from '@matrixai/rpc'; import { Session } from './sessions'; +import * as ids from './ids'; import * as utils from './utils'; import * as errors from './errors'; import * as events from './events'; -import config from './config'; import * as networkUtils from './network/utils'; +import * as validationErrors from './validation/errors'; +import * as clientUtils from './client/utils'; +import * as clientMiddleware from './client/middleware'; import clientClientManifest from './client/callers'; +import config from './config'; /** - * This PolykeyClient would create a new PolykeyClient object that constructs - * a new RPCClient which attempts to connect to an existing PolykeyAgent's - * RPC server. + * Optional configuration for`PolykeyClient`. */ +type PolykeyClientOptions = { + nodePath: string; + keepAliveTimeoutTime: number; + keepAliveIntervalTime: number; + rpcCallTimeoutTime: number; + rpcParserBufferSize: number; +}; + interface PolykeyClient extends CreateDestroyStartStop {} @CreateDestroyStartStop( new errors.ErrorPolykeyClientRunning(), @@ -33,48 +48,127 @@ interface PolykeyClient extends CreateDestroyStartStop {} }, ) class PolykeyClient { - static async createPolykeyClient({ - nodePath = config.defaultsUser.nodePath, - streamFactory, - streamKeepAliveTimeoutTime, - parserBufferByteLimit, - fs = require('fs'), - logger = new Logger(this.name), - fresh = false, - }: { - nodePath?: string; - streamFactory: StreamFactory; - streamKeepAliveTimeoutTime?: number; - parserBufferByteLimit?: number; - fs?: FileSystem; - logger?: Logger; - fresh?: boolean; - }): Promise { + /** + * Creates a Polykey Client. + * + * @param opts + * @param opts.nodeId + * @param opts.host + * @param opts.port + * @param ctx + */ + public static createPolykeyClient( + opts: { + nodeId: string | NodeId; + port: number; + host: string; + options?: DeepPartial; + fresh?: boolean; + fs?: FileSystem; + logger?: Logger; + }, + ctx?: Partial, + ): PromiseCancellable; + @timedCancellable( + true, + config.defaultsSystem.clientConnectTimeoutTime, + errors.ErrorPolykeyClientCreateTimeout, + ) + public static async createPolykeyClient( + { + // Required parameters + nodeId, + host, + port, + // Options + options = {}, + fresh = false, + // Optional dependencies + fs = require('fs'), + logger = new Logger(this.name), + }: { + nodeId: string | NodeId; + host: string; + port: number; + options?: DeepPartial; + fresh?: boolean; + fs?: FileSystem; + logger?: Logger; + }, + @context ctx: ContextTimed, + ): Promise { logger.info(`Creating ${this.name}`); - if (nodePath == null) { + let nodeId_: NodeId; + if (typeof nodeId === 'string') { + try { + nodeId_ = ids.parseNodeId(nodeId); + } catch (e) { + if (e instanceof validationErrors.ErrorParse) { + throw new errors.ErrorPolykeyClientNodeIdInvalid( + 'Encoded node ID must be a multibase base32hex encoded public-key', + { + cause: e, + data: { nodeId }, + }, + ); + } + throw e; + } + } else { + nodeId_ = nodeId; + } + const optionsDefaulted = utils.mergeObjects(options, { + nodePath: config.defaultsUser.nodePath, + connectTimeoutTime: config.defaultsSystem.clientConnectTimeoutTime, + keepAliveTimeoutTime: config.defaultsSystem.clientKeepAliveTimeoutTime, + keepAliveIntervalTime: config.defaultsSystem.clientKeepAliveIntervalTime, + callTimeoutTime: config.defaultsSystem.rpcCallTimeoutTime, + parserBufferSize: config.defaultsSystem.rpcParserBufferSize, + }) as PolykeyClientOptions; + if (optionsDefaulted.nodePath == null) { throw new errors.ErrorUtilsNodePath(); } - await utils.mkdirExists(fs, nodePath); - const sessionTokenPath = path.join(nodePath, config.paths.tokenBase); + await utils.mkdirExists(fs, optionsDefaulted.nodePath); + const sessionTokenPath = path.join( + optionsDefaulted.nodePath, + config.paths.tokenBase, + ); const session = await Session.createSession({ sessionTokenPath, logger: logger.getChild(Session.name), fresh, }); - const rpcClientClient = new RPCClient({ + const webSocketClient = await WebSocketClient.createWebSocketClient( + { + host, + port, + config: { + verifyPeer: true, + verifyCallback: async (certs) => { + await clientUtils.verifyServerCertificateChain([nodeId_], certs); + }, + keepAliveTimeoutTime: optionsDefaulted.keepAliveTimeoutTime, + keepAliveIntervalTime: optionsDefaulted.keepAliveIntervalTime, + }, + logger: logger.getChild(WebSocketClient.name), + }, + ctx, + ); + const rpcClient = new RPCClient({ manifest: clientClientManifest, - streamFactory, + streamFactory: () => webSocketClient.connection.newStream(), middlewareFactory: rpcMiddleware.defaultClientMiddlewareWrapper( clientMiddleware.middlewareClient(session), - parserBufferByteLimit, + optionsDefaulted.rpcParserBufferSize, ), toError: networkUtils.toError, - streamKeepAliveTimeoutTime, + streamKeepAliveTimeoutTime: optionsDefaulted.rpcCallTimeoutTime, logger: logger.getChild(RPCClient.name), }); const pkClient = new this({ - nodePath, - rpcClientClient: rpcClientClient, + nodePath: optionsDefaulted.nodePath, + webSocketClient, + rpcClient, session, fs, logger, @@ -86,31 +180,55 @@ class PolykeyClient { public readonly nodePath: string; public readonly session: Session; - public readonly rpcClientClient: RPCClient; + public readonly webSocketClient: WebSocketClient; + public readonly rpcClient: RPCClient; protected fs: FileSystem; protected logger: Logger; constructor({ nodePath, - rpcClientClient, + webSocketClient, + rpcClient, session, fs, logger, }: { nodePath: string; - rpcClientClient: RPCClient; + webSocketClient: WebSocketClient; + rpcClient: RPCClient; session: Session; fs: FileSystem; logger: Logger; }) { this.logger = logger; this.nodePath = nodePath; + this.webSocketClient = webSocketClient; + this.rpcClient = rpcClient; this.session = session; - this.rpcClientClient = rpcClientClient; this.fs = fs; } + @ready(new errors.ErrorPolykeyClientNotRunning()) + public get host() { + return this.webSocketClient.connection.remoteHost; + } + + @ready(new errors.ErrorPolykeyClientNotRunning()) + public get port() { + return this.webSocketClient.connection.remotePort; + } + + @ready(new errors.ErrorPolykeyClientNotRunning()) + public get localHost() { + return this.webSocketClient.connection.localHost; + } + + @ready(new errors.ErrorPolykeyClientNotRunning()) + public get localPort() { + return this.webSocketClient.connection.localPort; + } + public async start(): Promise { this.logger.info(`Starting ${this.constructor.name}`); this.logger.info(`Started ${this.constructor.name}`); @@ -122,11 +240,14 @@ class PolykeyClient { this.logger.info(`Stopped ${this.constructor.name}`); } - public async destroy() { + public async destroy({ force = false }: { force?: boolean }) { this.logger.info(`Destroying ${this.constructor.name}`); + await this.webSocketClient.destroy({ force }); await this.session.destroy(); this.logger.info(`Destroyed ${this.constructor.name}`); } } export default PolykeyClient; + +export type { PolykeyClientOptions }; diff --git a/src/bootstrap/utils.ts b/src/bootstrap/utils.ts index 2dc9f1243..23643dcd5 100644 --- a/src/bootstrap/utils.ts +++ b/src/bootstrap/utils.ts @@ -1,5 +1,11 @@ -import type { DeepPartial, FileSystem } from '../types'; -import type { RecoveryCode, Key, KeysOptions } from '../keys/types'; +import type { FileSystem } from '../types'; +import type { + RecoveryCode, + Key, + PrivateKey, + PasswordOpsLimit, + PasswordMemLimit, +} from '../keys/types'; import path from 'path'; import Logger from '@matrixai/logger'; import { DB } from '@matrixai/db'; @@ -23,26 +29,34 @@ import config from '../config'; import * as utils from '../utils'; import * as errors from '../errors'; -type BootstrapOptions = { - nodePath: string; - keys: KeysOptions; -}; - /** * Bootstraps the Node Path */ async function bootstrapState({ // Required parameters password, - // Optional configuration - options = {}, + nodePath = config.defaultsUser.nodePath, + recoveryCode, + privateKey, + privateKeyPath, + passwordOpsLimit, + passwordMemLimit, + strictMemoryLock = false, + certDuration = config.defaultsUser.certDuration, fresh = false, // Optional dependencies fs = require('fs'), logger = new Logger(bootstrapState.name), }: { password: string; - options?: DeepPartial; + nodePath?: string; + recoveryCode?: RecoveryCode; + privateKey?: PrivateKey; + privateKeyPath?: string; + passwordOpsLimit?: PasswordOpsLimit; + passwordMemLimit?: PasswordMemLimit; + strictMemoryLock?: boolean; + certDuration?: number; fresh?: boolean; fs?: FileSystem; logger?: Logger; @@ -50,30 +64,15 @@ async function bootstrapState({ const umask = 0o077; logger.info(`Setting umask to ${umask.toString(8).padStart(3, '0')}`); process.umask(umask); - const optionsDefaulted = utils.mergeObjects(options, { - nodePath: config.defaultsUser.nodePath, - keys: { - certDuration: config.defaultsUser.certDuration, - }, - }); - logger.info(`Setting node path to ${optionsDefaulted.nodePath}`); - if (optionsDefaulted.nodePath == null) { + logger.info(`Setting node path to ${nodePath}`); + if (nodePath == null) { throw new errors.ErrorUtilsNodePath(); } - await utils.mkdirExists(fs, optionsDefaulted.nodePath); + await utils.mkdirExists(fs, nodePath); // Setup node path and sub paths - const statusPath = path.join( - optionsDefaulted.nodePath, - config.paths.statusBase, - ); - const statusLockPath = path.join( - optionsDefaulted.nodePath, - config.paths.statusLockBase, - ); - const statePath = path.join( - optionsDefaulted.nodePath, - config.paths.stateBase, - ); + const statusPath = path.join(nodePath, config.paths.statusBase); + const statusLockPath = path.join(nodePath, config.paths.statusLockBase); + const statePath = path.join(nodePath, config.paths.stateBase); const dbPath = path.join(statePath, config.paths.dbBase); const keysPath = path.join(statePath, config.paths.keysBase); const vaultsPath = path.join(statePath, config.paths.vaultsBase); @@ -87,7 +86,7 @@ async function bootstrapState({ await status.start({ pid: process.pid }); if (!fresh) { // Check the if number of directory entries is greater than 1 due to status.json and status.lock - if ((await fs.promises.readdir(optionsDefaulted.nodePath)).length > 2) { + if ((await fs.promises.readdir(nodePath)).length > 2) { throw new bootstrapErrors.ErrorBootstrapExistingState(); } } @@ -104,7 +103,12 @@ async function bootstrapState({ const keyRing = await KeyRing.createKeyRing({ keysPath, password, - options: optionsDefaulted.keys, + recoveryCode, + privateKey, + privateKeyPath, + passwordOpsLimit, + passwordMemLimit, + strictMemoryLock, fs, logger: logger.getChild(KeyRing.name), fresh, @@ -141,7 +145,7 @@ async function bootstrapState({ keyRing, db, taskManager, - options: optionsDefaulted.keys, + certDuration, fresh, logger, }); @@ -235,5 +239,3 @@ async function bootstrapState({ } export { bootstrapState }; - -export type { BootstrapOptions }; diff --git a/src/config.ts b/src/config.ts index 97ddd2f29..0c4375c21 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,5 +1,6 @@ -import type { Host, Port } from './network/types'; +import type { PasswordMemLimit, PasswordOpsLimit } from './keys/types'; import type { NodeAddress } from './nodes/types'; +import type { Host, Port } from './network/types'; import { getDefaultNodePath } from './utils'; // @ts-ignore package.json is outside rootDir import { version } from '../package.json'; @@ -253,6 +254,33 @@ const config = { */ defaultsUser: { nodePath: getDefaultNodePath(), + /** + * Ops limit for password hashing. + * + * This is the moderate choice: 0.7 seconds on 2.8 Ghz Intel Core i7. + * This can only be set when a new password is set. + * If this changes, the password hash for the same password will change. + */ + passwordOpsLimit: 3 as PasswordOpsLimit, + /** + * Memory limit for password hashing. + * + * This is the moderate choice: requiring at least 512 MiB of memory. + * This can only be set when a new password is set. + * If this changes, the password hash for the same password will change. + */ + passwordMemLimit: 268435456 as PasswordMemLimit, + /** + * Locking sensitive memory from being swapped to disk. + * + * Locks the memory used for keys and password hashes to prevent swapping. + * On some systems, this can also prevent the memory being included during + * core dumps. + * + * This should be disabled during testing, as only a limited amount of + * memory is allowed to be locked. + */ + strictMemoryLock: true, certDuration: 31536000, certRenewLeadTime: 86400, /** diff --git a/src/errors.ts b/src/errors.ts index 31d5160eb..2501fc5d7 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -41,6 +41,16 @@ class ErrorPolykeyClientDestroyed extends ErrorPolykey { exitCode = sysexits.USAGE; } +class ErrorPolykeyClientCreateTimeout extends ErrorPolykey { + static description = 'PolykeyClient create timeout'; + exitCode = sysexits.UNAVAILABLE; +} + +class ErrorPolykeyClientNodeIdInvalid extends ErrorPolykey { + static description = 'PolykeyClient failed parsing encoded node ID'; + exitCode = sysexits.USAGE; +} + export { sysexits, ErrorPolykey, @@ -52,6 +62,8 @@ export { ErrorPolykeyClientRunning, ErrorPolykeyClientNotRunning, ErrorPolykeyClientDestroyed, + ErrorPolykeyClientCreateTimeout, + ErrorPolykeyClientNodeIdInvalid, }; /** diff --git a/src/identities/providers/github/GitHubProvider.ts b/src/identities/providers/github/GitHubProvider.ts index 9c2098c71..d3e413069 100644 --- a/src/identities/providers/github/GitHubProvider.ts +++ b/src/identities/providers/github/GitHubProvider.ts @@ -280,102 +280,120 @@ class GitHubProvider extends Provider { ); } providerToken = await this.checkToken(providerToken, authIdentityId); - let pageNum = 1; - while (true) { - const request = this.createRequest( - `${this.apiUrl}/user/following?per_page=100&page=${pageNum}`, - { - method: 'GET', - }, - providerToken, - ); - const response = await fetch(request); - if (!response.ok) { - if (response.status === 401) { - throw new identitiesErrors.ErrorProviderUnauthenticated( - `Invalid access token`, + const foundIdentityIds: Set = new Set(); + for (const identityGroup of ['following', 'followers'] as const) { + let cursor: string | undefined; + while (true) { + const request = this.createRequest( + `${this.apiUrl}/graphql`, + { + method: 'POST', + body: this.getConnectedIdentityDatasGraphQLBody( + authIdentityId, + identityGroup, + cursor, + ), + }, + providerToken, + ); + const response = await fetch(request); + if (!response.ok) { + if (response.status === 401) { + throw new identitiesErrors.ErrorProviderUnauthenticated( + `Invalid access token`, + ); + } + throw new identitiesErrors.ErrorProviderCall( + `Provider responded with ${response.status} ${response.statusText}`, ); } - throw new identitiesErrors.ErrorProviderCall( - `Provider responded with ${response.status} ${response.statusText}`, - ); - } - let data; - try { - data = await response.json(); - } catch (e) { - throw new identitiesErrors.ErrorProviderCall( - `Provider response body is not valid JSON`, - { cause: e }, - ); - } - for (const item of data) { - const identityData = await this.getIdentityData( - authIdentityId, - item.login, - ); - if ( - identityData && - identitiesUtils.matchIdentityData(identityData, searchTerms) - ) { - yield identityData; + let data; + try { + data = await response.json(); + } catch (e) { + throw new identitiesErrors.ErrorProviderCall( + `Provider response body is not valid JSON`, + { cause: e }, + ); } - } - if (data.length === 0) { - break; - } else { - pageNum = pageNum + 1; - } - } - pageNum = 1; - while (true) { - const request = this.createRequest( - `${this.apiUrl}/user/followers?per_page=100&page=${pageNum}`, - { - method: 'GET', - }, - providerToken, - ); - const response = await fetch(request); - if (!response.ok) { - if (response.status === 401) { - throw new identitiesErrors.ErrorProviderUnauthenticated( - `Invalid access token`, + const error = data?.errors?.at?.(0); + if (error != null) { + throw new identitiesErrors.ErrorProviderCall( + `Provider response body contains an error: ${error.message}`, + { + data: error, + }, ); } - throw new identitiesErrors.ErrorProviderCall( - `Provider responded with ${response.status} ${response.statusText}`, - ); - } - let data; - try { - data = await response.json(); - } catch (e) { - throw new identitiesErrors.ErrorProviderCall( - `Provider response body is not valid JSON`, - { cause: e }, - ); - } - for (const item of data) { - const identityData = await this.getIdentityData( - authIdentityId, - item.login, - ); - if ( - identityData && - identitiesUtils.matchIdentityData(identityData, searchTerms) - ) { - yield identityData; + // FollowerConnection and FollowingConnection always exists on User + const foundIdentityGroupData = data.data.user[identityGroup]; + // Array always exists on FollowerConnection and FollowingConnection + const foundIdentityData: IdentityData[] = foundIdentityGroupData.nodes; + for (const identityData of foundIdentityData) { + identityData.providerId = this.id; + if ( + !foundIdentityIds.has(identityData.identityId) && + identitiesUtils.matchIdentityData(identityData, searchTerms) + ) { + foundIdentityIds.add(identityData.identityId); + yield identityData; + } + } + if (foundIdentityData.length === 0) { + break; + } else { + // EndCursor may be nullish if this is the last page + const endCursor: string | null = + foundIdentityGroupData.pageInfo.endCursor; + if (endCursor == null) break; + cursor = endCursor; } - } - if (data.length === 0) { - break; - } else { - pageNum = pageNum + 1; } } } + /** + * Returns a string suitable for use as the request body to the GitHub GraphQL endpoint. + * This is used to construct a query that returns either the `followers` or the `following` of a user. + * + * Schemas Used: + * - https://docs.github.com/en/graphql/reference/queries#user + * - https://docs.github.com/en/graphql/reference/objects#user + * - https://docs.github.com/en/graphql/reference/objects#followerconnection + * + * @param authIdentityId - The GitHub authentication token to use when getting user data + * @param identityGroup - Specify whether the GraphQL query requests the `followers` or the `following` of a user + * @param cursor - cursor for pagination, + * this can be retrieved from `.data.user[identityGroup].pageinfo.endCursor` + * of the JSON body on a response for a request made with the return value of this method as the body. + */ + protected getConnectedIdentityDatasGraphQLBody( + authIdentityId: IdentityId, + identityGroup: 'following' | 'followers', + cursor?: string, + ): string { + const query = `query { + user(login: "${authIdentityId}") { + ${identityGroup}(first: 100${ + cursor == null ? '' : `, after: "${cursor}"` + }) { + nodes { + identityId: login + name + email + url + } + pageInfo { + endCursor + startCursor + } + totalCount + } + } + }`; + return JSON.stringify({ query }); + } + /** * Publish an identity claim. * These are published as gists. diff --git a/src/ids/index.ts b/src/ids/index.ts index 921a5cccd..be3a96743 100644 --- a/src/ids/index.ts +++ b/src/ids/index.ts @@ -43,14 +43,37 @@ function createNodeIdGenerator(): () => NodeId { }; } -function parseNodeId(data: any): NodeId { - data = decodeNodeId(data); - if (data == null) { +function isNodeId(nodeId: any): nodeId is NodeId { + if (!(nodeId instanceof IdInternal)) { + return false; + } + if (nodeId.length !== 32) { + return false; + } + return true; +} + +function assertNodeId(nodeId: unknown): asserts nodeId is NodeId { + if (!(nodeId instanceof IdInternal)) { + throw new validationErrors.ErrorParse('must be instance of Id'); + } + if (nodeId.length !== 32) { + throw new validationErrors.ErrorParse('must be 32 bytes long'); + } +} + +function generateNodeId(nodeId: NodeId): NodeIdEncoded { + return encodeNodeId(nodeId); +} + +function parseNodeId(nodeIdEncoded: unknown): NodeId { + const nodeId = decodeNodeId(nodeIdEncoded); + if (nodeId == null) { throw new validationErrors.ErrorParse( 'Node ID must be multibase base32hex encoded public-keys', ); } - return data; + return nodeId; } /** @@ -315,6 +338,21 @@ function parseGestaltId(data: any): GestaltId { return ['identity', [providerId, identityId]]; } +function parseGestaltIdentityId(data: any): ['identity', ProviderIdentityId] { + if (typeof data !== 'string') { + throw new validationErrors.ErrorParse('Gestalt identity ID must be string'); + } + const match = (data as string).match(/^(.+):(.+)$/); + if (match == null) { + throw new validationErrors.ErrorParse( + 'Gestalt identity ID must be `Provider ID:Identity ID`', + ); + } + const providerId = parseProviderId(match[1]); + const identityId = parseIdentityId(match[2]); + return ['identity', [providerId, identityId]]; +} + function encodeGestaltId(gestaltId: GestaltId): GestaltIdEncoded { switch (gestaltId[0]) { case 'node': @@ -426,6 +464,9 @@ function decodeNotificationId( export { createPermIdGenerator, createNodeIdGenerator, + isNodeId, + assertNodeId, + generateNodeId, parseNodeId, encodeNodeId, decodeNodeId, @@ -450,6 +491,7 @@ export { encodeProviderIdentityId, decodeProviderIdentityId, parseGestaltId, + parseGestaltIdentityId, encodeGestaltId, encodeGestaltNodeId, encodeGestaltIdentityId, diff --git a/src/keys/CertManager.ts b/src/keys/CertManager.ts index abf457bcb..eb28f2f98 100644 --- a/src/keys/CertManager.ts +++ b/src/keys/CertManager.ts @@ -8,7 +8,6 @@ import type { KeyPair, RecoveryCode, CertificatePEMChain, - CertManagerOptions, } from './types'; import type KeyRing from './KeyRing'; import type TaskManager from '../tasks/TaskManager'; @@ -26,7 +25,6 @@ import * as keysUtils from './utils'; import * as keysErrors from './errors'; import * as keysEvents from './events'; import * as ids from '../ids'; -import * as utils from '../utils/utils'; import config from '../config'; /** @@ -68,7 +66,8 @@ class CertManager { db, keyRing, taskManager, - options = {}, + certDuration = config.defaultsUser.certDuration, + certRenewLeadTime = config.defaultsUser.certRenewLeadTime, workerManager, logger = new Logger(this.name), subjectAttrsExtra, @@ -79,7 +78,8 @@ class CertManager { db: DB; keyRing: KeyRing; taskManager: TaskManager; - options?: Partial; + certDuration?: number; + certRenewLeadTime?: number; workerManager?: PolykeyWorkerManagerInterface; logger?: Logger; subjectAttrsExtra?: Array<{ [key: string]: Array }>; @@ -89,15 +89,12 @@ class CertManager { fresh?: boolean; }): Promise { logger.info(`Creating ${this.name}`); - const optionsDefaulted = utils.mergeObjects(options, { - certDuration: config.defaultsUser.certDuration, - certRenewLeadTime: config.defaultsUser.certRenewLeadTime, - }) as CertManagerOptions; const certManager = new this({ db, keyRing, taskManager, - options: optionsDefaulted, + certDuration, + certRenewLeadTime, workerManager, logger, }); @@ -153,14 +150,16 @@ class CertManager { db, keyRing, taskManager, - options, + certDuration, + certRenewLeadTime, workerManager, logger, }: { db: DB; keyRing: KeyRing; taskManager: TaskManager; - options: CertManagerOptions; + certDuration: number; + certRenewLeadTime: number; workerManager?: PolykeyWorkerManagerInterface; logger: Logger; }) { @@ -168,8 +167,8 @@ class CertManager { this.db = db; this.keyRing = keyRing; this.taskManager = taskManager; - this.certDuration = options.certDuration; - this.certRenewLeadTime = options.certRenewLeadTime; + this.certDuration = certDuration; + this.certRenewLeadTime = certRenewLeadTime; this.workerManager = workerManager; } diff --git a/src/keys/KeyRing.ts b/src/keys/KeyRing.ts index b23a22845..5b9bcd27f 100644 --- a/src/keys/KeyRing.ts +++ b/src/keys/KeyRing.ts @@ -12,7 +12,6 @@ import type { RecoveryCodeLocked, PasswordOpsLimit, PasswordMemLimit, - KeyRingOptions, } from './types'; import type { NodeId } from '../ids/types'; import type { PolykeyWorkerManagerInterface } from '../workers/types'; @@ -28,7 +27,6 @@ import * as keysUtils from './utils'; import * as keysErrors from './errors'; import * as keysEvents from './events'; import { bufferLock, bufferUnlock } from './utils/memory'; -import * as utils from '../utils/utils'; interface KeyRing extends CreateDestroyStartStop {} @CreateDestroyStartStop( @@ -46,35 +44,48 @@ interface KeyRing extends CreateDestroyStartStop {} class KeyRing { public static async createKeyRing({ keysPath, - password, - options = {}, + passwordOpsLimit, + passwordMemLimit, + strictMemoryLock = true, workerManager, fs = require('fs'), logger = new Logger(this.name), - fresh, + ...startOptions }: { keysPath: string; password: string; - options?: Partial; + passwordOpsLimit?: PasswordOpsLimit; + passwordMemLimit?: PasswordMemLimit; + strictMemoryLock?: boolean; workerManager?: PolykeyWorkerManagerInterface; fs?: FileSystem; logger?: Logger; fresh?: boolean; - }): Promise { + } & ( // eslint-disable-next-line @typescript-eslint/ban-types + | {} + | { + recoveryCode: RecoveryCode; + } + | { + privateKey: PrivateKey; + } + | { + privateKeyPath: string; + } + )): Promise { logger.info(`Creating ${this.name}`); logger.info(`Setting keys path to ${keysPath}`); - const optionsDefaulted = utils.mergeObjects(options, { - strictMemoryLock: true, - }) as KeyRingOptions; const keyRing = new this({ keysPath, + passwordOpsLimit, + passwordMemLimit, + strictMemoryLock, workerManager, - options: optionsDefaulted, fs, logger, }); // Spreading defaulted options to start to provide the keys overrides - await keyRing.start({ password, fresh, ...optionsDefaulted }); + await keyRing.start(startOptions); logger.info(`Created ${this.name}`); return keyRing; } @@ -102,13 +113,17 @@ class KeyRing { public constructor({ keysPath, workerManager, - options, + passwordOpsLimit, + passwordMemLimit, + strictMemoryLock, fs, logger, }: { keysPath: string; workerManager?: PolykeyWorkerManagerInterface; - options: KeyRingOptions; + passwordOpsLimit?: PasswordOpsLimit; + passwordMemLimit?: PasswordMemLimit; + strictMemoryLock: boolean; fs: FileSystem; logger: Logger; }) { @@ -116,9 +131,9 @@ class KeyRing { this.keysPath = keysPath; this.workerManager = workerManager; this.fs = fs; - this.passwordOpsLimit = options.passwordOpsLimit; - this.passwordMemLimit = options.passwordMemLimit; - this.strictMemoryLock = options.strictMemoryLock; + this.passwordOpsLimit = passwordOpsLimit; + this.passwordMemLimit = passwordMemLimit; + this.strictMemoryLock = strictMemoryLock; this.publicKeyPath = path.join(keysPath, 'public.jwk'); this.privateKeyPath = path.join(keysPath, 'private.jwk'); this.dbKeyPath = path.join(keysPath, 'db.jwk'); diff --git a/src/keys/types.ts b/src/keys/types.ts index c8ba9da88..de920b19d 100644 --- a/src/keys/types.ts +++ b/src/keys/types.ts @@ -1,6 +1,6 @@ import type { X509Certificate } from '@peculiar/x509'; import type { NodeId } from '../ids/types'; -import type { Opaque, InverseRecord, ObjectEmpty } from '../types'; +import type { Opaque, InverseRecord } from '../types'; /** * Locked buffer wrapper type for sensitive in-memory data. @@ -213,8 +213,22 @@ type PasswordHash = Opaque<'PasswordHash', Buffer>; type PasswordSalt = Opaque<'PasswordSalt', Buffer>; +type PasswordOpsLimitChoice = + | 'min' + | 'max' + | 'interactive' + | 'moderate' + | 'sensitive'; + type PasswordOpsLimit = Opaque<'PasswordOpsLimit', number>; +type PasswordMemLimitChoice = + | 'min' + | 'max' + | 'interactive' + | 'moderate' + | 'sensitive'; + type PasswordMemLimit = Opaque<'PasswordMemLimit', number>; /** @@ -262,39 +276,6 @@ type CertManagerChangeData = { recoveryCode?: RecoveryCode; }; -/** - * Used by the PolykeyAgent for it's top level options - */ -type KeysOptions = KeyRingOptions & CertManagerOptions; - -/** - * Options for the KeyRing - */ -type KeyRingOptions = { - passwordOpsLimit?: PasswordOpsLimit; - passwordMemLimit?: PasswordMemLimit; - strictMemoryLock: boolean; -} & ( - | ObjectEmpty - | { - recoveryCode: RecoveryCode; - } - | { - privateKey: PrivateKey; - } - | { - privateKeyPath: string; - } -); - -/** - * Options for the CertManager - */ -type CertManagerOptions = { - certDuration: number; - certRenewLeadTime: number; -}; - export type { BufferLocked, Key, @@ -322,7 +303,9 @@ export type { MAC, PasswordHash, PasswordSalt, + PasswordOpsLimitChoice, PasswordOpsLimit, + PasswordMemLimitChoice, PasswordMemLimit, RecoveryCode, RecoveryCodeLocked, @@ -331,9 +314,6 @@ export type { CertificatePEM, CertificatePEMChain, CertManagerChangeData, - KeysOptions, - KeyRingOptions, - CertManagerOptions, }; export type { CertId, CertIdString, CertIdEncoded } from '../ids/types'; diff --git a/src/keys/utils/password.ts b/src/keys/utils/password.ts index 44fb3b9e6..8e9f920ed 100644 --- a/src/keys/utils/password.ts +++ b/src/keys/utils/password.ts @@ -1,7 +1,9 @@ import type { PasswordHash, PasswordSalt, + PasswordOpsLimitChoice, PasswordOpsLimit, + PasswordMemLimitChoice, PasswordMemLimit, } from '../types'; import sodium from 'sodium-native'; @@ -11,13 +13,7 @@ import * as keysErrors from '../errors'; /** * Use the `min` limit during testing to improve performance. */ -const passwordOpsLimits: { - min: PasswordOpsLimit; - max: PasswordOpsLimit; - interactive: PasswordOpsLimit; - moderate: PasswordOpsLimit; - sensitive: PasswordOpsLimit; -} = { +const passwordOpsLimits: Record = { min: sodium.crypto_pwhash_OPSLIMIT_MIN, max: sodium.crypto_pwhash_OPSLIMIT_MAX, interactive: sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE, @@ -28,13 +24,7 @@ const passwordOpsLimits: { /** * Use the `min` limit during testing to improve performance. */ -const passwordMemLimits: { - min: PasswordMemLimit; - max: PasswordMemLimit; - interactive: PasswordMemLimit; - moderate: PasswordMemLimit; - sensitive: PasswordMemLimit; -} = { +const passwordMemLimits: Record = { min: sodium.crypto_pwhash_MEMLIMIT_MIN, max: sodium.crypto_pwhash_MEMLIMIT_MAX, interactive: sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE, diff --git a/src/nodes/NodeConnectionManager.ts b/src/nodes/NodeConnectionManager.ts index b66302a68..bcbbd1a95 100644 --- a/src/nodes/NodeConnectionManager.ts +++ b/src/nodes/NodeConnectionManager.ts @@ -1,7 +1,6 @@ import type { LockRequest } from '@matrixai/async-locks'; import type { ResourceAcquire } from '@matrixai/resources'; import type { ContextTimedInput, ContextTimed } from '@matrixai/contexts'; -import type { PromiseCancellable } from '@matrixai/async-cancellable'; import type { ClientCryptoOps, QUICConnection } from '@matrixai/quic'; import type NodeGraph from './NodeGraph'; import type { @@ -10,7 +9,6 @@ import type { NodeId, NodeIdString, SeedNodes, - NodesOptions, } from './types'; import type KeyRing from '../keys/KeyRing'; import type { Key, CertificatePEM } from '../keys/types'; @@ -22,15 +20,15 @@ import type { TLSConfig, } from '../network/types'; import type { ServerManifest } from '@matrixai/rpc'; -import type { HolePunchRelayMessage } from './agent/types'; import Logger from '@matrixai/logger'; import { withF } from '@matrixai/resources'; import { ready, StartStop } from '@matrixai/async-init/dist/StartStop'; import { IdInternal } from '@matrixai/id'; -import { Lock, LockBox } from '@matrixai/async-locks'; +import { Lock, LockBox, Semaphore } from '@matrixai/async-locks'; import { Timer } from '@matrixai/timer'; import { timedCancellable, context } from '@matrixai/contexts/dist/decorators'; import { AbstractEvent, EventAll } from '@matrixai/events'; +import { PromiseCancellable } from '@matrixai/async-cancellable'; import { QUICSocket, QUICServer, @@ -44,11 +42,11 @@ import * as nodesUtils from './utils'; import * as nodesErrors from './errors'; import * as nodesEvents from './events'; import manifestClientAgent from './agent/callers'; -import * as ids from '../ids'; import * as keysUtils from '../keys/utils'; import * as networkUtils from '../network/utils'; import * as utils from '../utils'; import config from '../config'; +import RateLimiter from '../utils/ratelimiter/RateLimiter'; type ManifestClientAgent = typeof manifestClientAgent; @@ -130,6 +128,31 @@ class NodeConnectionManager { * Default timeout for RPC handlers */ public readonly rpcCallTimeoutTime: number; + /** + * Used to track active hole punching attempts. + * Attempts are mapped by a string of `${host}:${port}`. + * This is used to coalesce attempts to a target host and port. + * Used to cancel and await punch attempts when stopping to prevent orphaned promises. + */ + protected activeHolePunchPs = new Map>(); + /** + * Used to rate limit hole punch attempts per IP Address. + * We use a semaphore to track the number of active hole punch attempts to that address. + * We Use a semaphore here to allow a limit of 3 attempts per host. + * To allow concurrent attempts to the same host while limiting the number of different ports. + * This is mainly used to limit requests to a single target host. + */ + protected activeHolePunchAddresses = new Map(); + /** + * Used track the active `nodesConnectionSignalFinal` attempts and prevent orphaned promises. + * Used to cancel and await the active `nodesConnectionSignalFinal` when stopping. + */ + protected activeSignalFinalPs = new Set>(); + /** + * Used to limit signalling requests on a per-requester basis. + * This is mainly used to limit a single source node making too many requests through a relay. + */ + protected rateLimiter = new RateLimiter(60000, 20, 10, 1); protected logger: Logger; protected keyRing: KeyRing; @@ -265,32 +288,36 @@ class NodeConnectionManager { nodeGraph, tlsConfig, seedNodes = {}, - options = {}, + connectionFindConcurrencyLimit = config.defaultsSystem + .nodesConnectionFindConcurrencyLimit, + connectionIdleTimeoutTime = config.defaultsSystem + .nodesConnectionIdleTimeoutTime, + connectionConnectTimeoutTime = config.defaultsSystem + .nodesConnectionConnectTimeoutTime, + connectionKeepAliveTimeoutTime = config.defaultsSystem + .nodesConnectionKeepAliveTimeoutTime, + connectionKeepAliveIntervalTime = config.defaultsSystem + .nodesConnectionKeepAliveIntervalTime, + connectionHolePunchIntervalTime = config.defaultsSystem + .nodesConnectionHolePunchIntervalTime, + rpcParserBufferSize = config.defaultsSystem.rpcParserBufferSize, + rpcCallTimeoutTime = config.defaultsSystem.rpcCallTimeoutTime, logger, }: { keyRing: KeyRing; nodeGraph: NodeGraph; tlsConfig: TLSConfig; seedNodes?: SeedNodes; - options?: Partial; + connectionFindConcurrencyLimit?: number; + connectionIdleTimeoutTime?: number; + connectionConnectTimeoutTime?: number; + connectionKeepAliveTimeoutTime?: number; + connectionKeepAliveIntervalTime?: number; + connectionHolePunchIntervalTime?: number; + rpcParserBufferSize?: number; + rpcCallTimeoutTime?: number; logger?: Logger; }) { - const optionsDefaulted = utils.mergeObjects(options, { - connectionFindConcurrencyLimit: - config.defaultsSystem.nodesConnectionFindConcurrencyLimit, - connectionIdleTimeoutTime: - config.defaultsSystem.nodesConnectionIdleTimeoutTime, - connectionConnectTimeoutTime: - config.defaultsSystem.clientConnectTimeoutTime, - connectionKeepAliveTimeoutTime: - config.defaultsSystem.clientKeepAliveTimeoutTime, - connectionKeepAliveIntervalTime: - config.defaultsSystem.clientKeepAliveIntervalTime, - connectionHolePunchIntervalTime: - config.defaultsSystem.nodesConnectionHolePunchIntervalTime, - rpcParserBufferSize: config.defaultsSystem.rpcParserBufferSize, - rpcCallTimeoutTime: config.defaultsSystem.rpcCallTimeoutTime, - }); this.logger = logger ?? new Logger(this.constructor.name); this.keyRing = keyRing; this.nodeGraph = nodeGraph; @@ -300,19 +327,14 @@ class NodeConnectionManager { this.seedNodes = utils.filterObject(seedNodes, ([k]) => { return k !== nodeIdEncodedOwn; }) as SeedNodes; - this.connectionFindConcurrencyLimit = - optionsDefaulted.connectionFindConcurrencyLimit; - this.connectionIdleTimeoutTime = optionsDefaulted.connectionIdleTimeoutTime; - this.connectionConnectTimeoutTime = - optionsDefaulted.connectionConnectTimeoutTime; - this.connectionKeepAliveTimeoutTime = - optionsDefaulted.connectionKeepAliveTimeoutTime; - this.connectionKeepAliveIntervalTime = - optionsDefaulted.connectionKeepAliveIntervalTime; - this.connectionHolePunchIntervalTime = - optionsDefaulted.connectionHolePunchIntervalTime; - this.rpcParserBufferSize = optionsDefaulted.rpcParserBufferSize; - this.rpcCallTimeoutTime = optionsDefaulted.rpcCallTimeoutTime; + this.connectionFindConcurrencyLimit = connectionFindConcurrencyLimit; + this.connectionIdleTimeoutTime = connectionIdleTimeoutTime; + this.connectionConnectTimeoutTime = connectionConnectTimeoutTime; + this.connectionKeepAliveTimeoutTime = connectionKeepAliveTimeoutTime; + this.connectionKeepAliveIntervalTime = connectionKeepAliveIntervalTime; + this.connectionHolePunchIntervalTime = connectionHolePunchIntervalTime; + this.rpcParserBufferSize = rpcParserBufferSize; + this.rpcCallTimeoutTime = rpcCallTimeoutTime; // Note that all buffers allocated for crypto operations is using // `allocUnsafeSlow`. Which ensures that the underlying `ArrayBuffer` // is not shared. Also, all node buffers satisfy the `ArrayBuffer` interface. @@ -355,8 +377,8 @@ class NodeConnectionManager { // procedures. const quicServer = new QUICServer({ config: { - maxIdleTimeout: optionsDefaulted.connectionKeepAliveTimeoutTime, - keepAliveIntervalTime: optionsDefaulted.connectionKeepAliveIntervalTime, + maxIdleTimeout: connectionKeepAliveTimeoutTime, + keepAliveIntervalTime: connectionKeepAliveIntervalTime, key: tlsConfig.keyPrivatePem, cert: tlsConfig.certChainPem, verifyPeer: true, @@ -366,7 +388,7 @@ class NodeConnectionManager { socket: quicSocket, reasonToCode: nodesUtils.reasonToCode, codeToReason: nodesUtils.codeToReason, - minIdleTimeout: optionsDefaulted.connectionConnectTimeoutTime, + minIdleTimeout: connectionConnectTimeoutTime, logger: this.logger.getChild(QUICServer.name), }); // Setting up RPCServer @@ -458,11 +480,13 @@ class NodeConnectionManager { this.handleEventQUICServerConnection, ); this.quicSocket.addEventListener(EventAll.name, this.handleEventAll); + this.rateLimiter.startRefillInterval(); this.logger.info(`Started ${this.constructor.name}`); } public async stop() { this.logger.info(`Stop ${this.constructor.name}`); + this.rateLimiter.stop(); this.removeEventListener( nodesEvents.EventNodeConnectionManagerError.name, @@ -505,6 +529,16 @@ class NodeConnectionManager { destroyProms.push(destroyProm); } await Promise.all(destroyProms); + const signallingProms: Array> = []; + for (const [, activePunch] of this.activeHolePunchPs) { + signallingProms.push(activePunch); + activePunch.cancel(); + } + for (const activeSignal of this.activeSignalFinalPs) { + signallingProms.push(activeSignal); + activeSignal.cancel(); + } + await Promise.allSettled(signallingProms); await this.quicServer.stop({ force: true }); await this.quicSocket.stop({ force: true }); await this.rpcServer.stop({ force: true }); @@ -906,12 +940,7 @@ class NodeConnectionManager { // 3. if already exists then clean up await connection.destroy({ force: true }); // I can only see this happening as a race condition with creating a forward connection and receiving a reverse. - // FIXME: only here to see if this condition happens. - // this NEEDS to be removed, but I want to know if this branch happens at all. - throw Error( - 'TMP IMP, This should be exceedingly rare, lets see if it happens', - ); - // Return; + return; } // Final setup const newConnAndTimer = this.addConnection(nodeId, connection); @@ -1070,24 +1099,27 @@ class NodeConnectionManager { * Open up a port in the NAT by sending packets to the target address. * The packets will be sent in an exponential backoff dialing pattern and contain random data. * + * This is only ever done used in the reverse direction to open up the nat for the connection to establish from the + * forward direction. + * * This can't know it succeeded, it will continue until timed out or cancelled. * * @param host host of the target client. * @param port port of the target client. * @param ctx */ - public async holePunchReverse( + public holePunch( host: Host, port: Port, - ctx?: Partial, - ): Promise; + ctx?: Partial, + ): PromiseCancellable; @ready(new nodesErrors.ErrorNodeConnectionManagerNotRunning()) @timedCancellable( true, (nodeConnectionManager: NodeConnectionManager) => nodeConnectionManager.connectionConnectTimeoutTime, ) - public async holePunchReverse( + public async holePunch( host: Host, port: Port, @context ctx: ContextTimed, @@ -1380,106 +1412,149 @@ class NodeConnectionManager { } /** - * Performs an RPC request to send a hole-punch message to the target. Used to - * initially establish the NodeConnection from source to target. + * This is used by the `NodesConnectionSignalFinal` to initiate the hole punch procedure. * - * @param relayNodeId node ID of the relay node (i.e. the seed node) - * @param sourceNodeId node ID of the current node (i.e. the sender) - * @param targetNodeId node ID of the target node to hole punch - * @param address - * @param ctx + * Will validate the message, and initiate hole punching in the background and return immediately. + * Attempts to the same host and port are coalesced. + * Attempts to the same host are limited by a semaphore. + * Active attempts are tracked inside of the `activeHolePunchPs` set and are cancelled and awaited when the + * `NodeConnectionManager` stops. */ - public sendSignalingMessage( - relayNodeId: NodeId, - sourceNodeId: NodeId, - targetNodeId: NodeId, - address?: NodeAddress, - ctx?: Partial, - ): PromiseCancellable; - @ready(new nodesErrors.ErrorNodeConnectionManagerNotRunning()) - @timedCancellable( - true, - (nodeConnectionManager: NodeConnectionManager) => - nodeConnectionManager.connectionConnectTimeoutTime, - ) - public async sendSignalingMessage( - relayNodeId: NodeId, + @ready(new nodesErrors.ErrorNodeManagerNotRunning()) + public handleNodesConnectionSignalFinal(host: Host, port: Port) { + const id = `${host}:${port}`; + if (this.activeHolePunchPs.has(id)) return; + // Checking for resource semaphore + let semaphore: Semaphore | undefined = + this.activeHolePunchAddresses.get(host); + if (semaphore == null) { + semaphore = new Semaphore(3); + this.activeHolePunchAddresses.set(host, semaphore); + } + const holePunchAttempt = new PromiseCancellable( + async (res, rej, signal) => { + await semaphore!.withF(async () => { + this.holePunch(host, port, { signal }) + .finally(() => { + this.activeHolePunchPs.delete(id); + if (semaphore!.count === 0) { + this.activeHolePunchAddresses.delete(host); + } + }) + .then(res, rej); + }); + }, + ); + this.activeHolePunchPs.set(id, holePunchAttempt); + } + + /** + * This is used by the `NodesConnectionSignalInitial` to initiate a relay request. + * Requests can only be relayed to nodes this node is currently connected to. + * + * Requests made by the same node are rate limited, when the limit has been exceeded the request + * throws an `ErrorNodeConnectionManagerRequestRateExceeded` error. + * + * Active relay attempts are tracked in `activeSignalFinalPs` and are cancelled and awaited when the + * `NodeConnectionManager` stops. + * + * @param sourceNodeId - NodeId of the node making the request. Used for rate limiting. + * @param targetNodeId - NodeId of the node that needs to initiate hole punching. + * @param address - Address the target needs to punch to. + * @param requestSignature - `base64url` encoded signature + */ + @ready(new nodesErrors.ErrorNodeManagerNotRunning()) + public handleNodesConnectionSignalInitial( sourceNodeId: NodeId, targetNodeId: NodeId, - address: NodeAddress | undefined, - @context ctx: ContextTimed, - ): Promise { - if ( - this.keyRing.getNodeId().equals(relayNodeId) || - this.keyRing.getNodeId().equals(targetNodeId) - ) { - // Logging and silently dropping operation - this.logger.debug( - 'Attempted to send signaling message to our own NodeId', - ); - return; + address: NodeAddress, + requestSignature: string, + ) { + // Need to get the connection details of the requester and add it to the message. + // Then send the message to the target. + // This would only function with existing connections + if (!this.hasConnection(targetNodeId)) { + throw new nodesErrors.ErrorNodeConnectionManagerConnectionNotFound(); } - const rlyNode = nodesUtils.encodeNodeId(relayNodeId); - const srcNode = nodesUtils.encodeNodeId(sourceNodeId); - const tgtNode = nodesUtils.encodeNodeId(targetNodeId); - const addressString = - address != null ? `, address: ${address.host}:${address.port}` : ''; - this.logger.debug( - `sendSignalingMessage sending Signaling message relay: ${rlyNode}, source: ${srcNode}, target: ${tgtNode}${addressString}`, + // Do other checks. + const sourceNodeIdString = sourceNodeId.toString(); + if (!this.rateLimiter.consume(sourceNodeIdString)) { + throw new nodesErrors.ErrorNodeConnectionManagerRequestRateExceeded(); + } + // Generating relay signature, data is just `
` concatenated + const data = Buffer.concat([ + sourceNodeId, + targetNodeId, + Buffer.from(JSON.stringify(address), 'utf-8'), + Buffer.from(requestSignature, 'base64url'), + ]); + const relaySignature = keysUtils.signWithPrivateKey( + this.keyRing.keyPair, + data, ); - // Send message and ignore any error - await this.withConnF( - relayNodeId, - async (connection) => { - const client = connection.getClient(); - await client.methods.nodesHolePunchMessageSend( - { - srcIdEncoded: srcNode, - dstIdEncoded: tgtNode, - address, - }, - ctx, - ); - }, - ctx, - ).catch(() => {}); + const connProm = this.withConnF(targetNodeId, async (conn) => { + const client = conn.getClient(); + await client.methods.nodesConnectionSignalFinal({ + sourceNodeIdEncoded: nodesUtils.encodeNodeId(sourceNodeId), + targetNodeIdEncoded: nodesUtils.encodeNodeId(targetNodeId), + address, + requestSignature: requestSignature, + relaySignature: relaySignature.toString('base64url'), + }); + }).finally(() => { + this.activeSignalFinalPs.delete(connProm); + }); + this.activeSignalFinalPs.add(connProm); } /** - * Forwards a received hole punch message on to the target. - * If not known, the node ID -> address mapping is attempted to be discovered - * through Kademlia (note, however, this is currently only called by a 'broker' - * node). - * @param message the original relay message (assumed to be created in - * nodeConnection.start()) - * @param sourceAddress + * Will make a connection to the signalling node and make a nodesConnectionSignalInitial` request. + * + * If the signalling node does not have an existing connection to the target then this will throw. + * If verification of the request fails then this will throw, but this shouldn't happen. + * The request contains a signature generated from ``. + * + * + * + * @param targetNodeId - NodeId of the node that needs to signal back. + * @param signallingNodeId - NodeId of the signalling node. * @param ctx */ - public relaySignalingMessage( - message: HolePunchRelayMessage, - sourceAddress: NodeAddress, - ctx?: Partial, + public connectionSignalInitial( + targetNodeId: NodeId, + signallingNodeId: NodeId, + ctx?: Partial, ): PromiseCancellable; - @ready(new nodesErrors.ErrorNodeConnectionManagerNotRunning()) + @ready(new nodesErrors.ErrorNodeManagerNotRunning()) @timedCancellable( true, (nodeConnectionManager: NodeConnectionManager) => nodeConnectionManager.connectionConnectTimeoutTime, ) - public async relaySignalingMessage( - message: HolePunchRelayMessage, - sourceAddress: NodeAddress, + public async connectionSignalInitial( + targetNodeId: NodeId, + signallingNodeId: NodeId, @context ctx: ContextTimed, ): Promise { - // First check if we already have an existing ID -> address record - // If we're relaying then we trust our own node graph records over - // what was provided in the message - const sourceNode = ids.parseNodeId(message.srcIdEncoded); - await this.sendSignalingMessage( - ids.parseNodeId(message.dstIdEncoded), - sourceNode, - ids.parseNodeId(message.dstIdEncoded), - sourceAddress, + await this.withConnF( + signallingNodeId, + async (conn) => { + const client = conn.getClient(); + const sourceNodeId = this.keyRing.getNodeId(); + // Data is just `` concatenated + const data = Buffer.concat([sourceNodeId, targetNodeId]); + const signature = keysUtils.signWithPrivateKey( + this.keyRing.keyPair, + data, + ); + await client.methods.nodesConnectionSignalInitial( + { + targetNodeIdEncoded: nodesUtils.encodeNodeId(targetNodeId), + signature: signature.toString('base64url'), + }, + ctx, + ); + }, ctx, ); } @@ -1643,8 +1718,8 @@ class NodeConnectionManager { * This is pretty simple, it will contact all known seed nodes and get them to * relay a punch signal message. * - * Note: Avoid using a large set of target nodes, It could trigger a large - * amount of pings to a single target. + * This doesn't care if the requests fail or succeed, so any errors or results are ignored. + * */ protected initiateHolePunch( targetNodeIds: Array, @@ -1659,15 +1734,10 @@ class NodeConnectionManager { const allProms: Array>> = []; for (const targetNodeId of targetNodeIds) { if (!this.isSeedNode(targetNodeId)) { + // Ask seed nodes to signal hole punching for target const holePunchProms = seedNodes.map((seedNodeId) => { return ( - this.sendSignalingMessage( - seedNodeId, - this.keyRing.getNodeId(), - targetNodeId, - undefined, - ctx, - ) + this.connectionSignalInitial(targetNodeId, seedNodeId, ctx) // Ignore results .then( () => {}, diff --git a/src/nodes/agent/callers/index.ts b/src/nodes/agent/callers/index.ts index 32e6c916e..3bb027081 100644 --- a/src/nodes/agent/callers/index.ts +++ b/src/nodes/agent/callers/index.ts @@ -1,7 +1,8 @@ import nodesClaimsGet from './nodesClaimsGet'; import nodesClosestLocalNodesGet from './nodesClosestLocalNodesGet'; +import nodesConnectionSignalFinal from './nodesConnectionSignalFinal'; +import nodesConnectionSignalInitial from './nodesConnectionSignalInitial'; import nodesCrossSignClaim from './nodesCrossSignClaim'; -import nodesHolePunchMessageSend from './nodesHolePunchMessageSend'; import notificationsSend from './notificationsSend'; import vaultsGitInfoGet from './vaultsGitInfoGet'; import vaultsGitPackGet from './vaultsGitPackGet'; @@ -13,8 +14,9 @@ import vaultsScan from './vaultsScan'; const manifestClient = { nodesClaimsGet, nodesClosestLocalNodesGet, + nodesConnectionSignalFinal, + nodesConnectionSignalInitial, nodesCrossSignClaim, - nodesHolePunchMessageSend, notificationsSend, vaultsGitInfoGet, vaultsGitPackGet, @@ -26,8 +28,9 @@ export default manifestClient; export { nodesClaimsGet, nodesClosestLocalNodesGet, + nodesConnectionSignalFinal, + nodesConnectionSignalInitial, nodesCrossSignClaim, - nodesHolePunchMessageSend, notificationsSend, vaultsGitInfoGet, vaultsGitPackGet, diff --git a/src/nodes/agent/callers/nodesConnectionSignalFinal.ts b/src/nodes/agent/callers/nodesConnectionSignalFinal.ts new file mode 100644 index 000000000..2d341f2d4 --- /dev/null +++ b/src/nodes/agent/callers/nodesConnectionSignalFinal.ts @@ -0,0 +1,12 @@ +import type { HandlerTypes } from '@matrixai/rpc'; +import type NodesConnectionSignalFinal from '../handlers/NodesConnectionSignalFinal'; +import { UnaryCaller } from '@matrixai/rpc'; + +type CallerTypes = HandlerTypes; + +const nodesConnectionSignalFinal = new UnaryCaller< + CallerTypes['input'], + CallerTypes['output'] +>(); + +export default nodesConnectionSignalFinal; diff --git a/src/nodes/agent/callers/nodesConnectionSignalInitial.ts b/src/nodes/agent/callers/nodesConnectionSignalInitial.ts new file mode 100644 index 000000000..eef756e4e --- /dev/null +++ b/src/nodes/agent/callers/nodesConnectionSignalInitial.ts @@ -0,0 +1,12 @@ +import type { HandlerTypes } from '@matrixai/rpc'; +import type NodesConnectionSignalInitial from '../handlers/NodesConnectionSignalInitial'; +import { UnaryCaller } from '@matrixai/rpc'; + +type CallerTypes = HandlerTypes; + +const nodesConnectionSignalInitial = new UnaryCaller< + CallerTypes['input'], + CallerTypes['output'] +>(); + +export default nodesConnectionSignalInitial; diff --git a/src/nodes/agent/callers/nodesHolePunchMessageSend.ts b/src/nodes/agent/callers/nodesHolePunchMessageSend.ts deleted file mode 100644 index cea3acc06..000000000 --- a/src/nodes/agent/callers/nodesHolePunchMessageSend.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { HandlerTypes } from '@matrixai/rpc'; -import type NodesHolePunchMessageSend from '../handlers/NodesHolePunchMessageSend'; -import { UnaryCaller } from '@matrixai/rpc'; - -type CallerTypes = HandlerTypes; - -const nodesHolePunchMessageSend = new UnaryCaller< - CallerTypes['input'], - CallerTypes['output'] ->(); - -export default nodesHolePunchMessageSend; diff --git a/src/nodes/agent/errors.ts b/src/nodes/agent/errors.ts index b52b6f9ac..44bad9347 100644 --- a/src/nodes/agent/errors.ts +++ b/src/nodes/agent/errors.ts @@ -8,4 +8,22 @@ class ErrorAgentNodeIdMissing extends ErrorAgent { exitCode = sysexits.UNAVAILABLE; } -export { ErrorAgentNodeIdMissing }; +class ErrorNodesConnectionSignalRequestVerificationFailed< + T, +> extends ErrorAgent { + static description = 'Failed to verify request message signature'; + exitCode = sysexits.UNAVAILABLE; +} + +class ErrorNodesConnectionSignalRelayVerificationFailed< + T, +> extends ErrorAgent { + static description = 'Failed to verify relay message signature'; + exitCode = sysexits.UNAVAILABLE; +} + +export { + ErrorAgentNodeIdMissing, + ErrorNodesConnectionSignalRequestVerificationFailed, + ErrorNodesConnectionSignalRelayVerificationFailed, +}; diff --git a/src/nodes/agent/handlers/NodesConnectionSignalFinal.ts b/src/nodes/agent/handlers/NodesConnectionSignalFinal.ts new file mode 100644 index 000000000..4e8b52457 --- /dev/null +++ b/src/nodes/agent/handlers/NodesConnectionSignalFinal.ts @@ -0,0 +1,73 @@ +import type Logger from '@matrixai/logger'; +import type { + AgentRPCRequestParams, + AgentRPCResponseResult, + HolePunchRequestMessage, +} from '../types'; +import type NodeConnectionManager from '../../NodeConnectionManager'; +import type { Host, Port } from '../../../network/types'; +import { UnaryHandler } from '@matrixai/rpc'; +import * as keysUtils from '../../../keys/utils'; +import * as ids from '../../../ids'; +import * as agentErrors from '../errors'; +import * as agentUtils from '../utils'; + +class NodesConnectionSignalFinal extends UnaryHandler< + { + nodeConnectionManager: NodeConnectionManager; + logger: Logger; + }, + AgentRPCRequestParams, + AgentRPCResponseResult +> { + public handle = async ( + input: AgentRPCRequestParams, + _cancel, + meta, + ): Promise => { + const { nodeConnectionManager, logger } = this.container; + // Connections should always be validated + const sourceNodeId = ids.parseNodeId(input.sourceNodeIdEncoded); + const targetNodeId = ids.parseNodeId(input.targetNodeIdEncoded); + const relayingNodeId = agentUtils.nodeIdFromMeta(meta); + if (relayingNodeId == null) { + throw new agentErrors.ErrorAgentNodeIdMissing(); + } + const requestSignature = Buffer.from(input.requestSignature, 'base64url'); + // Checking request requestSignature, requestData is just `` concatenated + const requestData = Buffer.concat([sourceNodeId, targetNodeId]); + const sourcePublicKey = keysUtils.publicKeyFromNodeId(sourceNodeId); + if ( + !keysUtils.verifyWithPublicKey( + sourcePublicKey, + requestData, + requestSignature, + ) + ) { + throw new agentErrors.ErrorNodesConnectionSignalRequestVerificationFailed(); + } + // Checking relay message relaySignature. + // relayData is just `
` concatenated. + const relayData = Buffer.concat([ + sourceNodeId, + targetNodeId, + Buffer.from(JSON.stringify(input.address), 'utf-8'), + requestSignature, + ]); + const relayPublicKey = keysUtils.publicKeyFromNodeId(relayingNodeId); + const relaySignature = Buffer.from(input.relaySignature, 'base64url'); + if ( + !keysUtils.verifyWithPublicKey(relayPublicKey, relayData, relaySignature) + ) { + throw new agentErrors.ErrorNodesConnectionSignalRelayVerificationFailed(); + } + + const host = input.address.host as Host; + const port = input.address.port as Port; + logger.debug(`Received signaling message to target ${host}:${port}`); + nodeConnectionManager.handleNodesConnectionSignalFinal(host, port); + return {}; + }; +} + +export default NodesConnectionSignalFinal; diff --git a/src/nodes/agent/handlers/NodesConnectionSignalInitial.ts b/src/nodes/agent/handlers/NodesConnectionSignalInitial.ts new file mode 100644 index 000000000..2ee438bbc --- /dev/null +++ b/src/nodes/agent/handlers/NodesConnectionSignalInitial.ts @@ -0,0 +1,66 @@ +import type { + AgentRPCRequestParams, + AgentRPCResponseResult, + HolePunchSignalMessage, +} from '../types'; +import type NodeConnectionManager from '../../../nodes/NodeConnectionManager'; +import type { Host, Port } from '../../../network/types'; +import type { NodeAddress } from '../../../nodes/types'; +import type { JSONValue } from '../../../types'; +import { UnaryHandler } from '@matrixai/rpc'; +import * as agentErrors from '../errors'; +import * as agentUtils from '../utils'; +import { never } from '../../../utils'; +import * as keysUtils from '../../../keys/utils'; +import * as ids from '../../../ids'; + +class NodesConnectionSignalInitial extends UnaryHandler< + { + nodeConnectionManager: NodeConnectionManager; + }, + AgentRPCRequestParams, + AgentRPCResponseResult +> { + public handle = async ( + input: AgentRPCRequestParams, + _cancel, + meta: Record | undefined, + ): Promise => { + const { nodeConnectionManager } = this.container; + // Connections should always be validated + const requestingNodeId = agentUtils.nodeIdFromMeta(meta); + if (requestingNodeId == null) { + throw new agentErrors.ErrorAgentNodeIdMissing(); + } + const targetNodeId = ids.parseNodeId(input.targetNodeIdEncoded); + const signature = Buffer.from(input.signature, 'base64url'); + // Checking signature, data is just `` concatenated + const data = Buffer.concat([requestingNodeId, targetNodeId]); + const sourcePublicKey = keysUtils.publicKeyFromNodeId(requestingNodeId); + if (!keysUtils.verifyWithPublicKey(sourcePublicKey, data, signature)) { + throw new agentErrors.ErrorNodesConnectionSignalRelayVerificationFailed(); + } + if (meta == null) never('Missing metadata from stream'); + const remoteHost = meta.remoteHost; + const remotePort = meta.remotePort; + if (remoteHost == null || typeof remoteHost !== 'string') { + never('Missing or invalid remoteHost'); + } + if (remotePort == null || typeof remotePort !== 'number') { + never('Missing or invalid remotePort'); + } + const address: NodeAddress = { + host: remoteHost as Host, + port: remotePort as Port, + }; + nodeConnectionManager.handleNodesConnectionSignalInitial( + requestingNodeId, + targetNodeId, + address, + input.signature, + ); + return {}; + }; +} + +export default NodesConnectionSignalInitial; diff --git a/src/nodes/agent/handlers/NodesHolePunchMessageSend.ts b/src/nodes/agent/handlers/NodesHolePunchMessageSend.ts deleted file mode 100644 index 947fc2b94..000000000 --- a/src/nodes/agent/handlers/NodesHolePunchMessageSend.ts +++ /dev/null @@ -1,113 +0,0 @@ -import type { DB } from '@matrixai/db'; -import type Logger from '@matrixai/logger'; -import type { - AgentRPCRequestParams, - AgentRPCResponseResult, - HolePunchRelayMessage, -} from '../types'; -import type NodeConnectionManager from '../../NodeConnectionManager'; -import type NodeManager from '../../NodeManager'; -import type KeyRing from '../../../keys/KeyRing'; -import type { Host, Port } from '../../../network/types'; -import type { NodeId } from '../../../ids'; -import { UnaryHandler } from '@matrixai/rpc'; -import * as agentUtils from '../utils'; -import * as agentErrors from '../errors'; -import * as ids from '../../../ids'; -import * as nodesUtils from '../../utils'; -import * as validation from '../../../validation'; -import * as utils from '../../../utils'; - -/** - * Sends a hole punch message to a node - */ -class NodesHolePunchMessageSend extends UnaryHandler< - { - db: DB; - nodeConnectionManager: NodeConnectionManager; - keyRing: KeyRing; - nodeManager: NodeManager; - logger: Logger; - }, - AgentRPCRequestParams, - AgentRPCResponseResult -> { - public handle = async ( - input: AgentRPCRequestParams, - _cancel, - meta, - ): Promise => { - const { db, nodeConnectionManager, keyRing, nodeManager, logger } = - this.container; - const { - targetId, - sourceId, - }: { - targetId: NodeId; - sourceId: NodeId; - } = validation.validateSync( - (keyPath, value) => { - return utils.matchSync(keyPath)( - [['targetId'], ['sourceId'], () => ids.parseNodeId(value)], - () => value, - ); - }, - { - targetId: input.dstIdEncoded, - sourceId: input.srcIdEncoded, - }, - ); - // Connections should always be validated - const requestingNodeId = agentUtils.nodeIdFromMeta(meta); - if (requestingNodeId == null) { - throw new agentErrors.ErrorAgentNodeIdMissing(); - } - const srcNodeId = nodesUtils.encodeNodeId(requestingNodeId); - // Firstly, check if this node is the desired node - // If so, then we want to make this node start sending hole punching packets - // back to the source node. - await db.withTransactionF(async (tran) => { - if (keyRing.getNodeId().equals(targetId)) { - if (input.address != null) { - const host = input.address.host as Host; - const port = input.address.port as Port; - logger.debug( - `Received signaling message to target ${input.srcIdEncoded}@${host}:${port}`, - ); - // Ignore failure - await nodeConnectionManager - .holePunchReverse(host, port) - .catch(() => {}); - } else { - logger.error( - 'Received signaling message, target information was missing, skipping reverse hole punch', - ); - } - } else if (await nodeManager.knowsNode(sourceId, tran)) { - // Otherwise, find if node in table - // If so, ask the nodeManager to relay to the node - const targetNodeId = input.dstIdEncoded; - const agentAddress = { - host: meta.remoteHost, - port: meta.remotePort, - }; - // Checking if the source and destination are the same - if (sourceId?.equals(targetId)) { - // Logging and silently dropping operation - logger.warn('Signaling relay message requested signal to itself'); - return {}; - } - logger.debug( - `Relaying signaling message from ${srcNodeId}@${agentAddress.host}:${agentAddress.port} to ${targetNodeId} with information ${agentAddress}`, - ); - await nodeConnectionManager.relaySignalingMessage(input, { - host: meta.remoteHost, - port: meta.remotePort, - }); - } - }); - return {}; - }; -} - -export default NodesHolePunchMessageSend; diff --git a/src/nodes/agent/handlers/index.ts b/src/nodes/agent/handlers/index.ts index ee8e72914..cb2549c72 100644 --- a/src/nodes/agent/handlers/index.ts +++ b/src/nodes/agent/handlers/index.ts @@ -10,8 +10,9 @@ import type NotificationsManager from '../../../notifications/NotificationsManag import type VaultManager from '../../../vaults/VaultManager'; import NodesClaimsGet from './NodesClaimsGet'; import NodesClosestLocalNodesGet from './NodesClosestLocalNodesGet'; +import NodesConnectionSignalFinal from './NodesConnectionSignalFinal'; +import NodesConnectionSignalInitial from './NodesConnectionSignalInitial'; import NodesCrossSignClaim from './NodesCrossSignClaim'; -import NodesHolePunchMessageSend from './NodesHolePunchMessageSend'; import NotificationsSend from './NotificationsSend'; import VaultsGitInfoGet from './VaultsGitInfoGet'; import VaultsGitPackGet from './VaultsGitPackGet'; @@ -35,8 +36,9 @@ const manifestServer = (container: { return { nodesClaimsGet: new NodesClaimsGet(container), nodesClosestLocalNodesGet: new NodesClosestLocalNodesGet(container), + nodesConnectionSignalFinal: new NodesConnectionSignalFinal(container), + nodesConnectionSignalInitial: new NodesConnectionSignalInitial(container), nodesCrossSignClaim: new NodesCrossSignClaim(container), - nodesHolePunchMessageSend: new NodesHolePunchMessageSend(container), notificationsSend: new NotificationsSend(container), vaultsGitInfoGet: new VaultsGitInfoGet(container), vaultsGitPackGet: new VaultsGitPackGet(container), @@ -49,8 +51,9 @@ export default manifestServer; export { NodesClaimsGet, NodesClosestLocalNodesGet, + NodesConnectionSignalFinal, + NodesConnectionSignalInitial, NodesCrossSignClaim, - NodesHolePunchMessageSend, NotificationsSend, VaultsGitInfoGet, VaultsGitPackGet, diff --git a/src/nodes/agent/types.ts b/src/nodes/agent/types.ts index 95de72950..a96242396 100644 --- a/src/nodes/agent/types.ts +++ b/src/nodes/agent/types.ts @@ -45,10 +45,17 @@ type AddressMessage = { type NodeAddressMessage = NodeIdMessage & AddressMessage; -type HolePunchRelayMessage = { - srcIdEncoded: NodeIdEncoded; - dstIdEncoded: NodeIdEncoded; - address?: AddressMessage; +type HolePunchRequestMessage = { + sourceNodeIdEncoded: NodeIdEncoded; + targetNodeIdEncoded: NodeIdEncoded; + address: AddressMessage; + requestSignature: string; + relaySignature: string; +}; + +type HolePunchSignalMessage = { + targetNodeIdEncoded: NodeIdEncoded; + signature: string; }; type SignedNotificationEncoded = { @@ -72,7 +79,8 @@ export type { NodeIdMessage, AddressMessage, NodeAddressMessage, - HolePunchRelayMessage, + HolePunchRequestMessage, + HolePunchSignalMessage, SignedNotificationEncoded, VaultInfo, VaultsScanMessage, diff --git a/src/nodes/errors.ts b/src/nodes/errors.ts index faa22173d..1e06b0b69 100644 --- a/src/nodes/errors.ts +++ b/src/nodes/errors.ts @@ -1,8 +1,6 @@ import ErrorPolykey from '../ErrorPolykey'; import sysexits from '../utils/sysexits'; -// TODO: Some errors may need to be removed here, TBD in stage 2 agent migration - class ErrorNodes extends ErrorPolykey {} class ErrorNodeManager extends ErrorNodes {} @@ -137,6 +135,20 @@ class ErrorNodeConnectionManagerMultiConnectionFailed< exitCode = sysexits.TEMPFAIL; } +class ErrorNodeConnectionManagerConnectionNotFound< + T, +> extends ErrorNodeConnectionManager { + static description = 'No existing connection was found for target NodeId'; + exitCode = sysexits.TEMPFAIL; +} + +class ErrorNodeConnectionManagerRequestRateExceeded< + T, +> extends ErrorNodeConnectionManager { + static description = 'Rate limit exceeded while making request'; + exitCode = sysexits.TEMPFAIL; +} + class ErrorNodePingFailed extends ErrorNodes { static description = 'Failed to ping the node when attempting to authenticate'; @@ -175,6 +187,8 @@ export { ErrorNodeConnectionManagerInternalError, ErrorNodeConnectionManagerNodeIdRequired, ErrorNodeConnectionManagerMultiConnectionFailed, + ErrorNodeConnectionManagerConnectionNotFound, + ErrorNodeConnectionManagerRequestRateExceeded, ErrorNodePingFailed, ErrorNodePermissionDenied, }; diff --git a/src/nodes/types.ts b/src/nodes/types.ts index f52cd7aac..41eb082b3 100644 --- a/src/nodes/types.ts +++ b/src/nodes/types.ts @@ -26,17 +26,6 @@ type NodeData = { type SeedNodes = Record; -type NodesOptions = { - connectionIdleTimeoutTime: number; - connectionFindConcurrencyLimit: number; - connectionConnectTimeoutTime: number; - connectionKeepAliveTimeoutTime: number; - connectionKeepAliveIntervalTime: number; - connectionHolePunchIntervalTime: number; - rpcParserBufferSize: number; - rpcCallTimeoutTime: number; -}; - export type { NodeId, NodeIdString, @@ -48,5 +37,4 @@ export type { NodeBucket, NodeData, NodeGraphSpace, - NodesOptions, }; diff --git a/src/utils/errors.ts b/src/utils/errors.ts index 23ea67744..49de51398 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -1,5 +1,6 @@ import sysexits from './sysexits'; import ErrorPolykey from '../ErrorPolykey'; +export * from './ratelimiter/errors'; class ErrorUtils extends ErrorPolykey {} diff --git a/src/utils/ratelimiter/RateLimiter.ts b/src/utils/ratelimiter/RateLimiter.ts new file mode 100644 index 000000000..4e310b5bd --- /dev/null +++ b/src/utils/ratelimiter/RateLimiter.ts @@ -0,0 +1,151 @@ +import { Timer } from '@matrixai/timer'; +import * as rateLimiterErrors from './errors'; + +/** + * Internal data structure used to track a buckets' information. + * Internal use only so explicitly not exported. + */ +type TokenBucket = { + creationTimestamp: number; + tokens: number; + lastRefillTimestamp: number; + capacity: number; + refillRatePerSecond: number; +}; + +class RateLimiter { + protected tokenBuckets: Map; + protected expirationTimers: Map; + protected refillTimer: Timer | undefined; + + constructor( + protected defaultTTL: number = 60000, + protected defaultCapacity: number = 100, + protected defaultRate: number = 100, + protected defaultConsume: number = 1, + ) { + // Default TTL 1 minute + this.tokenBuckets = new Map(); + this.expirationTimers = new Map(); + } + + /** + * Starts the Refill interval timer + */ + public startRefillInterval(): void { + if (this.refillTimer != null) return; + const handler = () => { + this.refill(); + this.refillTimer = new Timer({ + handler, + delay: 1000, + }); + }; + this.refillTimer = new Timer({ + handler, + delay: 1000, + }); + } + + /** + * Stops the Refill interval timer + */ + public stopRefillInterval(): void { + if (this.refillTimer != null) { + this.refillTimer.cancel(); + delete this.refillTimer; + } + } + + /** + * Refills a second worth of tokens defined by the `refillRatePerSecond`. + */ + public refill(): void { + for (const [, bucket] of this.tokenBuckets) { + bucket.tokens += bucket.refillRatePerSecond; + if (bucket.tokens > bucket.capacity) bucket.tokens = bucket.capacity; + bucket.lastRefillTimestamp = performance.now(); + } + } + + /** + * Consumes an amount of tokens for a given bucket. Will return true if the tokens were available to be consumed. + * Otherwise, returns false if there were insufficient tokens. + * @param key - Key for the given bucket. + * @param tokensToConsume - Number of tokens to consume. + * @returns True if there were sufficient tokens that were consumed. False otherwise with no tokens consumed. + */ + public consume( + key: string, + tokensToConsume: number = this.defaultConsume, + ): boolean { + // Scaled default value for example + const bucket = this.getBucket(key); + if (tokensToConsume <= 0) { + throw new rateLimiterErrors.ErrorRateLimiterInvalidTokens(); + } + // Refreshing TTL + this.expirationTimers.get(key)?.refresh(); + if (bucket.tokens < tokensToConsume) return false; + bucket.tokens -= tokensToConsume; + return true; + } + + /** + * Gets the available tokens for a given bucket. + * @param key - Key for the given bucket. + */ + public tokens(key: string): number { + return this.getBucket(key).tokens; + } + + /** + * Clears all existing `TokenBuckets` . + */ + public clearBuckets(): void { + // Clear timers + for (const [, expirationTimer] of this.expirationTimers) { + expirationTimer.cancel(); + } + this.expirationTimers.clear(); + // Clear buckets + this.tokenBuckets.clear(); + } + + /** + * Stops refreshing and clears all existing `TokenBucket`s + */ + public stop(): void { + this.stopRefillInterval(); + this.clearBuckets(); + } + + protected scheduleExpiration(key: string, ttl: number): void { + const timer = new Timer({ + handler: () => { + this.tokenBuckets.delete(key); + this.expirationTimers.delete(key); + }, + delay: ttl, + }); + this.expirationTimers.set(key, timer); + } + + protected getBucket(key: string, ttl?: number): TokenBucket { + let bucket = this.tokenBuckets.get(key); + if (!bucket) { + bucket = { + capacity: this.defaultCapacity, + creationTimestamp: performance.now(), + lastRefillTimestamp: performance.now(), + refillRatePerSecond: this.defaultRate, + tokens: this.defaultCapacity, + }; + this.tokenBuckets.set(key, bucket); + this.scheduleExpiration(key, ttl || this.defaultTTL); + } + return bucket; + } +} + +export default RateLimiter; diff --git a/src/utils/ratelimiter/errors.ts b/src/utils/ratelimiter/errors.ts new file mode 100644 index 000000000..531a7693d --- /dev/null +++ b/src/utils/ratelimiter/errors.ts @@ -0,0 +1,10 @@ +import { ErrorPolykey, sysexits } from '../../errors'; + +class ErrorRateLimiter extends ErrorPolykey {} + +class ErrorRateLimiterInvalidTokens extends ErrorRateLimiter { + static description = 'Consumed tokens must be greater than 0'; + exitCode = sysexits.USAGE; +} + +export { ErrorRateLimiter, ErrorRateLimiterInvalidTokens }; diff --git a/src/utils/ratelimiter/index.ts b/src/utils/ratelimiter/index.ts new file mode 100644 index 000000000..12e24d331 --- /dev/null +++ b/src/utils/ratelimiter/index.ts @@ -0,0 +1,2 @@ +export { default as RateLimiter } from './RateLimiter'; +export * as errors from './errors'; diff --git a/tests/PolykeyClient.test.ts b/tests/PolykeyClient.test.ts index dc5587e91..36ba060e0 100644 --- a/tests/PolykeyClient.test.ts +++ b/tests/PolykeyClient.test.ts @@ -2,114 +2,182 @@ import type { SessionToken } from '@/sessions/types'; import os from 'os'; import path from 'path'; import fs from 'fs'; +import net from 'net'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; -import { WebSocketClient } from '@matrixai/ws'; -import { PolykeyClient, PolykeyAgent } from '@'; -import { Session } from '@/sessions'; +import { + utils as webSocketUtils, + errors as webSocketErrors, +} from '@matrixai/ws'; +import PolykeyAgent from '@/PolykeyAgent'; +import PolykeyClient from '@/PolykeyClient'; +import Session from '@/sessions/Session'; import config from '@/config'; +import * as ids from '@/ids'; import * as clientUtils from '@/client/utils'; import * as keysUtils from '@/keys/utils'; import * as errors from '@/errors'; import * as testUtils from './utils'; -describe('PolykeyClient', () => { - const password = 'password'; - const localhost = '127.0.0.1'; - const logger = new Logger('PolykeyClient Test', LogLevel.WARN, [ +describe(PolykeyClient.name, () => { + const logger = new Logger(`${PolykeyClient.name} Test`, LogLevel.WARN, [ new StreamHandler(), ]); + const password = 'password'; + const localHost = '127.0.0.1'; + const nodeIdGenerator = ids.createNodeIdGenerator(); let dataDir: string; let nodePath: string; - let pkAgent: PolykeyAgent; beforeEach(async () => { dataDir = await fs.promises.mkdtemp( path.join(os.tmpdir(), 'polykey-test-'), ); nodePath = path.join(dataDir, 'polykey'); - pkAgent = await PolykeyAgent.createPolykeyAgent({ - password, - options: { - nodePath, - agentServiceHost: localhost, - clientServiceHost: localhost, - keys: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, - }, - logger, - }); }); afterEach(async () => { - await pkAgent.stop(); await fs.promises.rm(dataDir, { force: true, recursive: true, }); }); - test('preserving and destroying session state', async () => { - const session = await Session.createSession({ - sessionTokenPath: path.join(nodePath, config.paths.tokenBase), - fs, - logger, - }); - await session.writeToken('dummy' as SessionToken); - // Using fresh: true means that any token would be destroyed - const webSocketClient = await WebSocketClient.createWebSocketClient({ - config: { - verifyPeer: false, - }, - host: pkAgent.clientServiceHost, - port: pkAgent.clientServicePort, - logger, + test('connect to the nothing', async () => { + await expect( + PolykeyClient.createPolykeyClient({ + nodeId: nodeIdGenerator(), + host: '127.0.0.1', + port: 1, + options: { + nodePath: nodePath, + }, + fs, + logger: logger.getChild(PolykeyClient.name), + fresh: true, + }), + ).rejects.toThrow(webSocketErrors.ErrorWebSocketConnectionLocal); + }); + test('connect timeout', async () => { + const sockets: Array = []; + const server = net.createServer((socket) => { + sockets.push(socket); }); - const pkClient = await PolykeyClient.createPolykeyClient({ - streamFactory: () => webSocketClient.connection.newStream(), - nodePath, - fs, - logger, - fresh: true, + await new Promise((resolve) => { + server.listen(0, () => { + resolve(); + }); }); - expect(await session.readToken()).toBeUndefined(); - await session.writeToken('abc' as SessionToken); - await pkClient.stop(); - expect(await session.readToken()).toBeDefined(); - await pkClient.destroy(); - expect(await session.readToken()).toBeUndefined(); - await webSocketClient.destroy({ force: true }); + const serverPort = (server.address() as net.AddressInfo).port; + await expect( + PolykeyClient.createPolykeyClient( + { + nodeId: nodeIdGenerator(), + host: '127.0.0.1', + port: serverPort, + options: { + nodePath: nodePath, + }, + fs, + logger: logger.getChild(PolykeyClient.name), + fresh: true, + }, + { timer: 1000 }, + ), + ).rejects.toThrow(errors.ErrorPolykeyClientCreateTimeout); + server.close(); + for (const socket of sockets) { + socket.destroy(); + } }); - test('end to end with authentication logic', async () => { - const webSocketClient = await WebSocketClient.createWebSocketClient({ - config: { - verifyPeer: true, - verifyCallback: async (certs) => { - await clientUtils.verifyServerCertificateChain( - [pkAgent.keyRing.getNodeId()], - certs, - ); + describe('with polykey agent', () => { + let pkAgent: PolykeyAgent; + beforeEach(async () => { + pkAgent = await PolykeyAgent.createPolykeyAgent({ + password, + options: { + nodePath, + agentServiceHost: localHost, + clientServiceHost: localHost, + keys: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, }, - }, - host: pkAgent.clientServiceHost, - port: pkAgent.clientServicePort, - logger: logger.getChild(WebSocketClient.name), + logger: logger.getChild(PolykeyAgent.name), + }); }); - const pkClient = await PolykeyClient.createPolykeyClient({ - streamFactory: () => webSocketClient.connection.newStream(), - nodePath, - fs, - logger: logger.getChild(PolykeyClient.name), - fresh: true, + afterEach(async () => { + await pkAgent.stop(); }); - - const callP = pkClient.rpcClientClient.methods.agentStatus({}); - await expect(callP).rejects.toThrow(errors.ErrorPolykeyRemote); - await testUtils.expectRemoteError(callP, errors.ErrorClientAuthMissing); - // Correct auth runs without error - await pkClient.rpcClientClient.methods.agentStatus({ - metadata: { - authorization: clientUtils.encodeAuthFromPassword(password), - }, + test('preserving and destroying session state', async () => { + const session = await Session.createSession({ + sessionTokenPath: path.join(nodePath, config.paths.tokenBase), + fs, + logger, + }); + await session.writeToken('dummy' as SessionToken); + const pkClient = await PolykeyClient.createPolykeyClient({ + nodeId: pkAgent.keyRing.getNodeId(), + host: pkAgent.clientServiceHost, + port: pkAgent.clientServicePort, + options: { + nodePath, + }, + fs, + logger: logger.getChild(PolykeyClient.name), + // Using fresh: true means that any token would be destroyed + fresh: true, + }); + expect(await session.readToken()).toBeUndefined(); + await session.writeToken('abc' as SessionToken); + await pkClient.stop(); + expect(await session.readToken()).toBeDefined(); + await pkClient.destroy({ force: true }); + expect(await session.readToken()).toBeUndefined(); + }); + test('connect to agent client service', async () => { + const pkClient = await PolykeyClient.createPolykeyClient({ + nodeId: pkAgent.keyRing.getNodeId(), + port: pkAgent.clientServicePort, + host: pkAgent.clientServiceHost, + options: { + nodePath: nodePath, + }, + fs, + logger: logger.getChild(PolykeyClient.name), + fresh: true, + }); + expect(pkClient.host).toBe(pkAgent.clientServiceHost); + expect(pkClient.port).toBe(pkAgent.clientServicePort); + const connectionMeta = pkClient.webSocketClient.connection.meta(); + expect(connectionMeta.remoteCertsChain).toHaveLength(1); + const remoteCert = connectionMeta.remoteCertsChain[0]; + const remoteCertPem = webSocketUtils.derToPEM(remoteCert); + const agentCertPem = await pkAgent.certManager.getCurrentCertPEM(); + expect(remoteCertPem).toEqual(agentCertPem); + await pkClient.stop(); + }); + test('authenticated RPC request to agent client service', async () => { + const pkClient = await PolykeyClient.createPolykeyClient({ + nodeId: pkAgent.keyRing.getNodeId(), + port: pkAgent.clientServicePort, + host: pkAgent.clientServiceHost, + options: { + nodePath: nodePath, + }, + fs, + logger: logger.getChild(PolykeyClient.name), + fresh: true, + }); + const callP = pkClient.rpcClient.methods.agentStatus({}); + // Authentication error + await expect(callP).rejects.toThrow(errors.ErrorPolykeyRemote); + await testUtils.expectRemoteError(callP, errors.ErrorClientAuthMissing); + // Correct auth runs without error + await pkClient.rpcClient.methods.agentStatus({ + metadata: { + authorization: clientUtils.encodeAuthFromPassword(password), + }, + }); + await pkClient.stop(); }); }); }); diff --git a/tests/bootstrap/utils.test.ts b/tests/bootstrap/utils.test.ts index 615eb9351..e3a6b332a 100644 --- a/tests/bootstrap/utils.test.ts +++ b/tests/bootstrap/utils.test.ts @@ -28,9 +28,7 @@ describe('bootstrap/utils', () => { const password = 'password'; const recoveryCode = await bootstrapUtils.bootstrapState({ password, - options: { - nodePath, - }, + nodePath, fs, logger, }); @@ -56,9 +54,7 @@ describe('bootstrap/utils', () => { const password = 'password'; const recoveryCode = await bootstrapUtils.bootstrapState({ password, - options: { - nodePath, - }, + nodePath, fs, logger, }); @@ -91,9 +87,7 @@ describe('bootstrap/utils', () => { await expect( bootstrapUtils.bootstrapState({ password, - options: { - nodePath: nodePath1, - }, + nodePath: nodePath1, fs, logger, }), @@ -109,9 +103,7 @@ describe('bootstrap/utils', () => { await expect( bootstrapUtils.bootstrapState({ password, - options: { - nodePath: nodePath2, - }, + nodePath: nodePath2, fs, logger, }), @@ -123,9 +115,7 @@ describe('bootstrap/utils', () => { await expect( bootstrapUtils.bootstrapState({ password, - options: { - nodePath: nodePath3, - }, + nodePath: nodePath3, fs, logger, }), @@ -137,17 +127,13 @@ describe('bootstrap/utils', () => { const [result1, result2] = await Promise.allSettled([ bootstrapUtils.bootstrapState({ password, - options: { - nodePath, - }, + nodePath, fs, logger, }), bootstrapUtils.bootstrapState({ password, - options: { - nodePath, - }, + nodePath, fs, logger, }), diff --git a/tests/client/authenticationMiddleware.test.ts b/tests/client/authenticationMiddleware.test.ts index 9aa80157e..ca0d89b05 100644 --- a/tests/client/authenticationMiddleware.test.ts +++ b/tests/client/authenticationMiddleware.test.ts @@ -57,11 +57,9 @@ describe('authenticationMiddleware', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); taskManager = await TaskManager.createTaskManager({ db, logger }); diff --git a/tests/client/handlers/agent.test.ts b/tests/client/handlers/agent.test.ts index b8e6bd730..935208c1a 100644 --- a/tests/client/handlers/agent.test.ts +++ b/tests/client/handlers/agent.test.ts @@ -68,11 +68,9 @@ describe('agentLockAll', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); taskManager = await TaskManager.createTaskManager({ db, logger }); @@ -248,11 +246,9 @@ describe('agentStop', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); taskManager = await TaskManager.createTaskManager({ db, logger }); @@ -361,11 +357,9 @@ describe('agentUnlock', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); taskManager = await TaskManager.createTaskManager({ db, logger }); diff --git a/tests/client/handlers/gestalts.test.ts b/tests/client/handlers/gestalts.test.ts index 8f6360502..860e55f1d 100644 --- a/tests/client/handlers/gestalts.test.ts +++ b/tests/client/handlers/gestalts.test.ts @@ -108,11 +108,9 @@ describe('gestaltsActionsByIdentity', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); taskManager = await TaskManager.createTaskManager({ db, logger }); @@ -269,11 +267,9 @@ describe('gestaltsActionsByNode', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); taskManager = await TaskManager.createTaskManager({ db, logger }); @@ -407,11 +403,9 @@ describe('gestaltsDiscoveryByIdentity', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); taskManager = await TaskManager.createTaskManager({ @@ -586,11 +580,9 @@ describe('gestaltsDiscoveryByNode', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); taskManager = await TaskManager.createTaskManager({ @@ -757,11 +749,9 @@ describe('gestaltsGestaltGetByIdentity', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); taskManager = await TaskManager.createTaskManager({ db, logger }); @@ -906,11 +896,9 @@ describe('gestaltsGestaltGetByNode', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); taskManager = await TaskManager.createTaskManager({ db, logger }); @@ -1053,11 +1041,9 @@ describe('gestaltsGestaltList', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); taskManager = await TaskManager.createTaskManager({ db, logger }); @@ -1206,11 +1192,9 @@ describe('gestaltsGestaltTrustByIdentity', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); taskManager = await TaskManager.createTaskManager({ @@ -1579,11 +1563,9 @@ describe('gestaltsGestaltTrustByNode', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); taskManager = await TaskManager.createTaskManager({ diff --git a/tests/client/handlers/identities.test.ts b/tests/client/handlers/identities.test.ts index e901e7c11..3894c8aab 100644 --- a/tests/client/handlers/identities.test.ts +++ b/tests/client/handlers/identities.test.ts @@ -96,11 +96,9 @@ describe('identitiesAuthenticate', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); identitiesManager = await IdentitiesManager.createIdentitiesManager({ @@ -234,11 +232,9 @@ describe('identitiesAuthenticatedGet', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); identitiesManager = await IdentitiesManager.createIdentitiesManager({ @@ -496,11 +492,9 @@ describe('identitiesClaim', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); sigchain = await Sigchain.createSigchain({ @@ -641,11 +635,9 @@ describe('identitiesInfoConnectedGet', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); identitiesManager = await IdentitiesManager.createIdentitiesManager({ @@ -1327,11 +1319,9 @@ describe('identitiesInfoGet', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); identitiesManager = await IdentitiesManager.createIdentitiesManager({ @@ -1776,11 +1766,9 @@ describe('identitiesInvite', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); sigchain = await Sigchain.createSigchain({ @@ -1902,11 +1890,9 @@ describe('identitiesProvidersList', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); identitiesManager = await IdentitiesManager.createIdentitiesManager({ @@ -2005,11 +1991,9 @@ describe('identitiesTokenPutDeleteGet', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); identitiesManager = await IdentitiesManager.createIdentitiesManager({ diff --git a/tests/client/handlers/keys.test.ts b/tests/client/handlers/keys.test.ts index 93a70dcd0..108488b73 100644 --- a/tests/client/handlers/keys.test.ts +++ b/tests/client/handlers/keys.test.ts @@ -86,11 +86,9 @@ describe('keysCertsChainGet', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); identitiesManager = await IdentitiesManager.createIdentitiesManager({ @@ -200,11 +198,9 @@ describe('keysCertsGet', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); identitiesManager = await IdentitiesManager.createIdentitiesManager({ @@ -305,11 +301,9 @@ describe('keysEncrypt and keysDecrypt', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); identitiesManager = await IdentitiesManager.createIdentitiesManager({ @@ -406,11 +400,9 @@ describe('keysKeyPair', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); identitiesManager = await IdentitiesManager.createIdentitiesManager({ @@ -768,11 +760,9 @@ describe('keysPasswordChange', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); identitiesManager = await IdentitiesManager.createIdentitiesManager({ @@ -864,11 +854,9 @@ describe('keysPublicKey', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); identitiesManager = await IdentitiesManager.createIdentitiesManager({ @@ -963,11 +951,9 @@ describe('keysSign and keysVerify', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); identitiesManager = await IdentitiesManager.createIdentitiesManager({ diff --git a/tests/client/handlers/nodes.test.ts b/tests/client/handlers/nodes.test.ts index 3cea8aa98..a69454f75 100644 --- a/tests/client/handlers/nodes.test.ts +++ b/tests/client/handlers/nodes.test.ts @@ -61,11 +61,9 @@ describe('nodesAdd', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); tlsConfig = await testsUtils.createTLSConfig(keyRing.keyPair); @@ -89,10 +87,8 @@ describe('nodesAdd', () => { nodeGraph, // TLS not needed for this test tlsConfig: {} as TLSConfig, - options: { - connectionConnectTimeoutTime: 2000, - connectionIdleTimeoutTime: 2000, - }, + connectionConnectTimeoutTime: 2000, + connectionIdleTimeoutTime: 2000, logger: logger.getChild('NodeConnectionManager'), }); nodeManager = new NodeManager({ @@ -270,11 +266,9 @@ describe('nodesClaim', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); tlsConfig = await testsUtils.createTLSConfig(keyRing.keyPair); @@ -303,10 +297,8 @@ describe('nodesClaim', () => { nodeGraph, // TLS not needed for this test tlsConfig: {} as TLSConfig, - options: { - connectionConnectTimeoutTime: 2000, - connectionIdleTimeoutTime: 2000, - }, + connectionConnectTimeoutTime: 2000, + connectionIdleTimeoutTime: 2000, logger: logger.getChild('NodeConnectionManager'), }); nodeManager = new NodeManager({ @@ -441,11 +433,9 @@ describe('nodesFind', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); tlsConfig = await testsUtils.createTLSConfig(keyRing.keyPair); @@ -469,10 +459,8 @@ describe('nodesFind', () => { nodeGraph, // TLS not needed for this test tlsConfig: {} as TLSConfig, - options: { - connectionConnectTimeoutTime: 2000, - connectionIdleTimeoutTime: 2000, - }, + connectionConnectTimeoutTime: 2000, + connectionIdleTimeoutTime: 2000, logger: logger.getChild('NodeConnectionManager'), }); await nodeConnectionManager.start({ host: localhost as Host }); @@ -572,11 +560,9 @@ describe('nodesPing', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); tlsConfig = await testsUtils.createTLSConfig(keyRing.keyPair); @@ -605,10 +591,8 @@ describe('nodesPing', () => { nodeGraph, // TLS not needed for this test tlsConfig: {} as TLSConfig, - options: { - connectionConnectTimeoutTime: 2000, - connectionIdleTimeoutTime: 2000, - }, + connectionConnectTimeoutTime: 2000, + connectionIdleTimeoutTime: 2000, logger: logger.getChild('NodeConnectionManager'), }); nodeManager = new NodeManager({ diff --git a/tests/client/handlers/notifications.test.ts b/tests/client/handlers/notifications.test.ts index 3efac49c3..266a419b6 100644 --- a/tests/client/handlers/notifications.test.ts +++ b/tests/client/handlers/notifications.test.ts @@ -71,11 +71,9 @@ describe('notificationsClear', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); tlsConfig = await testsUtils.createTLSConfig(keyRing.keyPair); @@ -108,10 +106,8 @@ describe('notificationsClear', () => { nodeGraph, // TLS not needed for this test tlsConfig: {} as TLSConfig, - options: { - connectionConnectTimeoutTime: 2000, - connectionIdleTimeoutTime: 2000, - }, + connectionConnectTimeoutTime: 2000, + connectionIdleTimeoutTime: 2000, logger: logger.getChild('NodeConnectionManager'), }); nodeManager = new NodeManager({ @@ -230,11 +226,9 @@ describe('notificationsRead', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); tlsConfig = await testsUtils.createTLSConfig(keyRing.keyPair); @@ -267,10 +261,8 @@ describe('notificationsRead', () => { nodeGraph, // TLS not needed for this test tlsConfig: {} as TLSConfig, - options: { - connectionConnectTimeoutTime: 2000, - connectionIdleTimeoutTime: 2000, - }, + connectionConnectTimeoutTime: 2000, + connectionIdleTimeoutTime: 2000, logger: logger.getChild('NodeConnectionManager'), }); nodeManager = new NodeManager({ @@ -616,11 +608,9 @@ describe('notificationsSend', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); tlsConfig = await testsUtils.createTLSConfig(keyRing.keyPair); @@ -652,10 +642,8 @@ describe('notificationsSend', () => { keyRing, nodeGraph, tlsConfig: {} as TLSConfig, - options: { - connectionConnectTimeoutTime: 2000, - connectionIdleTimeoutTime: 2000, - }, + connectionConnectTimeoutTime: 2000, + connectionIdleTimeoutTime: 2000, logger: logger.getChild('NodeConnectionManager'), }); nodeManager = new NodeManager({ diff --git a/tests/client/handlers/vaults.test.ts b/tests/client/handlers/vaults.test.ts index dcf000fe8..ab2cb15b4 100644 --- a/tests/client/handlers/vaults.test.ts +++ b/tests/client/handlers/vaults.test.ts @@ -89,11 +89,9 @@ describe('vaultsClone', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); // TlsConfig = await testsUtils.createTLSConfig(keyRing.keyPair); @@ -155,11 +153,9 @@ describe('vaultsCreate and vaultsDelete and vaultsList', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); tlsConfig = await testsUtils.createTLSConfig(keyRing.keyPair); @@ -292,11 +288,9 @@ describe('vaultsLog', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); tlsConfig = await testsUtils.createTLSConfig(keyRing.keyPair); @@ -447,11 +441,9 @@ describe('vaultsPermissionSet and vaultsPermissionUnset and vaultsPermissionGet' keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); tlsConfig = await testsUtils.createTLSConfig(keyRing.keyPair); @@ -624,11 +616,9 @@ describe('vaultsPull', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); const dbPath = path.join(dataDir, 'db'); @@ -711,11 +701,9 @@ describe('vaultsRename', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); tlsConfig = await testsUtils.createTLSConfig(keyRing.keyPair); @@ -809,11 +797,9 @@ describe('vaultsScan', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); const dbPath = path.join(dataDir, 'db'); @@ -872,11 +858,9 @@ describe('vaultsSecretsEdit', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); tlsConfig = await testsUtils.createTLSConfig(keyRing.keyPair); @@ -987,11 +971,9 @@ describe('vaultsSecretsMkdir', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); tlsConfig = await testsUtils.createTLSConfig(keyRing.keyPair); @@ -1097,11 +1079,9 @@ describe('vaultsSecretsNew and vaultsSecretsDelete, vaultsSecretsGet', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); tlsConfig = await testsUtils.createTLSConfig(keyRing.keyPair); @@ -1234,11 +1214,9 @@ describe('vaultsSecretsNewDir and vaultsSecretsList', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); tlsConfig = await testsUtils.createTLSConfig(keyRing.keyPair); @@ -1364,11 +1342,9 @@ describe('vaultsSecretsRename', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); tlsConfig = await testsUtils.createTLSConfig(keyRing.keyPair); @@ -1480,11 +1456,9 @@ describe('vaultsSecretsStat', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); tlsConfig = await testsUtils.createTLSConfig(keyRing.keyPair); @@ -1601,11 +1575,9 @@ describe('vaultsVersion', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); tlsConfig = await testsUtils.createTLSConfig(keyRing.keyPair); diff --git a/tests/client/timeoutMiddleware.test.ts b/tests/client/timeoutMiddleware.test.ts index 762e5618f..c4b2f2ef0 100644 --- a/tests/client/timeoutMiddleware.test.ts +++ b/tests/client/timeoutMiddleware.test.ts @@ -56,18 +56,15 @@ describe('timeoutMiddleware', () => { password, keysPath, logger, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, }); taskManager = await TaskManager.createTaskManager({ db, logger }); certManager = await CertManager.createCertManager({ db, keyRing, taskManager, - options: {}, logger, }); tlsConfig = await testsUtils.createTLSConfig(keyRing.keyPair); diff --git a/tests/discovery/Discovery.test.ts b/tests/discovery/Discovery.test.ts index fe8981922..ba3d58447 100644 --- a/tests/discovery/Discovery.test.ts +++ b/tests/discovery/Discovery.test.ts @@ -85,11 +85,9 @@ describe('Discovery', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger: logger.getChild('KeyRing'), }); const dbPath = path.join(dataDir, 'db'); @@ -158,10 +156,8 @@ describe('Discovery', () => { keyRing, nodeGraph, tlsConfig, - options: { - connectionConnectTimeoutTime: 2000, - connectionIdleTimeoutTime: 2000, - }, + connectionConnectTimeoutTime: 2000, + connectionIdleTimeoutTime: 2000, logger: logger.getChild('NodeConnectionManager'), }); nodeManager = new NodeManager({ diff --git a/tests/identities/IdentitiesManager.test.ts b/tests/identities/IdentitiesManager.test.ts index 04063c913..f9e6bf131 100644 --- a/tests/identities/IdentitiesManager.test.ts +++ b/tests/identities/IdentitiesManager.test.ts @@ -329,11 +329,9 @@ describe('IdentitiesManager', () => { const keyRing = await KeyRing.createKeyRing({ password: 'password', keysPath: path.join(dataDir, 'keys'), - options: { - strictMemoryLock: false, - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - }, + strictMemoryLock: false, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, logger, fresh: true, }); diff --git a/tests/keys/CertManager.test.ts b/tests/keys/CertManager.test.ts index 7e542fd97..32b967b90 100644 --- a/tests/keys/CertManager.test.ts +++ b/tests/keys/CertManager.test.ts @@ -39,12 +39,10 @@ describe(CertManager.name, () => { keyRing = await KeyRing.createKeyRing({ keysPath, password, - options: { - privateKey, - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + privateKey, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); dbPath = `${dataDir}/db`; diff --git a/tests/keys/KeyRing.test.ts b/tests/keys/KeyRing.test.ts index c83608a18..0714f1a5d 100644 --- a/tests/keys/KeyRing.test.ts +++ b/tests/keys/KeyRing.test.ts @@ -30,10 +30,8 @@ describe(KeyRing.name, () => { const keyRing = await KeyRing.createKeyRing({ keysPath, password, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, logger, }); await expect(async () => { @@ -58,10 +56,8 @@ describe(KeyRing.name, () => { const keyRing = await KeyRing.createKeyRing({ keysPath, password, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, logger, }); const keysPathContents = await fs.promises.readdir(keysPath); @@ -81,10 +77,8 @@ describe(KeyRing.name, () => { const keyRing = await KeyRing.createKeyRing({ keysPath, password, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, logger, }); const nodeId = keyRing.getNodeId(); @@ -110,10 +104,8 @@ describe(KeyRing.name, () => { const keyRing = await KeyRing.createKeyRing({ keysPath, password, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, logger, }); const keysPathContents1 = await fs.promises.readdir(keysPath); @@ -132,10 +124,8 @@ describe(KeyRing.name, () => { const keyRing = await KeyRing.createKeyRing({ keysPath, password, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, logger, }); expect(await keyRing.checkPassword(password)).toBe(true); @@ -149,10 +139,8 @@ describe(KeyRing.name, () => { keysPath, password: 'first password', logger, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, }); await keyRing.changePassword('second password'); await keyRing.stop(); @@ -165,10 +153,8 @@ describe(KeyRing.name, () => { await KeyRing.createKeyRing({ password: 'wrong password', keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, logger, }); }).rejects.toThrow(keysErrors.ErrorKeyPairParse); @@ -180,10 +166,8 @@ describe(KeyRing.name, () => { const keyRing = await KeyRing.createKeyRing({ keysPath, password, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, logger, }); const nodeId = keyRing.getNodeId(); @@ -222,11 +206,9 @@ describe(KeyRing.name, () => { const keyRing1 = await KeyRing.createKeyRing({ password, keysPath: keysPath1, - options: { - recoveryCode, - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - }, + recoveryCode, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, logger, }); expect(keyRing1.recoveryCode).toBe(recoveryCode); @@ -236,11 +218,9 @@ describe(KeyRing.name, () => { const keyRing2 = await KeyRing.createKeyRing({ password, keysPath: keysPath2, - options: { - recoveryCode, - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - }, + recoveryCode, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, logger, }); expect(keyRing2.recoveryCode).toBe(recoveryCode); @@ -255,10 +235,8 @@ describe(KeyRing.name, () => { const keyRing = await KeyRing.createKeyRing({ keysPath, password, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, logger, }); const keyPair = { @@ -283,11 +261,9 @@ describe(KeyRing.name, () => { const keyRing = await KeyRing.createKeyRing({ keysPath, password, - options: { - privateKey: keyPair.privateKey, - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - }, + privateKey: keyPair.privateKey, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, logger, }); expect(keyRing.keyPair).toStrictEqual(keyPair); @@ -313,11 +289,9 @@ describe(KeyRing.name, () => { const keyRing = await KeyRing.createKeyRing({ keysPath, password: 'newpassword', - options: { - privateKeyPath: `${dataDir}/private-key.jwe`, - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - }, + privateKeyPath: `${dataDir}/private-key.jwe`, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, logger, }); expect(keyRing.keyPair).toStrictEqual(keyPair); @@ -339,11 +313,9 @@ describe(KeyRing.name, () => { const keyRing = await KeyRing.createKeyRing({ keysPath, password: 'newpassword', - options: { - privateKeyPath: `${dataDir}/private-key.jwk`, - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - }, + privateKeyPath: `${dataDir}/private-key.jwk`, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, logger, }); expect(keyRing.keyPair).toStrictEqual(keyPair); @@ -364,10 +336,8 @@ describe(KeyRing.name, () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, logger, }); }); @@ -406,10 +376,8 @@ describe(KeyRing.name, () => { const keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, logger, }); // Make a copy of the existing DB key diff --git a/tests/keys/utils/password.test.ts b/tests/keys/utils/password.test.ts new file mode 100644 index 000000000..7f13c4bc8 --- /dev/null +++ b/tests/keys/utils/password.test.ts @@ -0,0 +1,24 @@ +import * as password from '@/keys/utils/password'; + +describe('keys/utils/password', () => { + test('password hashing ops limits raw numbers', () => { + expect(password.passwordOpsLimits['min']).toBe(1); + expect(password.passwordOpsLimits['interactive']).toBe(2); + expect(password.passwordOpsLimits['moderate']).toBe(3); + expect(password.passwordOpsLimits['sensitive']).toBe(4); + expect(password.passwordOpsLimits['max']).toBe(4294967295); + expect(password.passwordOpsLimitDefault).toBe( + password.passwordOpsLimits['moderate'], + ); + }); + test('password hashing mem limits raw numbers', () => { + expect(password.passwordMemLimits['min']).toBe(8192); + expect(password.passwordMemLimits['interactive']).toBe(67108864); + expect(password.passwordMemLimits['moderate']).toBe(268435456); + expect(password.passwordMemLimits['sensitive']).toBe(1073741824); + expect(password.passwordMemLimits['max']).toBe(4294966272); + expect(password.passwordMemLimitDefault).toBe( + password.passwordMemLimits['moderate'], + ); + }); +}); diff --git a/tests/nodes/NodeConnectionManager.general.test.ts b/tests/nodes/NodeConnectionManager.general.test.ts index 059495927..35d7e8b10 100644 --- a/tests/nodes/NodeConnectionManager.general.test.ts +++ b/tests/nodes/NodeConnectionManager.general.test.ts @@ -1,5 +1,5 @@ import type { Host, Port, TLSConfig } from '@/network/types'; -import type { NodeId, NodeIdEncoded } from '@/ids'; +import type { NodeId } from '@/ids'; import type { NodeAddress, NodeBucket } from '@/nodes/types'; import fs from 'fs'; import path from 'path'; @@ -16,10 +16,11 @@ import KeyRing from '@/keys/KeyRing'; import ACL from '@/acl/ACL'; import GestaltGraph from '@/gestalts/GestaltGraph'; import NodeGraph from '@/nodes/NodeGraph'; +import * as nodesErrors from '@/nodes/errors'; import Sigchain from '@/sigchain/Sigchain'; import TaskManager from '@/tasks/TaskManager'; -import NodeManager from '@/nodes/NodeManager'; import PolykeyAgent from '@/PolykeyAgent'; +import * as utils from '@/utils'; import * as testNodesUtils from './utils'; import * as tlsTestUtils from '../utils/tls'; @@ -73,11 +74,13 @@ describe(`${NodeConnectionManager.name} general test`, () => { }; let dataDir: string; + let nodePathA: string; + let nodePathB: string; - let remotePolykeyAgent: PolykeyAgent; - let serverAddress: NodeAddress; - let serverNodeId: NodeId; - let serverNodeIdEncoded: NodeIdEncoded; + let remotePolykeyAgentA: PolykeyAgent; + let serverAddressA: NodeAddress; + let serverNodeIdA: NodeId; + let remotePolykeyAgentB: PolykeyAgent; let keyRing: KeyRing; let db: DB; @@ -86,22 +89,30 @@ describe(`${NodeConnectionManager.name} general test`, () => { let nodeGraph: NodeGraph; let sigchain: Sigchain; let taskManager: TaskManager; - let nodeManager: NodeManager; let nodeConnectionManager: NodeConnectionManager; - // Default stream handler, just drop the stream + + // Mocking the relay send + let mockedHolePunchReverse: jest.SpyInstance>; + let mockedPingNode: jest.SpyInstance>; beforeEach(async () => { + mockedHolePunchReverse = jest.spyOn( + NodeConnectionManager.prototype, + 'holePunch', + ); + mockedPingNode = jest.spyOn(NodeConnectionManager.prototype, 'pingNode'); dataDir = await fs.promises.mkdtemp( path.join(os.tmpdir(), 'polykey-test-'), ); // Setting up remote node - const nodePath = path.join(dataDir, 'agentA'); - remotePolykeyAgent = await PolykeyAgent.createPolykeyAgent({ + nodePathA = path.join(dataDir, 'agentA'); + nodePathB = path.join(dataDir, 'agentB'); + remotePolykeyAgentA = await PolykeyAgent.createPolykeyAgent({ password, options: { - nodePath, + nodePath: nodePathA, agentServiceHost: localHost, clientServiceHost: localHost, keys: { @@ -112,8 +123,25 @@ describe(`${NodeConnectionManager.name} general test`, () => { }, logger: logger.getChild('AgentA'), }); - serverNodeId = remotePolykeyAgent.keyRing.getNodeId(); - serverNodeIdEncoded = nodesUtils.encodeNodeId(serverNodeId); + serverNodeIdA = remotePolykeyAgentA.keyRing.getNodeId(); + serverAddressA = { + host: remotePolykeyAgentA.agentServiceHost as Host, + port: remotePolykeyAgentA.agentServicePort as Port, + }; + remotePolykeyAgentB = await PolykeyAgent.createPolykeyAgent({ + password, + options: { + nodePath: nodePathB, + agentServiceHost: localHost, + clientServiceHost: localHost, + keys: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, + }, + logger: logger.getChild('AgentB'), + }); // Setting up client dependencies const keysPath = path.join(dataDir, 'keys'); @@ -121,11 +149,9 @@ describe(`${NodeConnectionManager.name} general test`, () => { password, keysPath, logger, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, }); tlsConfig = await tlsTestUtils.createTLSConfig(keyRing.keyPair); const dbPath = path.join(dataDir, 'db'); @@ -156,17 +182,14 @@ describe(`${NodeConnectionManager.name} general test`, () => { db, logger, }); - serverAddress = { - host: remotePolykeyAgent.agentServiceHost, - port: remotePolykeyAgent.agentServicePort, - }; }); afterEach(async () => { logger.info('AFTER EACH'); + mockedHolePunchReverse.mockRestore(); + mockedPingNode.mockRestore(); await taskManager.stopProcessing(); await taskManager.stopTasks(); - await nodeManager?.stop(); await nodeConnectionManager?.stop(); await sigchain.stop(); await sigchain.destroy(); @@ -182,7 +205,8 @@ describe(`${NodeConnectionManager.name} general test`, () => { await keyRing.destroy(); await taskManager.stop(); - await remotePolykeyAgent.stop(); + await remotePolykeyAgentA?.stop(); + await remotePolykeyAgentB?.stop(); }); test('finds node (local)', async () => { @@ -193,17 +217,6 @@ describe(`${NodeConnectionManager.name} general test`, () => { tlsConfig, seedNodes: undefined, }); - nodeManager = new NodeManager({ - db, - gestaltGraph, - keyRing, - nodeConnectionManager, - nodeGraph, - sigchain, - taskManager, - logger, - }); - await nodeManager.start(); await nodeConnectionManager.start({ host: localHost as Host, }); @@ -228,24 +241,11 @@ describe(`${NodeConnectionManager.name} general test`, () => { keyRing, logger: logger.getChild(NodeConnectionManager.name), nodeGraph, - options: { - connectionKeepAliveTimeoutTime: 10000, - connectionKeepAliveIntervalTime: 1000, - }, + connectionKeepAliveTimeoutTime: 10000, + connectionKeepAliveIntervalTime: 1000, tlsConfig, seedNodes: undefined, }); - nodeManager = new NodeManager({ - db, - gestaltGraph, - keyRing, - nodeConnectionManager, - nodeGraph, - sigchain, - taskManager, - logger, - }); - await nodeManager.start(); await nodeConnectionManager.start({ host: localHost as Host, }); @@ -260,14 +260,14 @@ describe(`${NodeConnectionManager.name} general test`, () => { ); logger.info('DOING TEST'); - await nodeGraph.setNode(serverNodeId, serverAddress); + await nodeGraph.setNode(serverNodeIdA, serverAddressA); // Adding node information to remote node const nodeId = testNodesUtils.generateRandomNodeId(); const nodeAddress: NodeAddress = { host: localHost as Host, port: 11111 as Port, }; - await remotePolykeyAgent.nodeGraph.setNode(nodeId, nodeAddress); + await remotePolykeyAgentA.nodeGraph.setNode(nodeId, nodeAddress); // Expect no error thrown const findNodePromise = nodeConnectionManager.findNode(nodeId); @@ -281,24 +281,11 @@ describe(`${NodeConnectionManager.name} general test`, () => { keyRing, logger: logger.getChild(NodeConnectionManager.name), nodeGraph, - options: { - connectionKeepAliveTimeoutTime: 10000, - connectionKeepAliveIntervalTime: 1000, - }, + connectionKeepAliveTimeoutTime: 10000, + connectionKeepAliveIntervalTime: 1000, tlsConfig, seedNodes: undefined, }); - nodeManager = new NodeManager({ - db, - gestaltGraph, - keyRing, - nodeConnectionManager, - nodeGraph, - sigchain, - taskManager, - logger, - }); - await nodeManager.start(); await nodeConnectionManager.start({ host: localHost as Host, }); @@ -312,7 +299,7 @@ describe(`${NodeConnectionManager.name} general test`, () => { () => new PromiseCancellable((resolve) => resolve(true)), ); - await nodeGraph.setNode(serverNodeId, serverAddress); + await nodeGraph.setNode(serverNodeIdA, serverAddressA); // Adding node information to remote node const nodeId = testNodesUtils.generateRandomNodeId(); @@ -327,24 +314,11 @@ describe(`${NodeConnectionManager.name} general test`, () => { keyRing, logger: logger.getChild(NodeConnectionManager.name), nodeGraph, - options: { - connectionKeepAliveTimeoutTime: 10000, - connectionKeepAliveIntervalTime: 1000, - }, + connectionKeepAliveTimeoutTime: 10000, + connectionKeepAliveIntervalTime: 1000, tlsConfig, seedNodes: undefined, }); - nodeManager = new NodeManager({ - db, - gestaltGraph, - keyRing, - nodeConnectionManager, - nodeGraph, - sigchain, - taskManager, - logger, - }); - await nodeManager.start(); await nodeConnectionManager.start({ host: localHost as Host, }); @@ -358,20 +332,20 @@ describe(`${NodeConnectionManager.name} general test`, () => { () => new PromiseCancellable((resolve) => resolve(true)), ); - await nodeGraph.setNode(serverNodeId, serverAddress); + await nodeGraph.setNode(serverNodeIdA, serverAddressA); // Now generate and add 20 nodes that will be close to this node ID const addedClosestNodes: NodeBucket = []; for (let i = 1; i < 101; i += 5) { const closeNodeId = testNodesUtils.generateNodeIdForBucket( - serverNodeId, + serverNodeIdA, i, ); const nodeAddress = { host: (i + '.' + i + '.' + i + '.' + i) as Host, port: i as Port, }; - await remotePolykeyAgent.nodeGraph.setNode(closeNodeId, nodeAddress); + await remotePolykeyAgentA.nodeGraph.setNode(closeNodeId, nodeAddress); addedClosestNodes.push([ closeNodeId, { @@ -387,163 +361,302 @@ describe(`${NodeConnectionManager.name} general test`, () => { host: `${i}.${i}.${i}.${i}`, port: i, } as NodeAddress; - await remotePolykeyAgent.nodeGraph.setNode(farNodeId, nodeAddress); + await remotePolykeyAgentA.nodeGraph.setNode(farNodeId, nodeAddress); } // Get the closest nodes to the target node const closest = await nodeConnectionManager.getRemoteNodeClosestNodes( - serverNodeId, - serverNodeId, + serverNodeIdA, + serverNodeIdA, ); // Sort the received nodes on distance such that we can check its equality // with addedClosestNodes - nodesUtils.bucketSortByDistance(closest, serverNodeId); + nodesUtils.bucketSortByDistance(closest, serverNodeIdA); expect(closest.length).toBe(20); expect(closest).toEqual(addedClosestNodes); await nodeConnectionManager.stop(); }); - test('sendHolePunchMessage', async () => { + test('holePunchSignalRequest with no target node', async () => { nodeConnectionManager = new NodeConnectionManager({ keyRing, logger: logger.getChild(NodeConnectionManager.name), nodeGraph, - options: { - connectionKeepAliveTimeoutTime: 10000, - connectionKeepAliveIntervalTime: 1000, - }, + connectionKeepAliveTimeoutTime: 10000, + connectionKeepAliveIntervalTime: 1000, tlsConfig, seedNodes: undefined, }); - nodeManager = new NodeManager({ - db, - gestaltGraph, + await nodeConnectionManager.start({ + host: localHost as Host, + }); + await taskManager.startProcessing(); + + mockedHolePunchReverse.mockImplementation(() => { + return new PromiseCancellable((res) => { + res(); + }); + }); + + await nodeGraph.setNode(serverNodeIdA, serverAddressA); + + const targetNodeId = testNodesUtils.generateRandomNodeId(); + const relayNodeId = remotePolykeyAgentA.keyRing.getNodeId(); + + await expect( + nodeConnectionManager.connectionSignalInitial(targetNodeId, relayNodeId), + ).rejects.toThrow(); + await nodeConnectionManager.stop(); + }); + test('holePunchSignalRequest with target node', async () => { + // Establish connection between remote A and B + expect( + await remotePolykeyAgentA.nodeConnectionManager.pingNode( + remotePolykeyAgentB.keyRing.getNodeId(), + remotePolykeyAgentB.agentServiceHost, + remotePolykeyAgentB.agentServicePort, + ), + ).toBeTrue(); + + nodeConnectionManager = new NodeConnectionManager({ keyRing, - nodeConnectionManager, + logger: logger.getChild(NodeConnectionManager.name), nodeGraph, - sigchain, - taskManager, - logger, + connectionKeepAliveTimeoutTime: 10000, + connectionKeepAliveIntervalTime: 1000, + tlsConfig, + seedNodes: undefined, }); - await nodeManager.start(); await nodeConnectionManager.start({ host: localHost as Host, }); await taskManager.startProcessing(); - // Mocking pinging to always return true - const mockedPingNode = jest.spyOn( - NodeConnectionManager.prototype, - 'pingNode', - ); - mockedPingNode.mockImplementation( - () => new PromiseCancellable((resolve) => resolve(true)), - ); + mockedHolePunchReverse.mockImplementation(() => { + return new PromiseCancellable((res) => { + res(); + }); + }); + + const serverNodeId = remotePolykeyAgentA.keyRing.getNodeId(); + const serverAddress = { + host: remotePolykeyAgentA.agentServiceHost as Host, + port: remotePolykeyAgentA.agentServicePort as Port, + }; await nodeGraph.setNode(serverNodeId, serverAddress); - // Now generate and add 20 nodes that will be close to this node ID - const addedClosestNodes: NodeBucket = []; - for (let i = 1; i < 101; i += 5) { - const closeNodeId = testNodesUtils.generateNodeIdForBucket( - serverNodeId, - i, - ); - const nodeAddress = { - host: (i + '.' + i + '.' + i + '.' + i) as Host, - port: i as Port, - }; - await remotePolykeyAgent.nodeGraph.setNode(closeNodeId, nodeAddress); - addedClosestNodes.push([ - closeNodeId, - { - address: nodeAddress, - lastUpdated: 0, - }, - ]); - } - // Now create and add 10 more nodes that are far away from this node - for (let i = 1; i <= 10; i++) { - const farNodeId = nodeIdGenerator(i); - const nodeAddress = { - host: `${i}.${i}.${i}.${i}`, - port: i, - } as NodeAddress; - await remotePolykeyAgent.nodeGraph.setNode(farNodeId, nodeAddress); - } + const targetNodeId = remotePolykeyAgentB.keyRing.getNodeId(); + const relayNodeId = remotePolykeyAgentA.keyRing.getNodeId(); - // Get the closest nodes to the target node - const closest = await nodeConnectionManager.getRemoteNodeClosestNodes( - serverNodeId, - serverNodeId, + await nodeConnectionManager.connectionSignalInitial( + targetNodeId, + relayNodeId, ); - // Sort the received nodes on distance such that we can check its equality - // with addedClosestNodes - nodesUtils.bucketSortByDistance(closest, serverNodeId); - expect(closest.length).toBe(20); - expect(closest).toEqual(addedClosestNodes); - + // Await the FAF signalling to finish. + const signalMapA = + // @ts-ignore: kidnap protected property + remotePolykeyAgentA.nodeConnectionManager.activeSignalFinalPs; + for (const p of signalMapA) { + await p; + } + const punchMapB = + // @ts-ignore: kidnap protected property + remotePolykeyAgentB.nodeConnectionManager.activeHolePunchPs; + for await (const [, p] of punchMapB) { + await p; + } + expect(mockedHolePunchReverse).toHaveBeenCalled(); await nodeConnectionManager.stop(); }); - test('relayHolePunchMessage', async () => { + test('holePunchSignalRequest is nonblocking', async () => { nodeConnectionManager = new NodeConnectionManager({ keyRing, logger: logger.getChild(NodeConnectionManager.name), nodeGraph, - options: { - connectionKeepAliveTimeoutTime: 10000, - connectionKeepAliveIntervalTime: 1000, - }, + connectionKeepAliveTimeoutTime: 10000, + connectionKeepAliveIntervalTime: 1000, tlsConfig, seedNodes: undefined, }); - nodeManager = new NodeManager({ - db, - gestaltGraph, + await nodeConnectionManager.start({ + host: localHost as Host, + }); + await taskManager.startProcessing(); + + const { p: waitP, resolveP: waitResolveP } = utils.promise(); + mockedHolePunchReverse.mockImplementation(() => { + return new PromiseCancellable(async (res) => { + await waitP; + res(); + }); + }); + + const serverNodeId = remotePolykeyAgentA.keyRing.getNodeId(); + const serverAddress = { + host: remotePolykeyAgentA.agentServiceHost, + port: remotePolykeyAgentA.agentServicePort, + }; + await nodeGraph.setNode(serverNodeId, serverAddress); + // Establish connection between remote A and B + expect( + await remotePolykeyAgentA.nodeConnectionManager.pingNode( + remotePolykeyAgentB.keyRing.getNodeId(), + remotePolykeyAgentB.agentServiceHost, + remotePolykeyAgentB.agentServicePort, + ), + ).toBeTrue(); + + const targetNodeId = remotePolykeyAgentB.keyRing.getNodeId(); + const relayNodeId = remotePolykeyAgentA.keyRing.getNodeId(); + // Creating 5 concurrent attempts + const holePunchSignalRequests = [1, 2, 3, 4, 5].map(() => + nodeConnectionManager.connectionSignalInitial(targetNodeId, relayNodeId), + ); + // All should resolve immediately and not block + await Promise.all(holePunchSignalRequests); + + // Await the FAF signalling to finish. + const signalMapA = + // @ts-ignore: kidnap protected property + remotePolykeyAgentA.nodeConnectionManager.activeSignalFinalPs; + for (const p of signalMapA) { + await p; + } + // Only one attempt is being made + const punchMapB = + // @ts-ignore: kidnap protected property + remotePolykeyAgentB.nodeConnectionManager.activeHolePunchPs; + expect(punchMapB.size).toBe(1); + // Allow the attempt to complete + waitResolveP(); + for await (const [, p] of punchMapB) { + await p; + } + // Only attempted once + expect(mockedHolePunchReverse).toHaveBeenCalledTimes(1); + await nodeConnectionManager.stop(); + }); + test('holePunchRequest single target with multiple ports is rate limited', async () => { + nodeConnectionManager = new NodeConnectionManager({ keyRing, - nodeConnectionManager, + logger: logger.getChild(NodeConnectionManager.name), nodeGraph, - sigchain, - taskManager, - logger, + connectionKeepAliveTimeoutTime: 10000, + connectionKeepAliveIntervalTime: 1000, + tlsConfig, + seedNodes: undefined, }); - await nodeManager.start(); await nodeConnectionManager.start({ host: localHost as Host, }); await taskManager.startProcessing(); - // Mocking the relay send - const mockedHolePunchReverse = jest.spyOn( - NodeConnectionManager.prototype, - 'holePunchReverse', - ); + const { p: waitP, resolveP: waitResolveP } = utils.promise(); mockedHolePunchReverse.mockImplementation(() => { - return new PromiseCancellable((res) => { + return new PromiseCancellable(async (res) => { + await waitP; res(); }); }); - await nodeGraph.setNode(serverNodeId, serverAddress); + nodeConnectionManager.handleNodesConnectionSignalFinal( + '127.0.0.1' as Host, + 55550 as Port, + ); + nodeConnectionManager.handleNodesConnectionSignalFinal( + '127.0.0.1' as Host, + 55551 as Port, + ); + nodeConnectionManager.handleNodesConnectionSignalFinal( + '127.0.0.1' as Host, + 55552 as Port, + ); + nodeConnectionManager.handleNodesConnectionSignalFinal( + '127.0.0.1' as Host, + 55553 as Port, + ); + nodeConnectionManager.handleNodesConnectionSignalFinal( + '127.0.0.1' as Host, + 55554 as Port, + ); + nodeConnectionManager.handleNodesConnectionSignalFinal( + '127.0.0.1' as Host, + 55555 as Port, + ); - const srcNodeId = testNodesUtils.generateRandomNodeId(); - const srcNodeIdEncoded = nodesUtils.encodeNodeId(srcNodeId); + // @ts-ignore: protected property + expect(nodeConnectionManager.activeHolePunchPs.size).toBe(6); + // @ts-ignore: protected property + expect(nodeConnectionManager.activeHolePunchAddresses.size).toBe(1); + waitResolveP(); + // @ts-ignore: protected property + for await (const [, p] of nodeConnectionManager.activeHolePunchPs) { + await p; + } - await nodeConnectionManager.relaySignalingMessage( - { - srcIdEncoded: srcNodeIdEncoded, - dstIdEncoded: serverNodeIdEncoded, - address: { - host: '127.0.0.2', - port: 22222, - }, - }, - { - host: '127.0.0.3' as Host, - port: 33333 as Port, - }, - ); + // Only attempted once + expect(mockedHolePunchReverse).toHaveBeenCalledTimes(6); + await nodeConnectionManager.stop(); + }); + test('holePunchSignalRequest rejects excessive requests', async () => { + nodeConnectionManager = new NodeConnectionManager({ + keyRing, + logger: logger.getChild(NodeConnectionManager.name), + nodeGraph, + connectionKeepAliveTimeoutTime: 10000, + connectionKeepAliveIntervalTime: 1000, + tlsConfig, + seedNodes: undefined, + }); + await nodeConnectionManager.start({ + host: localHost as Host, + }); + await taskManager.startProcessing(); - expect(mockedHolePunchReverse).toHaveBeenCalled(); + mockedHolePunchReverse.mockImplementation(() => { + return new PromiseCancellable(async (res) => { + res(); + }); + }); + + expect( + await nodeConnectionManager.pingNode( + remotePolykeyAgentB.keyRing.getNodeId(), + remotePolykeyAgentB.agentServiceHost, + remotePolykeyAgentB.agentServicePort, + ), + ).toBeTrue(); + const keyPair = keysUtils.generateKeyPair(); + const sourceNodeId = keysUtils.publicKeyToNodeId(keyPair.publicKey); + const targetNodeId = remotePolykeyAgentB.keyRing.getNodeId(); + const data = Buffer.concat([sourceNodeId, targetNodeId]); + const signature = keysUtils.signWithPrivateKey(keyPair, data); + expect(() => { + for (let i = 0; i < 30; i++) { + nodeConnectionManager.handleNodesConnectionSignalInitial( + sourceNodeId, + targetNodeId, + { + host: '127.0.0.1' as Host, + port: 55555 as Port, + }, + signature.toString('base64url'), + ); + } + }).toThrow(nodesErrors.ErrorNodeConnectionManagerRequestRateExceeded); + + const signalMapA = + // @ts-ignore: kidnap protected property + nodeConnectionManager.activeSignalFinalPs; + for (const p of signalMapA.values()) { + await p; + } + const punchMapB = + // @ts-ignore: kidnap protected property + remotePolykeyAgentB.nodeConnectionManager.activeHolePunchPs; + for (const [, p] of punchMapB) { + await p; + } await nodeConnectionManager.stop(); }); diff --git a/tests/nodes/NodeConnectionManager.lifecycle.test.ts b/tests/nodes/NodeConnectionManager.lifecycle.test.ts index 5e2a12602..ff756d6e1 100644 --- a/tests/nodes/NodeConnectionManager.lifecycle.test.ts +++ b/tests/nodes/NodeConnectionManager.lifecycle.test.ts @@ -65,12 +65,10 @@ describe(`${NodeConnectionManager.name} lifecycle test`, () => { keyRingPeer = await KeyRing.createKeyRing({ password, keysPath: keysPathPeer, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, }); nodeConnectionManagerPeer1 = new NodeConnectionManager({ keyRing: keyRingPeer, @@ -98,12 +96,10 @@ describe(`${NodeConnectionManager.name} lifecycle test`, () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, }); const dbPath = path.join(dataDir, 'db'); db = await DB.createDB({ @@ -173,9 +169,7 @@ describe(`${NodeConnectionManager.name} lifecycle test`, () => { nodeConnectionManager = new NodeConnectionManager({ keyRing, nodeGraph, - options: { - connectionConnectTimeoutTime: 1000, - }, + connectionConnectTimeoutTime: 1000, logger: logger.getChild(`${NodeConnectionManager.name}Local`), tlsConfig: clientTlsConfig, seedNodes: undefined, diff --git a/tests/nodes/NodeConnectionManager.seednodes.test.ts b/tests/nodes/NodeConnectionManager.seednodes.test.ts index 3b6eea0db..f48f450f7 100644 --- a/tests/nodes/NodeConnectionManager.seednodes.test.ts +++ b/tests/nodes/NodeConnectionManager.seednodes.test.ts @@ -115,12 +115,10 @@ describe(`${NodeConnectionManager.name} seednodes test`, () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, }); const dbPath = path.join(dataDir, 'db'); db = await DB.createDB({ @@ -186,9 +184,7 @@ describe(`${NodeConnectionManager.name} seednodes test`, () => { keyRing, logger: logger.getChild(NodeConnectionManager.name), nodeGraph, - options: { - connectionConnectTimeoutTime: 1000, - }, + connectionConnectTimeoutTime: 1000, tlsConfig, seedNodes, }); @@ -245,9 +241,7 @@ describe(`${NodeConnectionManager.name} seednodes test`, () => { keyRing, logger: logger.getChild(NodeConnectionManager.name), nodeGraph, - options: { - connectionConnectTimeoutTime: 1000, - }, + connectionConnectTimeoutTime: 1000, tlsConfig, seedNodes, }); @@ -300,9 +294,7 @@ describe(`${NodeConnectionManager.name} seednodes test`, () => { keyRing, logger: logger.getChild(NodeConnectionManager.name), nodeGraph, - options: { - connectionConnectTimeoutTime: 1000, - }, + connectionConnectTimeoutTime: 1000, tlsConfig, seedNodes, }); @@ -357,9 +349,7 @@ describe(`${NodeConnectionManager.name} seednodes test`, () => { keyRing, logger: logger.getChild(NodeConnectionManager.name), nodeGraph, - options: { - connectionConnectTimeoutTime: 1000, - }, + connectionConnectTimeoutTime: 1000, tlsConfig, seedNodes, }); @@ -419,9 +409,7 @@ describe(`${NodeConnectionManager.name} seednodes test`, () => { keyRing, logger: logger.getChild(NodeConnectionManager.name), nodeGraph, - options: { - connectionConnectTimeoutTime: 1000, - }, + connectionConnectTimeoutTime: 1000, tlsConfig, seedNodes, }); @@ -478,9 +466,7 @@ describe(`${NodeConnectionManager.name} seednodes test`, () => { keyRing, logger: logger.getChild(NodeConnectionManager.name), nodeGraph, - options: { - connectionConnectTimeoutTime: 1000, - }, + connectionConnectTimeoutTime: 1000, tlsConfig, seedNodes, }); @@ -538,9 +524,7 @@ describe(`${NodeConnectionManager.name} seednodes test`, () => { keyRing, logger: logger.getChild(NodeConnectionManager.name), nodeGraph, - options: { - connectionConnectTimeoutTime: 1000, - }, + connectionConnectTimeoutTime: 1000, tlsConfig, seedNodes, }); diff --git a/tests/nodes/NodeConnectionManager.timeout.test.ts b/tests/nodes/NodeConnectionManager.timeout.test.ts index 49f0c3939..44a5d2e3e 100644 --- a/tests/nodes/NodeConnectionManager.timeout.test.ts +++ b/tests/nodes/NodeConnectionManager.timeout.test.ts @@ -75,11 +75,9 @@ describe(`${NodeConnectionManager.name} timeout test`, () => { password, keysPath, logger, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, }); const dbPath = path.join(dataDir, 'db'); db = await DB.createDB({ @@ -113,10 +111,8 @@ describe(`${NodeConnectionManager.name} timeout test`, () => { nodeGraph, tlsConfig, seedNodes: undefined, - options: { - connectionConnectTimeoutTime: 1000, - connectionIdleTimeoutTime: 100, - }, + connectionConnectTimeoutTime: 1000, + connectionIdleTimeoutTime: 100, }); await nodeConnectionManager.start({ host: localHost as Host, @@ -155,9 +151,7 @@ describe(`${NodeConnectionManager.name} timeout test`, () => { nodeGraph, tlsConfig, seedNodes: undefined, - options: { - connectionIdleTimeoutTime: 1000, - }, + connectionIdleTimeoutTime: 1000, }); await nodeConnectionManager.start({ host: localHost as Host, @@ -205,47 +199,6 @@ describe(`${NodeConnectionManager.name} timeout test`, () => { await nodeConnectionManager.stop(); }); - test('withConnection should extend timeout', async () => { - nodeConnectionManager = new NodeConnectionManager({ - keyRing, - logger: logger.getChild(NodeConnectionManager.name), - nodeGraph, - tlsConfig, - seedNodes: undefined, - options: { - connectionIdleTimeoutTime: 1000, - }, - }); - await nodeConnectionManager.start({ - host: localHost as Host, - }); - - await nodeGraph.setNode(remoteNodeId1, remoteAddress1); - - // @ts-ignore: kidnap connections - const connections = nodeConnectionManager.connections; - // @ts-ignore: kidnap connections - const connectionLocks = nodeConnectionManager.connectionLocks; - await nodeConnectionManager.withConnF(remoteNodeId1, async () => {}); - const midConnAndLock = connections.get( - remoteNodeId1.toString() as NodeIdString, - ); - // Check entry is in map and lock is released - expect(midConnAndLock).toBeDefined(); - expect(connectionLocks.isLocked(remoteNodeId1.toString())).toBeFalsy(); - expect(midConnAndLock?.timer).toBeDefined(); - - // Destroying the connection - // @ts-ignore: private method - await nodeConnectionManager.destroyConnection(remoteNodeId1); - const finalConnAndLock = connections.get( - remoteNodeId1.toString() as NodeIdString, - ); - expect(finalConnAndLock).not.toBeDefined(); - expect(connectionLocks.isLocked(remoteNodeId1.toString())).toBeFalsy(); - - await nodeConnectionManager.stop(); - }); test('Connection can time out', async () => { nodeConnectionManager = new NodeConnectionManager({ keyRing, @@ -253,10 +206,8 @@ describe(`${NodeConnectionManager.name} timeout test`, () => { nodeGraph, tlsConfig, seedNodes: undefined, - options: { - connectionIdleTimeoutTime: 5000, - connectionConnectTimeoutTime: 200, - }, + connectionIdleTimeoutTime: 5000, + connectionConnectTimeoutTime: 200, }); await nodeConnectionManager.start({ host: localHost as Host, @@ -280,10 +231,8 @@ describe(`${NodeConnectionManager.name} timeout test`, () => { nodeGraph, tlsConfig, seedNodes: undefined, - options: { - connectionIdleTimeoutTime: 5000, - connectionConnectTimeoutTime: 200, - }, + connectionIdleTimeoutTime: 5000, + connectionConnectTimeoutTime: 200, }); await nodeConnectionManager.start({ host: localHost as Host, @@ -313,10 +262,8 @@ describe(`${NodeConnectionManager.name} timeout test`, () => { nodeGraph, tlsConfig, seedNodes: undefined, - options: { - connectionIdleTimeoutTime: 5000, - connectionConnectTimeoutTime: 200, - }, + connectionIdleTimeoutTime: 5000, + connectionConnectTimeoutTime: 200, }); await nodeConnectionManager.start({ host: localHost as Host, diff --git a/tests/nodes/NodeGraph.test.ts b/tests/nodes/NodeGraph.test.ts index 81e30705e..01717a5f4 100644 --- a/tests/nodes/NodeGraph.test.ts +++ b/tests/nodes/NodeGraph.test.ts @@ -40,11 +40,9 @@ describe(`${NodeGraph.name} test`, () => { password, keysPath, logger, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, }); dbKey = keysUtils.generateKey(); dbPath = `${dataDir}/db`; diff --git a/tests/nodes/NodeManager.test.ts b/tests/nodes/NodeManager.test.ts index 3340cdd76..ba1c8a3bc 100644 --- a/tests/nodes/NodeManager.test.ts +++ b/tests/nodes/NodeManager.test.ts @@ -64,11 +64,9 @@ describe(`${NodeManager.name} test`, () => { password, keysPath, logger, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, }); const dbPath = path.join(dataDir, 'db'); db = await DB.createDB({ diff --git a/tests/nodes/agent/handlers/nodesClaimsGet.test.ts b/tests/nodes/agent/handlers/nodesClaimsGet.test.ts index 171cd1bba..bb0499ff8 100644 --- a/tests/nodes/agent/handlers/nodesClaimsGet.test.ts +++ b/tests/nodes/agent/handlers/nodesClaimsGet.test.ts @@ -51,11 +51,9 @@ describe('nodesClaimsGet', () => { keyRing = await KeyRing.createKeyRing({ keysPath, password, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); const dbPath = path.join(dataDir, 'db'); diff --git a/tests/nodes/agent/handlers/nodesClosestLocalNode.test.ts b/tests/nodes/agent/handlers/nodesClosestLocalNode.test.ts index 9f2443f95..e3fd1b497 100644 --- a/tests/nodes/agent/handlers/nodesClosestLocalNode.test.ts +++ b/tests/nodes/agent/handlers/nodesClosestLocalNode.test.ts @@ -50,11 +50,9 @@ describe('nodesClosestLocalNode', () => { keyRing = await KeyRing.createKeyRing({ keysPath, password, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); const dbPath = path.join(dataDir, 'db'); diff --git a/tests/nodes/agent/handlers/nodesConnectionSignalFinal.test.ts b/tests/nodes/agent/handlers/nodesConnectionSignalFinal.test.ts new file mode 100644 index 000000000..e334b27e5 --- /dev/null +++ b/tests/nodes/agent/handlers/nodesConnectionSignalFinal.test.ts @@ -0,0 +1,179 @@ +import type { KeyPair } from '@/keys/types'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import { QUICClient, QUICServer, events as quicEvents } from '@matrixai/quic'; +import { RPCClient, RPCServer } from '@matrixai/rpc'; +import { nodesConnectionSignalFinal } from '@/nodes/agent/callers'; +import { NodesConnectionSignalFinal } from '@/nodes/agent/handlers'; +import * as keysUtils from '@/keys/utils/index'; +import * as networkUtils from '@/network/utils'; +import * as nodesUtils from '@/nodes/utils'; +import * as tlsTestsUtils from '../../../utils/tls'; +import * as testsNodesUtils from '../../utils'; + +describe('nodesHolePunchRequest', () => { + const logger = new Logger('nodesHolePunchRequest test', LogLevel.WARN, [ + new StreamHandler(), + ]); + const crypto = tlsTestsUtils.createCrypto(); + const localHost = '127.0.0.1'; + + let keyPair: KeyPair; + let rpcServer: RPCServer; + let quicServer: QUICServer; + + const clientManifest = { + nodesConnectionSignalFinal, + }; + type ClientManifest = typeof clientManifest; + let rpcClient: RPCClient; + let quicClient: QUICClient; + const dummyNodeConnectionManager = { + handleNodesConnectionSignalFinal: jest.fn(), + }; + + beforeEach(async () => { + dummyNodeConnectionManager.handleNodesConnectionSignalFinal.mockClear(); + + // Handler dependencies + keyPair = keysUtils.generateKeyPair(); + const tlsConfigClient = await tlsTestsUtils.createTLSConfig(keyPair); + + // Setting up server + const serverManifest = { + nodesConnectionSignalFinal: new NodesConnectionSignalFinal({ + nodeConnectionManager: dummyNodeConnectionManager as any, + logger, + }), + }; + rpcServer = new RPCServer({ + fromError: networkUtils.fromError, + logger, + }); + await rpcServer.start({ manifest: serverManifest }); + const tlsConfig = await tlsTestsUtils.createTLSConfig(keyPair); + quicServer = new QUICServer({ + config: { + key: tlsConfig.keyPrivatePem, + cert: tlsConfig.certChainPem, + verifyPeer: true, + verifyCallback: async () => undefined, + }, + crypto: { + key: keysUtils.generateKey(), + ops: crypto, + }, + logger, + }); + const handleStream = async ( + event: quicEvents.EventQUICConnectionStream, + ) => { + // Streams are handled via the RPCServer. + const stream = event.detail; + logger.info('!!!!Handling new stream!!!!!'); + rpcServer.handleStream(stream); + }; + const handleConnection = async ( + event: quicEvents.EventQUICServerConnection, + ) => { + // Needs to setup stream handler + const conn = event.detail; + logger.info('!!!!Handling new Connection!!!!!'); + conn.addEventListener( + quicEvents.EventQUICConnectionStream.name, + handleStream, + ); + conn.addEventListener( + quicEvents.EventQUICConnectionStopped.name, + () => { + conn.removeEventListener( + quicEvents.EventQUICConnectionStream.name, + handleStream, + ); + }, + { once: true }, + ); + }; + quicServer.addEventListener( + quicEvents.EventQUICServerConnection.name, + handleConnection, + ); + quicServer.addEventListener( + quicEvents.EventQUICServerStopped.name, + () => { + quicServer.removeEventListener( + quicEvents.EventQUICServerConnection.name, + handleConnection, + ); + }, + { once: true }, + ); + await quicServer.start({ + host: localHost, + }); + + // Setting up client + rpcClient = new RPCClient({ + manifest: clientManifest, + streamFactory: async () => { + return quicClient.connection.newStream(); + }, + toError: networkUtils.toError, + logger, + }); + quicClient = await QUICClient.createQUICClient({ + crypto: { + ops: crypto, + }, + config: { + key: tlsConfigClient.keyPrivatePem, + cert: tlsConfigClient.certChainPem, + verifyPeer: true, + verifyCallback: async () => undefined, + }, + host: localHost, + port: quicServer.port, + localHost: localHost, + logger, + }); + }); + afterEach(async () => { + await rpcServer.stop({ force: true }); + await quicServer.stop({ force: true }); + }); + + test('should send hole punch relay', async () => { + const requestKeyPair = keysUtils.generateKeyPair(); + const targetNodeId = testsNodesUtils.generateRandomNodeId(); + const sourceNodeId = keysUtils.publicKeyToNodeId(requestKeyPair.publicKey); + // Data is just `` concatenated + const requestData = Buffer.concat([sourceNodeId, targetNodeId]); + const requestSignature = keysUtils.signWithPrivateKey( + requestKeyPair, + requestData, + ); + + // Generating relay signature, data is just `
` concatenated + const address = { + host: quicClient.host, + port: quicClient.port, + }; + const data = Buffer.concat([ + sourceNodeId, + targetNodeId, + Buffer.from(JSON.stringify(address), 'utf-8'), + requestSignature, + ]); + const relaySignature = keysUtils.signWithPrivateKey(keyPair, data); + + await rpcClient.methods.nodesConnectionSignalFinal({ + sourceNodeIdEncoded: nodesUtils.encodeNodeId(sourceNodeId), + targetNodeIdEncoded: nodesUtils.encodeNodeId(targetNodeId), + address, + requestSignature: requestSignature.toString('base64url'), + relaySignature: relaySignature.toString('base64url'), + }); + expect( + dummyNodeConnectionManager.handleNodesConnectionSignalFinal, + ).toHaveBeenCalled(); + }); +}); diff --git a/tests/nodes/agent/handlers/nodesConnectionSignalInitial.test.ts b/tests/nodes/agent/handlers/nodesConnectionSignalInitial.test.ts new file mode 100644 index 000000000..7ab902fc8 --- /dev/null +++ b/tests/nodes/agent/handlers/nodesConnectionSignalInitial.test.ts @@ -0,0 +1,158 @@ +import type { KeyPair } from '@/keys/types'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import { QUICClient, QUICServer, events as quicEvents } from '@matrixai/quic'; +import { RPCClient, RPCServer } from '@matrixai/rpc'; +import { nodesConnectionSignalInitial } from '@/nodes/agent/callers'; +import { NodesConnectionSignalInitial } from '@/nodes/agent/handlers'; +import * as keysUtils from '@/keys/utils/index'; +import * as nodesUtils from '@/nodes/utils'; +import * as networkUtils from '@/network/utils'; +import * as tlsTestsUtils from '../../../utils/tls'; +import * as testsNodesUtils from '../../../nodes/utils'; + +describe('nodesHolePunchSignal', () => { + const logger = new Logger('nodesHolePunchSignal test', LogLevel.WARN, [ + new StreamHandler(), + ]); + const crypto = tlsTestsUtils.createCrypto(); + const localHost = '127.0.0.1'; + + let keyPair: KeyPair; + let rpcServer: RPCServer; + let quicServer: QUICServer; + + const clientManifest = { + nodesConnectionSignalInitial, + }; + type ClientManifest = typeof clientManifest; + let rpcClient: RPCClient; + let quicClient: QUICClient; + const dummyNodeConnectionManager = { + handleNodesConnectionSignalInitial: jest.fn(), + }; + + beforeEach(async () => { + dummyNodeConnectionManager.handleNodesConnectionSignalInitial.mockClear(); + + // Handler dependencies + keyPair = keysUtils.generateKeyPair(); + const tlsConfigClient = await tlsTestsUtils.createTLSConfig(keyPair); + + // Setting up server + const serverManifest = { + nodesConnectionSignalInitial: new NodesConnectionSignalInitial({ + nodeConnectionManager: dummyNodeConnectionManager as any, + }), + }; + rpcServer = new RPCServer({ + fromError: networkUtils.fromError, + logger, + }); + await rpcServer.start({ manifest: serverManifest }); + const tlsConfig = await tlsTestsUtils.createTLSConfig(keyPair); + quicServer = new QUICServer({ + config: { + key: tlsConfig.keyPrivatePem, + cert: tlsConfig.certChainPem, + verifyPeer: true, + verifyCallback: async () => undefined, + }, + crypto: { + key: keysUtils.generateKey(), + ops: crypto, + }, + logger, + }); + const handleStream = async ( + event: quicEvents.EventQUICConnectionStream, + ) => { + // Streams are handled via the RPCServer. + const stream = event.detail; + logger.info('!!!!Handling new stream!!!!!'); + rpcServer.handleStream(stream); + }; + const handleConnection = async ( + event: quicEvents.EventQUICServerConnection, + ) => { + // Needs to setup stream handler + const conn = event.detail; + logger.info('!!!!Handling new Connection!!!!!'); + conn.addEventListener( + quicEvents.EventQUICConnectionStream.name, + handleStream, + ); + conn.addEventListener( + quicEvents.EventQUICConnectionStopped.name, + () => { + conn.removeEventListener( + quicEvents.EventQUICConnectionStream.name, + handleStream, + ); + }, + { once: true }, + ); + }; + quicServer.addEventListener( + quicEvents.EventQUICServerConnection.name, + handleConnection, + ); + quicServer.addEventListener( + quicEvents.EventQUICServerStopped.name, + () => { + quicServer.removeEventListener( + quicEvents.EventQUICServerConnection.name, + handleConnection, + ); + }, + { once: true }, + ); + await quicServer.start({ + host: localHost, + }); + + // Setting up client + rpcClient = new RPCClient({ + manifest: clientManifest, + streamFactory: async () => { + return quicClient.connection.newStream(); + }, + toError: networkUtils.toError, + logger, + }); + quicClient = await QUICClient.createQUICClient({ + crypto: { + ops: crypto, + }, + config: { + key: tlsConfigClient.keyPrivatePem, + cert: tlsConfigClient.certChainPem, + verifyPeer: true, + verifyCallback: async () => undefined, + }, + host: localHost, + port: quicServer.port, + localHost: localHost, + logger, + }); + }); + afterEach(async () => { + await rpcServer.stop({ force: true }); + await quicServer.stop({ force: true }); + }); + + test('should send hole punch relay', async () => { + const targetNodeId = testsNodesUtils.generateRandomNodeId(); + const targetNodeIdEncoded = nodesUtils.encodeNodeId(targetNodeId); + const sourceNodeId = keysUtils.publicKeyToNodeId(keyPair.publicKey); + // Data is just `` concatenated + const data = Buffer.concat([sourceNodeId, targetNodeId]); + const signature = keysUtils.signWithPrivateKey(keyPair, data); + await rpcClient.methods.nodesConnectionSignalInitial({ + targetNodeIdEncoded, + signature: signature.toString('base64url'), + }); + expect( + dummyNodeConnectionManager.handleNodesConnectionSignalInitial, + ).toHaveBeenCalled(); + }); +}); diff --git a/tests/nodes/agent/handlers/nodesCrossSignClaim.test.ts b/tests/nodes/agent/handlers/nodesCrossSignClaim.test.ts index 9df47527f..83b5f43fc 100644 --- a/tests/nodes/agent/handlers/nodesCrossSignClaim.test.ts +++ b/tests/nodes/agent/handlers/nodesCrossSignClaim.test.ts @@ -66,11 +66,9 @@ describe('nodesCrossSignClaim', () => { keyRing = await KeyRing.createKeyRing({ keysPath, password, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); remoteNodeId = keyRing.getNodeId(); diff --git a/tests/nodes/agent/handlers/nodesHolePunchMessage.test.ts b/tests/nodes/agent/handlers/nodesHolePunchMessage.test.ts deleted file mode 100644 index b248745f6..000000000 --- a/tests/nodes/agent/handlers/nodesHolePunchMessage.test.ts +++ /dev/null @@ -1,266 +0,0 @@ -import type GestaltGraph from '@/gestalts/GestaltGraph'; -import type { Host } from '@/network/types'; -import fs from 'fs'; -import path from 'path'; -import os from 'os'; -import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; -import { - QUICClient, - QUICServer, - QUICSocket, - events as quicEvents, -} from '@matrixai/quic'; -import { DB } from '@matrixai/db'; -import { RPCClient, RPCServer } from '@matrixai/rpc'; -import KeyRing from '@/keys/KeyRing'; -import * as nodesUtils from '@/nodes/utils'; -import NodeGraph from '@/nodes/NodeGraph'; -import { nodesHolePunchMessageSend } from '@/nodes/agent/callers'; -import NodesHolePunchMessageSend from '@/nodes/agent/handlers/NodesHolePunchMessageSend'; -import NodeConnectionManager from '@/nodes/NodeConnectionManager'; -import NodeManager from '@/nodes/NodeManager'; -import ACL from '@/acl/ACL'; -import Sigchain from '@/sigchain/Sigchain'; -import TaskManager from '@/tasks/TaskManager'; -import * as keysUtils from '@/keys/utils'; -import * as networkUtils from '@/network/utils'; -import * as tlsTestsUtils from '../../../utils/tls'; - -describe('nodesHolePunchMessage', () => { - const logger = new Logger('nodesHolePunchMessage test', LogLevel.WARN, [ - new StreamHandler(), - ]); - const password = 'password'; - const crypto = tlsTestsUtils.createCrypto(); - const localHost = '127.0.0.1'; - - let dataDir: string; - - let keyRing: KeyRing; - let db: DB; - let acl: ACL; - let sigchain: Sigchain; - let taskManager: TaskManager; - let quicSocket: QUICSocket; - let nodeConnectionManager: NodeConnectionManager; - let nodeManager: NodeManager; - let nodeGraph: NodeGraph; - let rpcServer: RPCServer; - let quicServer: QUICServer; - - const clientManifest = { - nodesHolePunchMessageSend, - }; - type ClientManifest = typeof clientManifest; - let rpcClient: RPCClient; - let quicClient: QUICClient; - - beforeEach(async () => { - dataDir = await fs.promises.mkdtemp( - path.join(os.tmpdir(), 'polykey-test-'), - ); - - // Handler dependencies - const keysPath = path.join(dataDir, 'keys'); - keyRing = await KeyRing.createKeyRing({ - keysPath, - password, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, - logger, - }); - const dbPath = path.join(dataDir, 'db'); - db = await DB.createDB({ - dbPath, - logger, - }); - nodeGraph = await NodeGraph.createNodeGraph({ - db, - keyRing, - logger, - }); - acl = await ACL.createACL({ - db, - logger, - }); - sigchain = await Sigchain.createSigchain({ - db, - keyRing, - logger, - }); - nodeGraph = await NodeGraph.createNodeGraph({ - db, - keyRing, - logger: logger.getChild('NodeGraph'), - }); - taskManager = await TaskManager.createTaskManager({ - db, - logger, - lazy: true, - }); - quicSocket = new QUICSocket({ - logger, - }); - await quicSocket.start({ - host: localHost, - }); - const tlsConfigClient = await tlsTestsUtils.createTLSConfig( - keyRing.keyPair, - ); - nodeConnectionManager = new NodeConnectionManager({ - tlsConfig: tlsConfigClient, - keyRing, - nodeGraph, - options: { - connectionConnectTimeoutTime: 2000, - connectionIdleTimeoutTime: 2000, - }, - logger: logger.getChild('NodeConnectionManager'), - }); - nodeManager = new NodeManager({ - db, - keyRing, - nodeGraph, - nodeConnectionManager, - sigchain, - taskManager, - gestaltGraph: {} as GestaltGraph, - logger, - }); - await nodeManager.start(); - await nodeConnectionManager.start({ host: localHost as Host }); - await taskManager.startProcessing(); - - // Setting up server - const serverManifest = { - nodesHolePunchMessageSend: new NodesHolePunchMessageSend({ - db, - keyRing, - nodeConnectionManager, - nodeManager: nodeManager, - logger, - }), - }; - rpcServer = new RPCServer({ - fromError: networkUtils.fromError, - logger, - }); - await rpcServer.start({ manifest: serverManifest }); - const tlsConfig = await tlsTestsUtils.createTLSConfig(keyRing.keyPair); - quicServer = new QUICServer({ - config: { - key: tlsConfig.keyPrivatePem, - cert: tlsConfig.certChainPem, - verifyPeer: true, - verifyCallback: async () => { - return undefined; - }, - }, - crypto: { - key: keysUtils.generateKey(), - ops: crypto, - }, - logger, - }); - const handleStream = async ( - event: quicEvents.EventQUICConnectionStream, - ) => { - // Streams are handled via the RPCServer. - const stream = event.detail; - logger.info('!!!!Handling new stream!!!!!'); - rpcServer.handleStream(stream); - }; - const handleConnection = async ( - event: quicEvents.EventQUICServerConnection, - ) => { - // Needs to setup stream handler - const conn = event.detail; - logger.info('!!!!Handling new Connection!!!!!'); - conn.addEventListener( - quicEvents.EventQUICConnectionStream.name, - handleStream, - ); - conn.addEventListener( - quicEvents.EventQUICConnectionStopped.name, - () => { - conn.removeEventListener( - quicEvents.EventQUICConnectionStream.name, - handleStream, - ); - }, - { once: true }, - ); - }; - quicServer.addEventListener('serverConnection', handleConnection); - quicServer.addEventListener( - 'serverStop', - () => { - quicServer.removeEventListener('serverConnection', handleConnection); - }, - { once: true }, - ); - await quicServer.start({ - host: localHost, - }); - - // Setting up client - rpcClient = new RPCClient({ - manifest: clientManifest, - streamFactory: async () => { - return quicClient.connection.newStream(); - }, - toError: networkUtils.toError, - logger, - }); - quicClient = await QUICClient.createQUICClient({ - crypto: { - ops: crypto, - }, - config: { - key: tlsConfigClient.keyPrivatePem, - cert: tlsConfigClient.certChainPem, - verifyPeer: true, - verifyCallback: async () => { - return undefined; - }, - }, - host: localHost, - port: quicServer.port, - localHost: localHost, - logger, - }); - }); - afterEach(async () => { - await rpcServer.stop({ force: true }); - await taskManager.stopProcessing(); - await taskManager.stopTasks(); - await quicServer.stop({ force: true }); - await nodeGraph.stop(); - await nodeManager.stop(); - await nodeConnectionManager.stop(); - await taskManager.stop(); - await sigchain.stop(); - await acl.stop(); - await db.stop(); - await keyRing.stop(); - await quicSocket.stop({ force: true }); - }); - - test('dummy test', async () => {}); - // TODO: holding process open for a short time, subject to change in agent migration stage 2 - test.skip('should send hole punch relay', async () => { - const nodeId = nodesUtils.encodeNodeId(keyRing.getNodeId()); - await rpcClient.methods.nodesHolePunchMessageSend({ - srcIdEncoded: nodeId, - dstIdEncoded: nodeId, - address: { - host: quicClient.host, - port: quicClient.port, - }, - }); - // TODO: check if the ping was sent - }); -}); diff --git a/tests/nodes/agent/handlers/notificationsSend.test.ts b/tests/nodes/agent/handlers/notificationsSend.test.ts index 8319b072a..e8d215fe6 100644 --- a/tests/nodes/agent/handlers/notificationsSend.test.ts +++ b/tests/nodes/agent/handlers/notificationsSend.test.ts @@ -70,11 +70,9 @@ describe('notificationsSend', () => { senderKeyRing = await KeyRing.createKeyRing({ keysPath: senderKeysPath, password, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); senderNodeId = senderKeyRing.getNodeId(); @@ -84,11 +82,9 @@ describe('notificationsSend', () => { keyRing = await KeyRing.createKeyRing({ keysPath, password, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); const dbPath = path.join(dataDir, 'db'); @@ -127,10 +123,8 @@ describe('notificationsSend', () => { tlsConfig: tlsConfigClient, keyRing, nodeGraph, - options: { - connectionConnectTimeoutTime: 2000, - connectionIdleTimeoutTime: 2000, - }, + connectionConnectTimeoutTime: 2000, + connectionIdleTimeoutTime: 2000, logger: logger.getChild('NodeConnectionManager'), }); nodeManager = new NodeManager({ diff --git a/tests/notifications/NotificationsManager.test.ts b/tests/notifications/NotificationsManager.test.ts index 579bd5c13..13c7c2ac0 100644 --- a/tests/notifications/NotificationsManager.test.ts +++ b/tests/notifications/NotificationsManager.test.ts @@ -64,11 +64,9 @@ describe('NotificationsManager', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); const dbPath = path.join(dataDir, 'db'); diff --git a/tests/scratch.test.ts b/tests/scratch.test.ts index 4959b5280..1813a6cc0 100644 --- a/tests/scratch.test.ts +++ b/tests/scratch.test.ts @@ -1,13 +1,6 @@ -import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; - -// This is a 'scratch paper' test file for quickly running tests in the CI +/** + * This is a 'scratch paper' test file for quickly running tests in the CI + */ describe('scratch', () => { - const _logger = new Logger(`scratch test`, LogLevel.WARN, [ - new StreamHandler(), - ]); - - // We can't have empty test files so here is a sanity test - test('Should avoid empty test suite', async () => { - expect(1 + 1).toBe(2); - }); + test('', async () => {}); }); diff --git a/tests/sessions/SessionManager.test.ts b/tests/sessions/SessionManager.test.ts index 9547094ab..bd6b5b746 100644 --- a/tests/sessions/SessionManager.test.ts +++ b/tests/sessions/SessionManager.test.ts @@ -30,11 +30,9 @@ describe('SessionManager', () => { keyRing = await KeyRing.createKeyRing({ password, keysPath, - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); const dbPath = path.join(dataDir, 'db'); diff --git a/tests/sigchain/Sigchain.test.ts b/tests/sigchain/Sigchain.test.ts index 20f82abca..d1c1647eb 100644 --- a/tests/sigchain/Sigchain.test.ts +++ b/tests/sigchain/Sigchain.test.ts @@ -35,12 +35,10 @@ describe(Sigchain.name, () => { keyRing = await KeyRing.createKeyRing({ keysPath, password, - options: { - privateKey, - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + privateKey, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); dbPath = `${dataDir}/db`; diff --git a/tests/utils/ratelimiter/RateLimiter.test.ts b/tests/utils/ratelimiter/RateLimiter.test.ts new file mode 100644 index 000000000..c4a01646d --- /dev/null +++ b/tests/utils/ratelimiter/RateLimiter.test.ts @@ -0,0 +1,57 @@ +import RateLimiter from '@/utils/ratelimiter/RateLimiter'; +import { sleep } from '@/utils'; + +describe(`${RateLimiter.name}`, () => { + let rateLimiter: RateLimiter; + + afterEach(() => { + rateLimiter.stop(); + }); + + test('limits rate', async () => { + rateLimiter = new RateLimiter(undefined, 100); + expect(rateLimiter.consume('', 101)).toBeFalse(); + expect(rateLimiter.consume('', 1)).toBeTrue(); + expect(rateLimiter.consume('', 50)).toBeTrue(); + expect(rateLimiter.consume('', 49)).toBeTrue(); + expect(rateLimiter.tokens('')).toBe(0); + expect(rateLimiter.consume('', 1)).toBeFalse(); + expect(rateLimiter.tokens('')).toBe(0); + }); + test('can refresh rate', async () => { + rateLimiter = new RateLimiter(undefined, 100); + expect(rateLimiter.consume('', 50)).toBeTrue(); + expect(rateLimiter.tokens('')).toBe(50); + rateLimiter.refill(); + expect(rateLimiter.tokens('')).toBe(100); + rateLimiter.refill(); + expect(rateLimiter.tokens('')).toBe(100); + }); + test('independent rates', async () => { + rateLimiter = new RateLimiter(undefined, 100); + expect(rateLimiter.consume('a', 50)).toBeTrue(); + expect(rateLimiter.tokens('a')).toBe(50); + expect(rateLimiter.tokens('b')).toBe(100); + expect(rateLimiter.consume('b', 50)).toBeTrue(); + expect(rateLimiter.tokens('b')).toBe(50); + expect(rateLimiter.consume('a', 50)).toBeTrue(); + expect(rateLimiter.consume('a', 1)).toBeFalse(); + expect(rateLimiter.consume('b', 25)).toBeTrue(); + expect(rateLimiter.consume('b', 26)).toBeFalse(); + }); + test('only positive tokens can be consumed', async () => { + rateLimiter = new RateLimiter(undefined, 100); + expect(() => rateLimiter.consume('', 0)).toThrow(); + expect(() => rateLimiter.consume('', -100)).toThrow(); + expect(() => rateLimiter.consume('', -1)).toThrow(); + expect(() => rateLimiter.consume('', -0)).toThrow(); + }); + test('rates refresh on an interval', async () => { + rateLimiter = new RateLimiter(undefined, 100); + expect(rateLimiter.consume('', 50)).toBeTrue(); + rateLimiter.startRefillInterval(); + expect(rateLimiter.tokens('')).toBe(50); + await sleep(1500); + expect(rateLimiter.tokens('')).toBe(100); + }); +}); diff --git a/tests/vaults/VaultManager.test.ts b/tests/vaults/VaultManager.test.ts index 28475b720..f23a84b63 100644 --- a/tests/vaults/VaultManager.test.ts +++ b/tests/vaults/VaultManager.test.ts @@ -555,11 +555,9 @@ describe('VaultManager', () => { keyRing = await KeyRing.createKeyRing({ keysPath: path.join(allDataDir, 'allKeyRing'), password: 'password', - options: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, logger, }); localNodeId = keyRing.getNodeId();