Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add problem report protocol #560

Merged
merged 9 commits into from
Dec 13, 2021
31 changes: 22 additions & 9 deletions packages/core/src/agent/Dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Lifecycle, scoped } from 'tsyringe'
import { AgentConfig } from '../agent/AgentConfig'
import { AriesFrameworkError } from '../error/AriesFrameworkError'

import { ProblemReportMessage } from './../modules/problem-reports/messages/ProblemReportMessage'
import { EventEmitter } from './EventEmitter'
import { AgentEventTypes } from './Events'
import { MessageSender } from './MessageSender'
Expand Down Expand Up @@ -45,15 +46,27 @@ class Dispatcher {
try {
outboundMessage = await handler.handle(messageContext)
} catch (error) {
this.logger.error(`Error handling message with type ${message.type}`, {
message: message.toJSON(),
error,
senderVerkey: messageContext.senderVerkey,
recipientVerkey: messageContext.recipientVerkey,
connectionId: messageContext.connection?.id,
})

throw error
const problemReportMessage = error.problemReport

if (problemReportMessage instanceof ProblemReportMessage && messageContext.connection) {
problemReportMessage.setThread({
threadId: messageContext.message.threadId,
})
outboundMessage = {
payload: problemReportMessage,
connection: messageContext.connection,
}
} else {
this.logger.error(`Error handling message with type ${message.type}`, {
message: message.toJSON(),
error,
senderVerkey: messageContext.senderVerkey,
recipientVerkey: messageContext.recipientVerkey,
connectionId: messageContext.connection?.id,
})

throw error
}
}

if (outboundMessage && isOutboundServiceMessage(outboundMessage)) {
Expand Down
72 changes: 64 additions & 8 deletions packages/core/src/agent/MessageReceiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,29 @@ import { Lifecycle, scoped } from 'tsyringe'

import { AriesFrameworkError } from '../error'
import { ConnectionService } from '../modules/connections/services/ConnectionService'
import { ProblemReportError, ProblemReportMessage } from '../modules/problem-reports'
import { JsonTransformer } from '../utils/JsonTransformer'
import { MessageValidator } from '../utils/MessageValidator'
import { replaceLegacyDidSovPrefixOnMessage } from '../utils/messageType'

import { CommonMessageType } from './../modules/common/messages/CommonMessageType'
import { AgentConfig } from './AgentConfig'
import { Dispatcher } from './Dispatcher'
import { EnvelopeService } from './EnvelopeService'
import { MessageSender } from './MessageSender'
import { TransportService } from './TransportService'
import { createOutboundMessage } from './helpers'
import { InboundMessageContext } from './models/InboundMessageContext'

export enum ProblemReportReason {
MessageParseFailure = 'message-parse-failure',
TimoGlastra marked this conversation as resolved.
Show resolved Hide resolved
}
@scoped(Lifecycle.ContainerScoped)
export class MessageReceiver {
private config: AgentConfig
private envelopeService: EnvelopeService
private transportService: TransportService
private messageSender: MessageSender
private connectionService: ConnectionService
private dispatcher: Dispatcher
private logger: Logger
Expand All @@ -33,12 +41,14 @@ export class MessageReceiver {
config: AgentConfig,
envelopeService: EnvelopeService,
transportService: TransportService,
messageSender: MessageSender,
connectionService: ConnectionService,
dispatcher: Dispatcher
) {
this.config = config
this.envelopeService = envelopeService
this.transportService = transportService
this.messageSender = messageSender
this.connectionService = connectionService
this.dispatcher = dispatcher
this.logger = this.config.logger
Expand Down Expand Up @@ -84,15 +94,12 @@ export class MessageReceiver {
unpackedMessage.message
)

const message = await this.transformMessage(unpackedMessage)
let message: AgentMessage | null = null
try {
await MessageValidator.validate(message)
message = await this.transformMessage(unpackedMessage)
await this.validateMessage(message)
} catch (error) {
this.logger.error(`Error validating message ${message.type}`, {
errors: error,
message: message.toJSON(),
})

if (connection) await this.sendProblemReportMessage(error.message, connection, unpackedMessage)
throw error
}

Expand Down Expand Up @@ -174,12 +181,61 @@ export class MessageReceiver {
const MessageClass = this.dispatcher.getMessageClassForType(messageType)

if (!MessageClass) {
throw new AriesFrameworkError(`No message class found for message type "${messageType}"`)
throw new ProblemReportError(`No message class found for message type "${messageType}"`, {
problemCode: ProblemReportReason.MessageParseFailure,
})
}

// Cast the plain JSON object to specific instance of Message extended from AgentMessage
const message = JsonTransformer.fromJSON(unpackedMessage.message, MessageClass)

return message
}

/**
* Validate an AgentMessage instance.
* @param message agent message to validate
*/
private async validateMessage(message: AgentMessage) {
try {
await MessageValidator.validate(message)
} catch (error) {
this.logger.error(`Error validating message ${message.type}`, {
errors: error,
message: message.toJSON(),
})
throw new ProblemReportError(`Error validating message ${message.type}`, {
problemCode: ProblemReportReason.MessageParseFailure,
})
}
}

/**
* Send the problem report message (https://didcomm.org/notification/1.0/problem-report) to the recipient.
* @param message error message to send
* @param connection connection to send the message to
* @param unpackedMessage received unpackedMessage
*/
private async sendProblemReportMessage(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should add an check here whether the received message is of @type == https://didcomm.org/notification/1.0/problem-reportand otherwise throw an error (not a problem report error) that we are not sending a problem report as response to a problem. to prevent accidential problem report loops from happening. We should probably never respond with a problem report to a problem report.

message: string,
connection: ConnectionRecord,
unpackedMessage: UnpackedMessageContext
) {
if (unpackedMessage.message['@type'] === CommonMessageType.ProblemReport) {
throw new AriesFrameworkError(message)
}
const problemReportMessage = new ProblemReportMessage({
description: {
en: message,
code: ProblemReportReason.MessageParseFailure,
},
})
problemReportMessage.setThread({
threadId: unpackedMessage.message['@id'],
})
const outboundMessage = createOutboundMessage(connection, problemReportMessage)
if (outboundMessage) {
await this.messageSender.sendMessage(outboundMessage)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ describe('Decorators | Signature | SignatureDecoratorUtils', () => {
try {
await unpackAndVerifySignatureDecorator(wronglySignedData, wallet)
} catch (error) {
expect(error.message).toEqual('Signature is not valid!')
expect(error.message).toEqual('Signature is not valid')
}
})
})
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Wallet } from '../../wallet/Wallet'

import { AriesFrameworkError } from '../../error'
import { ConnectionProblemReportError, ConnectionProblemReportReason } from '../../modules/connections/errors'
import { BufferEncoder } from '../../utils/BufferEncoder'
import { JsonEncoder } from '../../utils/JsonEncoder'
import { Buffer } from '../../utils/buffer'
Expand Down Expand Up @@ -29,7 +29,9 @@ export async function unpackAndVerifySignatureDecorator(
const isValid = await wallet.verify(signerVerkey, signedData, signature)

if (!isValid) {
throw new AriesFrameworkError('Signature is not valid!')
throw new ConnectionProblemReportError('Signature is not valid', {
problemCode: ConnectionProblemReportReason.RequestProcessingError,
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it too low-level area to care about a connection here? Throwing Signature is not valid! and handling it at the level above seems reasonable to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    let connectionJson = null
    try {
      connectionJson = await unpackAndVerifySignatureDecorator(message.connectionSig, this.wallet)
    } catch (error) {
      if(error instanceof AriesFrameworkError) {
        throw new ConnectionProblemReportError(error.message, {
          problemCode: ConnectionProblemReportReason.RequestProcessingError,
        })
      }
    }

Is it the right way to handle it in connection service? or any other suggestion @jakubkoci @TimoGlastra

}

// TODO: return Connection instance instead of raw json
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export enum CommonMessageType {
Ack = 'https://didcomm.org/notification/1.0/ack',
ProblemReport = 'https://didcomm.org/notification/1.0/problem-report',
TimoGlastra marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { ConnectionProblemReportReason } from '.'
import type { ProblemReportErrorOptions } from '../../problem-reports'

import { ProblemReportError } from '../../problem-reports'
import { ConnectionProblemReportMessage } from '../messages'

interface ConnectionProblemReportErrorOptions extends ProblemReportErrorOptions {
problemCode: ConnectionProblemReportReason
}
export class ConnectionProblemReportError extends ProblemReportError {
public problemReport: ConnectionProblemReportMessage

public constructor(public message: string, { problemCode }: ConnectionProblemReportErrorOptions) {
super(message, { problemCode })
this.problemReport = new ConnectionProblemReportMessage({
description: {
en: message,
code: problemCode,
},
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Connection error code in RFC 160.
*
* @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0160-connection-protocol/README.md#errors
*/
export enum ConnectionProblemReportReason {
RequestNotAccepted = 'request_not_accepted',
RequestProcessingError = 'request_processing_error',
ResponseNotAccepted = 'response_not_accepted',
ResponseProcessingError = 'response_processing_error',
}
2 changes: 2 additions & 0 deletions packages/core/src/modules/connections/errors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './ConnectionProblemReportError'
export * from './ConnectionProblemReportReason'
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Handler, HandlerInboundMessage } from '../../../agent/Handler'
import type { ConnectionService } from '../services'

import { ConnectionProblemReportMessage } from '../messages'

export class ConnectionProblemReportHandler implements Handler {
private connectionService: ConnectionService
public supportedMessages = [ConnectionProblemReportMessage]

public constructor(connectionService: ConnectionService) {
this.connectionService = connectionService
}

public async handle(messageContext: HandlerInboundMessage<ConnectionProblemReportHandler>) {
await this.connectionService.processProblemReport(messageContext)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { ProblemReportMessageOptions } from '../../problem-reports/messages/ProblemReportMessage'

import { Equals } from 'class-validator'

import { ProblemReportMessage } from '../../problem-reports/messages/ProblemReportMessage'

export type ConnectionProblemReportMessageOptions = ProblemReportMessageOptions

/**
* @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't find the connection problem report handler.

export class ConnectionProblemReportMessage extends ProblemReportMessage {
/**
* Create new ConnectionProblemReportMessage instance.
* @param options
*/
public constructor(options: ConnectionProblemReportMessageOptions) {
super(options)
}

@Equals(ConnectionProblemReportMessage.type)
public readonly type = ConnectionProblemReportMessage.type
public static readonly type = 'https://didcomm.org/connection/1.0/problem-report'
}
1 change: 1 addition & 0 deletions packages/core/src/modules/connections/messages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './ConnectionRequestMessage'
export * from './ConnectionResponseMessage'
export * from './TrustPingMessage'
export * from './TrustPingResponseMessage'
export * from './ConnectionProblemReportMessage'
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export enum ConnectionState {
Requested = 'requested',
Responded = 'responded',
Complete = 'complete',
None = 'none',
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface ConnectionRecordProps {
imageUrl?: string
multiUseInvitation: boolean
mediatorId?: string
errorMsg?: string
}

export type CustomConnectionTags = TagsBase
Expand Down Expand Up @@ -68,6 +69,7 @@ export class ConnectionRecord

public threadId?: string
public mediatorId?: string
public errorMsg?: string

public static readonly type = 'ConnectionRecord'
public readonly type = ConnectionRecord.type
Expand All @@ -94,6 +96,7 @@ export class ConnectionRecord
this.imageUrl = props.imageUrl
this.multiUseInvitation = props.multiUseInvitation
this.mediatorId = props.mediatorId
this.errorMsg = props.errorMsg
}
}

Expand Down
Loading