From 99db4fa70898a4495cf1d57716534e153c6fc0d3 Mon Sep 17 00:00:00 2001 From: Emma Casolin Date: Mon, 2 May 2022 17:13:36 +1000 Subject: [PATCH] feat: error serialisation/deserialisation over gRPC incorporating error chaining Our gRPC `toError` and `fromError` utils are now able to serialise and deserialise Polykey and non-Polykey errors (as well as non-errors), including the entire error chain if this exists. Also includes the ability to filter out sensitive data, for example when the error is being sent to another agent. Errors sent over the network in this way are now additionally wrapped on the receiving side in an `ErrorPolykeyRemote` to make the source of the error more clear. #304 --- src/ErrorPolykey.ts | 63 +++++-- src/agent/service/nodesChainDataGet.ts | 2 +- .../service/nodesClosestLocalNodesGet.ts | 2 +- src/agent/service/nodesCrossSignClaim.ts | 2 +- .../service/nodesHolePunchMessageSend.ts | 2 +- src/agent/service/notificationsSend.ts | 2 +- src/agent/service/vaultsGitInfoGet.ts | 2 +- src/agent/service/vaultsGitPackGet.ts | 2 +- src/agent/service/vaultsScan.ts | 2 +- src/client/service/gestaltsGestaltList.ts | 2 +- src/client/service/identitiesAuthenticate.ts | 2 +- .../service/identitiesAuthenticatedGet.ts | 2 +- .../service/identitiesInfoConnectedGet.ts | 2 +- src/client/service/identitiesInfoGet.ts | 2 +- src/client/service/keysCertsChainGet.ts | 2 +- src/client/service/vaultsList.ts | 2 +- src/client/service/vaultsLog.ts | 2 +- src/client/service/vaultsPermissionGet.ts | 2 +- src/client/service/vaultsScan.ts | 2 +- src/client/service/vaultsSecretsList.ts | 2 +- src/errors.ts | 12 ++ src/grpc/utils/utils.ts | 162 +++++++++++++++--- tests/grpc/utils.test.ts | 126 +++++++++++++- tests/grpc/utils/testService.ts | 14 +- 24 files changed, 353 insertions(+), 62 deletions(-) diff --git a/src/ErrorPolykey.ts b/src/ErrorPolykey.ts index 17a65d04a..3e418e757 100644 --- a/src/ErrorPolykey.ts +++ b/src/ErrorPolykey.ts @@ -1,20 +1,61 @@ +import type { POJO } from './types'; import { AbstractError } from '@matrixai/errors'; import sysexits from './utils/sysexits'; class ErrorPolykey extends AbstractError { static description: string = 'Polykey error'; exitCode: number = sysexits.GENERAL; - toJSON(): string { - return JSON.stringify({ - name: this.name, - description: this.description, - message: this.message, - exitCode: this.exitCode, - timestamp: this.timestamp, - data: this.data, - cause: this.cause, - stack: this.stack, - }); + public toJSON( + _key: string = '', + options: { + description?: boolean; + message?: boolean, + exitCode?: boolean, + timestamp?: boolean; + data?: boolean; + cause?: boolean; + stack?: boolean; + } = {} + ): { + type: string; + data: { + description?: string; + message?: string; + exitCode?: number, + timestamp?: Date, + data?: POJO; + cause?: T, + stack?: string + } + } { + options.description ??= true; + options.message ??= true; + options.exitCode ??= true; + options.timestamp ??= true; + options.data ??= true; + options.cause ??= true; + options.stack ??= true; + const data: POJO = {}; + if (options.description) data.description = this.description; + if (options.message) data.message = this.message; + if (options.exitCode) data.exitCode = this.exitCode; + if (options.timestamp) data.timestamp = this.timestamp; + if (options.data) data.data = this.data; + if (options.cause) { + // Propagate the options down the exception chain + // but only if the cause is another AbstractError + if (this.cause instanceof ErrorPolykey) { + data.cause = this.cause.toJSON('cause', options); + } else { + // Use `replacer` to further encode this object + data.cause = this.cause; + } + } + if (options.stack) data.stack = this.stack; + return { + type: this.name, + data + }; } } diff --git a/src/agent/service/nodesChainDataGet.ts b/src/agent/service/nodesChainDataGet.ts index 3ed37b99f..2a03bdd7a 100644 --- a/src/agent/service/nodesChainDataGet.ts +++ b/src/agent/service/nodesChainDataGet.ts @@ -37,7 +37,7 @@ function nodesChainDataGet({ sigchain }: { sigchain: Sigchain }) { callback(null, response); return; } catch (e) { - callback(grpcUtils.fromError(e)); + callback(grpcUtils.fromError(e, true)); return; } }; diff --git a/src/agent/service/nodesClosestLocalNodesGet.ts b/src/agent/service/nodesClosestLocalNodesGet.ts index 559337c9d..7e5739495 100644 --- a/src/agent/service/nodesClosestLocalNodesGet.ts +++ b/src/agent/service/nodesClosestLocalNodesGet.ts @@ -53,7 +53,7 @@ function nodesClosestLocalNodesGet({ callback(null, response); return; } catch (e) { - callback(grpcUtils.fromError(e)); + callback(grpcUtils.fromError(e, true)); return; } }; diff --git a/src/agent/service/nodesCrossSignClaim.ts b/src/agent/service/nodesCrossSignClaim.ts index 907494512..2606606ee 100644 --- a/src/agent/service/nodesCrossSignClaim.ts +++ b/src/agent/service/nodesCrossSignClaim.ts @@ -25,7 +25,7 @@ function nodesCrossSignClaim({ ) => { // TODO: Move all "await genClaims.throw" to a final catch(). Wrap this // entire thing in a try block. And re-throw whatever error is caught - const genClaims = grpcUtils.generatorDuplex(call); + const genClaims = grpcUtils.generatorDuplex(call, true); try { await sigchain.transaction(async (sigchain) => { const readStatus = await genClaims.read(); diff --git a/src/agent/service/nodesHolePunchMessageSend.ts b/src/agent/service/nodesHolePunchMessageSend.ts index d524e9f24..2196cceec 100644 --- a/src/agent/service/nodesHolePunchMessageSend.ts +++ b/src/agent/service/nodesHolePunchMessageSend.ts @@ -62,7 +62,7 @@ function nodesHolePunchMessageSend({ callback(null, response); return; } catch (e) { - callback(grpcUtils.fromError(e)); + callback(grpcUtils.fromError(e, true)); return; } }; diff --git a/src/agent/service/notificationsSend.ts b/src/agent/service/notificationsSend.ts index cf2b589ea..d942da04e 100644 --- a/src/agent/service/notificationsSend.ts +++ b/src/agent/service/notificationsSend.ts @@ -25,7 +25,7 @@ function notificationsSend({ callback(null, response); return; } catch (e) { - callback(grpcUtils.fromError(e)); + callback(grpcUtils.fromError(e, true)); return; } }; diff --git a/src/agent/service/vaultsGitInfoGet.ts b/src/agent/service/vaultsGitInfoGet.ts index 72f01a74c..1a043f92b 100644 --- a/src/agent/service/vaultsGitInfoGet.ts +++ b/src/agent/service/vaultsGitInfoGet.ts @@ -23,7 +23,7 @@ function vaultsGitInfoGet({ return async ( call: grpc.ServerWritableStream, ): Promise => { - const genWritable = grpcUtils.generatorWritable(call); + const genWritable = grpcUtils.generatorWritable(call, true); const request = call.request; const vaultMessage = request.getVault(); if (vaultMessage == null) { diff --git a/src/agent/service/vaultsGitPackGet.ts b/src/agent/service/vaultsGitPackGet.ts index 5528ade31..8c0d9232f 100644 --- a/src/agent/service/vaultsGitPackGet.ts +++ b/src/agent/service/vaultsGitPackGet.ts @@ -24,7 +24,7 @@ function vaultsGitPackGet({ return async ( call: grpc.ServerDuplexStream, ) => { - const genDuplex = grpcUtils.generatorDuplex(call); + const genDuplex = grpcUtils.generatorDuplex(call, true); const clientBodyBuffers: Uint8Array[] = []; const clientRequest = (await genDuplex.read()).value; clientBodyBuffers.push(clientRequest!.getChunk_asU8()); diff --git a/src/agent/service/vaultsScan.ts b/src/agent/service/vaultsScan.ts index 6dfd028e4..acdcfb0b1 100644 --- a/src/agent/service/vaultsScan.ts +++ b/src/agent/service/vaultsScan.ts @@ -17,7 +17,7 @@ function vaultsScan({ return async ( call: grpc.ServerWritableStream, ): Promise => { - const genWritable = grpcUtils.generatorWritable(call); + const genWritable = grpcUtils.generatorWritable(call, true); const listMessage = new vaultsPB.List(); // Getting the NodeId from the ReverseProxy connection info const connectionInfo = connectionInfoGet(call); diff --git a/src/client/service/gestaltsGestaltList.ts b/src/client/service/gestaltsGestaltList.ts index 458b5cf32..6f3c1c9ea 100644 --- a/src/client/service/gestaltsGestaltList.ts +++ b/src/client/service/gestaltsGestaltList.ts @@ -16,7 +16,7 @@ function gestaltsGestaltList({ return async ( call: grpc.ServerWritableStream, ): Promise => { - const genWritable = grpcUtils.generatorWritable(call); + const genWritable = grpcUtils.generatorWritable(call, false); let gestaltMessage: gestaltsPB.Gestalt; try { const metadata = await authenticate(call.metadata); diff --git a/src/client/service/identitiesAuthenticate.ts b/src/client/service/identitiesAuthenticate.ts index 24ccf2f7e..c80378790 100644 --- a/src/client/service/identitiesAuthenticate.ts +++ b/src/client/service/identitiesAuthenticate.ts @@ -21,7 +21,7 @@ function identitiesAuthenticate({ identitiesPB.AuthenticationProcess >, ): Promise => { - const genWritable = grpcUtils.generatorWritable(call); + const genWritable = grpcUtils.generatorWritable(call, false); try { const metadata = await authenticate(call.metadata); call.sendMetadata(metadata); diff --git a/src/client/service/identitiesAuthenticatedGet.ts b/src/client/service/identitiesAuthenticatedGet.ts index 1fd4bb9da..fa2ca756f 100644 --- a/src/client/service/identitiesAuthenticatedGet.ts +++ b/src/client/service/identitiesAuthenticatedGet.ts @@ -21,7 +21,7 @@ function identitiesAuthenticatedGet({ identitiesPB.Provider >, ): Promise => { - const genWritable = grpcUtils.generatorWritable(call); + const genWritable = grpcUtils.generatorWritable(call, false); try { const metadata = await authenticate(call.metadata); call.sendMetadata(metadata); diff --git a/src/client/service/identitiesInfoConnectedGet.ts b/src/client/service/identitiesInfoConnectedGet.ts index 399600900..865616d06 100644 --- a/src/client/service/identitiesInfoConnectedGet.ts +++ b/src/client/service/identitiesInfoConnectedGet.ts @@ -26,7 +26,7 @@ function identitiesInfoConnectedGet({ identitiesPB.Info >, ): Promise => { - const genWritable = grpcUtils.generatorWritable(call); + const genWritable = grpcUtils.generatorWritable(call, false); try { const metadata = await authenticate(call.metadata); call.sendMetadata(metadata); diff --git a/src/client/service/identitiesInfoGet.ts b/src/client/service/identitiesInfoGet.ts index bbf159f55..400367140 100644 --- a/src/client/service/identitiesInfoGet.ts +++ b/src/client/service/identitiesInfoGet.ts @@ -27,7 +27,7 @@ function identitiesInfoGet({ identitiesPB.Info >, ): Promise => { - const genWritable = grpcUtils.generatorWritable(call); + const genWritable = grpcUtils.generatorWritable(call, false); try { const metadata = await authenticate(call.metadata); call.sendMetadata(metadata); diff --git a/src/client/service/keysCertsChainGet.ts b/src/client/service/keysCertsChainGet.ts index 8355474fd..792b86916 100644 --- a/src/client/service/keysCertsChainGet.ts +++ b/src/client/service/keysCertsChainGet.ts @@ -15,7 +15,7 @@ function keysCertsChainGet({ return async ( call: grpc.ServerWritableStream, ): Promise => { - const genWritable = grpcUtils.generatorWritable(call); + const genWritable = grpcUtils.generatorWritable(call, false); try { const metadata = await authenticate(call.metadata); call.sendMetadata(metadata); diff --git a/src/client/service/vaultsList.ts b/src/client/service/vaultsList.ts index d81902976..c35f51eea 100644 --- a/src/client/service/vaultsList.ts +++ b/src/client/service/vaultsList.ts @@ -19,7 +19,7 @@ function vaultsList({ // Call.on('error', (e) => console.error(e)); // call.on('close', () => console.log('Got close')); // call.on('finish', () => console.log('Got finish')); - const genWritable = grpcUtils.generatorWritable(call); + const genWritable = grpcUtils.generatorWritable(call, false); try { const metadata = await authenticate(call.metadata); call.sendMetadata(metadata); diff --git a/src/client/service/vaultsLog.ts b/src/client/service/vaultsLog.ts index 99056911a..02b35a6f8 100644 --- a/src/client/service/vaultsLog.ts +++ b/src/client/service/vaultsLog.ts @@ -17,7 +17,7 @@ function vaultsLog({ return async ( call: grpc.ServerWritableStream, ): Promise => { - const genWritable = grpcUtils.generatorWritable(call); + const genWritable = grpcUtils.generatorWritable(call, false); try { const metadata = await authenticate(call.metadata); call.sendMetadata(metadata); diff --git a/src/client/service/vaultsPermissionGet.ts b/src/client/service/vaultsPermissionGet.ts index 23780000e..bb2b24dc1 100644 --- a/src/client/service/vaultsPermissionGet.ts +++ b/src/client/service/vaultsPermissionGet.ts @@ -24,7 +24,7 @@ function vaultsPermissionGet({ return async ( call: grpc.ServerWritableStream, ): Promise => { - const genWritable = grpcUtils.generatorWritable(call); + const genWritable = grpcUtils.generatorWritable(call, false); try { const vaultMessage = call.request; const metadata = await authenticate(call.metadata); diff --git a/src/client/service/vaultsScan.ts b/src/client/service/vaultsScan.ts index 3d8d73a7e..0ed5ebbbe 100644 --- a/src/client/service/vaultsScan.ts +++ b/src/client/service/vaultsScan.ts @@ -19,7 +19,7 @@ function vaultsScan({ return async ( call: grpc.ServerWritableStream, ): Promise => { - const genWritable = grpcUtils.generatorWritable(call); + const genWritable = grpcUtils.generatorWritable(call, false); try { const metadata = await authenticate(call.metadata); call.sendMetadata(metadata); diff --git a/src/client/service/vaultsSecretsList.ts b/src/client/service/vaultsSecretsList.ts index db2a1cc36..db71fc128 100644 --- a/src/client/service/vaultsSecretsList.ts +++ b/src/client/service/vaultsSecretsList.ts @@ -18,7 +18,7 @@ function vaultsSecretsList({ return async ( call: grpc.ServerWritableStream, ): Promise => { - const genWritable = grpcUtils.generatorWritable(call); + const genWritable = grpcUtils.generatorWritable(call, false); try { const metadata = await authenticate(call.metadata); call.sendMetadata(metadata); diff --git a/src/errors.ts b/src/errors.ts index 10d662ed3..53ec7b8ed 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -6,6 +6,16 @@ class ErrorPolykeyUnimplemented extends ErrorPolykey { exitCode = sysexits.UNAVAILABLE; } +class ErrorPolykeyUnknown extends ErrorPolykey { + static description = 'Unable to deserialise to known error'; + exitCode = sysexits.UNKNOWN; +} + +class ErrorPolykeyRemote extends ErrorPolykey { + static description = 'Remote error from RPC call'; + exitCode = sysexits.UNAVAILABLE; +} + class ErrorPolykeyAgentRunning extends ErrorPolykey { static description = 'PolykeyAgent is running'; exitCode = sysexits.USAGE; @@ -44,6 +54,8 @@ export { sysexits, ErrorPolykey, ErrorPolykeyUnimplemented, + ErrorPolykeyUnknown, + ErrorPolykeyRemote, ErrorPolykeyAgentRunning, ErrorPolykeyAgentNotRunning, ErrorPolykeyAgentDestroyed, diff --git a/src/grpc/utils/utils.ts b/src/grpc/utils/utils.ts index 0a223c1a9..b75a6d21e 100644 --- a/src/grpc/utils/utils.ts +++ b/src/grpc/utils/utils.ts @@ -29,6 +29,7 @@ import type { } from '../types'; import type { CertificatePemChain, PrivateKeyPem } from '../../keys/types'; import { Buffer } from 'buffer'; +import { AbstractError } from '@matrixai/errors'; import * as grpc from '@grpc/grpc-js'; import * as grpcErrors from '../errors'; import * as errors from '../../errors'; @@ -158,17 +159,16 @@ function getServerSession(call: ServerSurfaceCall): Http2Session { * Serializes Error instances into GRPC errors * Use this on the sending side to send exceptions * Do not send exceptions to clients you do not trust + * If sending to an agent (rather than a client), set sensitive to true to + * prevent sensitive information from being sent over the network */ -function fromError(error: Error): ServerStatusResponse { +function fromError(error: Error, sensitive: boolean = false): ServerStatusResponse { const metadata = new grpc.Metadata(); - // If the error is not ErrorPolykey, wrap it up so it can be serialised - // TODO: add additional metadata regarding the network location of the error - if (!(error instanceof errors.ErrorPolykey)) { - error = new errors.ErrorPolykey(error.message); + if (sensitive) { + metadata.set('error', JSON.stringify(error, sensitiveReplacer)); + } else { + metadata.set('error', JSON.stringify(error, replacer)); } - metadata.set('name', error.name); - metadata.set('message', error.message); - metadata.set('data', JSON.stringify((error as errors.ErrorPolykey).data)); return { metadata, }; @@ -178,10 +178,8 @@ function fromError(error: Error): ServerStatusResponse { * Deserialized GRPC errors into ErrorPolykey * Use this on the receiving side to receive exceptions */ -function toError(e: ServiceError): errors.ErrorPolykey { - const errorName = e.metadata.get('name')[0] as string; - const errorMessage = e.metadata.get('message')[0] as string; - const errorData = e.metadata.get('data')[0] as string; +function toError(e: ServiceError): errors.ErrorPolykey { + const errorData = e.metadata.get('error')[0] as string; // Grpc.status is an enum // this will iterate the enum values then enum keys // they will all be of string type @@ -192,14 +190,12 @@ function toError(e: ServiceError): errors.ErrorPolykey { if (isNaN(parseInt(key)) && e.code === grpc.status[key]) { if ( key === 'UNKNOWN' && - errorName != null && - errorMessage != null && - errorData != null && - errorName in errors + errorData != null ) { - return new errors[errorName](errorMessage, { data: JSON.parse(errorData) }); + const error: Error = JSON.parse(errorData, reviver); + return new errors.ErrorPolykeyRemote(error.message, { cause: error }); } else { - return new grpcErrors.ErrorGRPCClientCall(e.message, { + return new grpcErrors.ErrorGRPCClientCall(e.message, { data: { code: e.code, details: e.details, @@ -212,6 +208,124 @@ function toError(e: ServiceError): errors.ErrorPolykey { never(); } +/** + * Replacer function for serialising errors over GRPC (used by `JSON.stringify` + * in `fromError`) + * Polykey errors are handled by their inbuilt `toJSON` method , so this only + * serialises other errors + */ + function replacer(key: string, value: any): any { + if (value instanceof AbstractError) { + // Include the standard properties from an AbstractError + return { + type: value.name, + data: { + description: value.description, + message: value.message, + timestamp: value.timestamp, + data: value.data, + cause: value.cause, + stack: value.stack, + } + } + } else if (value instanceof Error) { + // If it's some other type of error then only serialise the message and + // stack (and the type of the error) + return { + type: value.name, + data: { + message: value.message, + stack: value.stack, + } + } + } else { + // If it's not an error then just leave as is + return value; + } +} + +/** + * The same as `replacer`, however this will additionally filter out any + * sensitive data that should not be sent over the network when sending to an + * agent (as opposed to a client) + */ +function sensitiveReplacer(key: string, value: any) { + if (key === 'stack') { + return; + } else { + return replacer(key, value); + } +} + +/** + * Error constructors for non-Polykey errors + * Allows these errors to be reconstructed from GRPC metadata + */ +const otherErrors = { + 'Error': Error, + 'EvalError': EvalError, + 'RangeError': RangeError, + 'ReferenceError': ReferenceError, + 'SyntaxError': SyntaxError, + 'TypeError': TypeError, + 'URIError': URIError +}; + +/** + * Reviver function for deserialising errors sent over GRPC (used by + * `JSON.parse` in `toError`) + * The final result returned will always be an error - if the deserialised + * data is of an unknown type then this will be wrapped as an + * `ErrorPolykeyUnknown` + */ +function reviver(key: string, value: any): any { + // If the value is an error then reconstruct it + if (typeof value === 'object' && typeof value.type === 'string' && typeof value.data === 'object') { + const message = value.data.message ?? ''; + if (value.type in errors) { + const error = new errors[value.type]( + message, + { + timestamp: value.data.timestamp, + data: value.data.data, + cause: value.data.cause, + }, + ); + error.exitCode = value.data.exitCode; + if (value.data.stack) { + error.stack = value.data.stack; + } + return error; + } else if (value.type in otherErrors) { + const error = new otherErrors[value.type](message); + if (value.data.stack) { + error.stack = value.data.stack; + } + return error; + } else { + const error = new errors.ErrorPolykeyUnknown('', { data: value }); + if (value.data.stack) { + error.stack = value.data.stack; + } + return error; + } + } else if (key === '') { + // The value is not an error + const error = new errors.ErrorPolykeyUnknown('', { data: value }); + return error; + } else if (key === 'timestamp') { + // Encode timestamps + const timestampParsed = Date.parse(value); + if (!isNaN(timestampParsed)) { + return new Date(timestampParsed); + } else { + return undefined; + } + } else { + return value; + } +} + /** * Converts GRPC unary call to promisified unary call * Used on the client side @@ -326,12 +440,15 @@ function promisifyReadableStreamCall( */ function generatorWritable( stream: ClientWritableStream, + sensitive: boolean, ): AsyncGeneratorWritableStream>; function generatorWritable( stream: ServerWritableStream, + sensitive: boolean, ): AsyncGeneratorWritableStream>; function generatorWritable( stream: ClientWritableStream | ServerWritableStream, + sensitive: boolean = false, ) { const streamWrite = promisify(stream.write).bind(stream); const gf = async function* () { @@ -340,7 +457,7 @@ function generatorWritable( try { vW = yield; } catch (e) { - stream.emit('error', fromError(e)); + stream.emit('error', fromError(e, sensitive)); stream.end(); return; } @@ -393,6 +510,7 @@ function promisifyWritableStreamCall( }); const g = generatorWritable( stream, + false, ) as AsyncGeneratorWritableStreamClient< TWrite, ClientWritableStream @@ -412,15 +530,18 @@ function promisifyWritableStreamCall( */ function generatorDuplex( stream: ClientDuplexStream, + sensitive: boolean, ): AsyncGeneratorDuplexStream>; function generatorDuplex( stream: ServerDuplexStream, + sensitive: boolean, ): AsyncGeneratorDuplexStream>; function generatorDuplex( stream: ClientDuplexStream | ServerDuplexStream, + sensitive: boolean = false, ) { const gR = generatorReadable(stream as any); - const gW = generatorWritable(stream as any); + const gW = generatorWritable(stream as any, sensitive); const gf = async function* () { let vR: any, vW: any; while (true) { @@ -485,6 +606,7 @@ function promisifyDuplexStreamCall( }); const g = generatorDuplex( stream, + false, ) as AsyncGeneratorDuplexStreamClient< TRead, TWrite, diff --git a/tests/grpc/utils.test.ts b/tests/grpc/utils.test.ts index c17f0457d..b2390e2fa 100644 --- a/tests/grpc/utils.test.ts +++ b/tests/grpc/utils.test.ts @@ -4,6 +4,7 @@ import * as grpc from '@grpc/grpc-js'; import { getLogger } from '@grpc/grpc-js/build/src/logging'; import * as grpcUtils from '@/grpc/utils'; import * as grpcErrors from '@/grpc/errors'; +import * as errors from '@/errors'; import * as utilsPB from '@/proto/js/polykey/v1/utils/utils_pb'; import * as utils from './utils'; @@ -57,13 +58,14 @@ describe('GRPC utils', () => { const messageTo = new utilsPB.EchoMessage(); messageTo.setChallenge('error'); const pCall = unary(messageTo); - await expect(pCall).rejects.toThrow(grpcErrors.ErrorGRPC); + await expect(pCall).rejects.toThrow(errors.ErrorPolykeyRemote); try { await pCall; } catch (e) { // This information comes from the server expect(e.message).toBe('test error'); - expect(e.data).toMatchObject({ + expect(e.cause).toBeInstanceOf(grpcErrors.ErrorGRPC); + expect(e.cause.data).toMatchObject({ grpc: true, }); } @@ -103,7 +105,7 @@ describe('GRPC utils', () => { const messageTo = new utilsPB.EchoMessage(); messageTo.setChallenge(challenge); const stream = serverStream(messageTo); - await expect(() => stream.next()).rejects.toThrow(grpcErrors.ErrorGRPC); + await expect(() => stream.next()).rejects.toThrow(errors.ErrorPolykeyRemote); // The generator will have ended // the internal stream will be automatically destroyed const result = await stream.next(); @@ -274,7 +276,7 @@ describe('GRPC utils', () => { const messageTo = new utilsPB.EchoMessage(); messageTo.setChallenge('error'); await genDuplex.write(messageTo); - await expect(() => genDuplex.read()).rejects.toThrow(grpcErrors.ErrorGRPC); + await expect(() => genDuplex.read()).rejects.toThrow(errors.ErrorPolykeyRemote); expect(genDuplex.stream.destroyed).toBe(true); expect(genDuplex.stream.getPeer()).toBe(`127.0.0.1:${port}`); }); @@ -287,9 +289,123 @@ describe('GRPC utils', () => { const messageTo = new utilsPB.EchoMessage(); messageTo.setChallenge('error'); await expect(() => genDuplex.next(messageTo)).rejects.toThrow( - grpcErrors.ErrorGRPC, + errors.ErrorPolykeyRemote, ); expect(genDuplex.stream.destroyed).toBe(true); expect(genDuplex.stream.getPeer()).toBe(`127.0.0.1:${port}`); }); + test('serialising and deserialising Polykey errors', async () => { + const timestamp = new Date(); + const error = new errors.ErrorPolykey('test error', { + timestamp, + data: { + int: 1, + str: 'one', + }, + }); + error.exitCode = 255; + const serialised = grpcUtils.fromError(error).metadata!; + const stringifiedError = serialised.get('error')[0] as string; + const parsedError = JSON.parse(stringifiedError); + expect(parsedError).toMatchObject({ + type: 'ErrorPolykey', + data: expect.any(Object), + }); + const deserialisedError = grpcUtils.toError({ + name: '', + message: '', + code: 2, + details: '', + metadata: serialised + }); + expect(deserialisedError).toBeInstanceOf(errors.ErrorPolykeyRemote); + expect(deserialisedError.message).toBe('test error'); + expect(deserialisedError.cause).toBeInstanceOf(errors.ErrorPolykey); + expect(deserialisedError.cause.message).toBe('test error'); + expect(deserialisedError.cause.exitCode).toBe(255); + expect(deserialisedError.cause.timestamp).toEqual(timestamp); + expect(deserialisedError.cause.data).toEqual(error.data); + expect(deserialisedError.cause.stack).toBe(error.stack); + }); + test('serialising and deserialising generic errors', async () => { + const error = new TypeError('test error'); + const serialised = grpcUtils.fromError(error).metadata!; + const stringifiedError = serialised.get('error')[0] as string; + const parsedError = JSON.parse(stringifiedError); + expect(parsedError).toMatchObject({ + type: 'TypeError', + data: expect.any(Object), + }); + const deserialisedError = grpcUtils.toError({ + name: '', + message: '', + code: 2, + details: '', + metadata: serialised + }); + expect(deserialisedError).toBeInstanceOf(errors.ErrorPolykeyRemote); + expect(deserialisedError.message).toBe('test error'); + expect(deserialisedError.cause).toBeInstanceOf(TypeError); + expect(deserialisedError.cause.message).toBe('test error'); + expect(deserialisedError.cause.stack).toBe(error.stack); + }); + test('serialising and deserialising non-errors', async () => { + const error = 'not an error' as unknown as Error; + const serialised = grpcUtils.fromError(error).metadata!; + const stringifiedError = serialised.get('error')[0] as string; + const parsedError = JSON.parse(stringifiedError); + expect(parsedError).toEqual('not an error'); + const deserialisedError = grpcUtils.toError({ + name: '', + message: '', + code: 2, + details: '', + metadata: serialised + }); + expect(deserialisedError).toBeInstanceOf(errors.ErrorPolykeyRemote); + expect(deserialisedError.message).toBe(''); + expect(deserialisedError.cause).toBeInstanceOf(errors.ErrorPolykeyUnknown); + expect(deserialisedError.cause.message).toBe(''); + expect(deserialisedError.cause.data).toEqual('not an error'); + }); + test('serialising and deserialising sensitive errors', async () => { + const timestamp = new Date(); + const error = new errors.ErrorPolykey('test error', { + timestamp, + data: { + int: 1, + str: 'one', + }, + }); + error.exitCode = 255; + const serialised = grpcUtils.fromError(error, true).metadata!; + const stringifiedError = serialised.get('error')[0] as string; + const parsedError = JSON.parse(stringifiedError); + // Stack is the only thing that should not be serialised + expect(parsedError).toEqual({ + type: 'ErrorPolykey', + data: { + description: errors.ErrorPolykey.description, + message: 'test error', + exitCode: 255, + timestamp: expect.any(String), + data: error.data, + }, + }); + const deserialisedError = grpcUtils.toError({ + name: '', + message: '', + code: 2, + details: '', + metadata: serialised + }); + expect(deserialisedError).toBeInstanceOf(errors.ErrorPolykeyRemote); + expect(deserialisedError.message).toBe('test error'); + expect(deserialisedError.cause).toBeInstanceOf(errors.ErrorPolykey); + expect(deserialisedError.cause.message).toBe('test error'); + expect(deserialisedError.cause.exitCode).toBe(255); + expect(deserialisedError.cause.timestamp).toEqual(timestamp); + expect(deserialisedError.cause.data).toEqual(error.data); + expect(deserialisedError.cause.stack).not.toBe(error.stack); + }); }); diff --git a/tests/grpc/utils/testService.ts b/tests/grpc/utils/testService.ts index 5c3356d7f..1297c354c 100644 --- a/tests/grpc/utils/testService.ts +++ b/tests/grpc/utils/testService.ts @@ -43,7 +43,7 @@ function createTestService({ // we'll send back an error callback( grpcUtils.fromError( - new grpcErrors.ErrorGRPC('test error', { grpc: true }), + new grpcErrors.ErrorGRPC('test error', { data: { grpc: true } }), ), ); } else { @@ -67,13 +67,13 @@ function createTestService({ ); call.sendMetadata(meta); } - const genWritable = grpcUtils.generatorWritable(call); + const genWritable = grpcUtils.generatorWritable(call, false); const messageFrom = call.request; const messageTo = new utilsPB.EchoMessage(); const challenge = messageFrom.getChallenge(); if (challenge === 'error') { await genWritable.throw( - new grpcErrors.ErrorGRPC('test error', { grpc: true }), + new grpcErrors.ErrorGRPC('test error', { data: { grpc: true } }), ); } else { // Will send back a number of message @@ -131,7 +131,7 @@ function createTestService({ ); call.sendMetadata(meta); } - const genDuplex = grpcUtils.generatorDuplex(call); + const genDuplex = grpcUtils.generatorDuplex(call, false); const readStatus = await genDuplex.read(); // If nothing to read, end and destroy if (readStatus.done) { @@ -143,7 +143,7 @@ function createTestService({ const incomingMessage = readStatus.value; if (incomingMessage.getChallenge() === 'error') { await genDuplex.throw( - new grpcErrors.ErrorGRPC('test error', { grpc: true }), + new grpcErrors.ErrorGRPC('test error', { data: { grpc: true } }), ); } else { const outgoingMessage = new utilsPB.EchoMessage(); @@ -169,7 +169,7 @@ function createTestService({ // we'll send back an error callback( grpcUtils.fromError( - new grpcErrors.ErrorGRPC('test error', { grpc: true }), + new grpcErrors.ErrorGRPC('test error', { data: { grpc: true } }), ), ); } else { @@ -182,7 +182,7 @@ function createTestService({ serverStreamFail: async ( call: grpc.ServerWritableStream, ): Promise => { - const genWritable = grpcUtils.generatorWritable(call); + const genWritable = grpcUtils.generatorWritable(call, false); try { const echoMessage = new utilsPB.EchoMessage().setChallenge('Hello!'); for (let i = 0; i < 10; i++) {