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(oob): support fetching shortened invitation urls #840

Merged
merged 50 commits into from
Jul 14, 2022
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
bd89bc2
Created messages for pickup-v2
TheTreek Apr 7, 2022
b7be29c
Formatting
TheTreek Apr 7, 2022
6d50301
Added handlers for pickup-v2 messages
KolbyRKunz Apr 8, 2022
924c30a
Formatting
KolbyRKunz Apr 8, 2022
6e9f655
send delivery-request through websockets
TheTreek Apr 12, 2022
7f21fd3
Added todo to fix forcing of websockets
TheTreek Apr 12, 2022
2e490cd
Force return_route for pickup-v2, fix delivery handler, general cleanup
TheTreek Apr 13, 2022
5573dcd
Added a limit on how many messages we can request from mediator
TheTreek Apr 13, 2022
fc3e380
Updated reciepnt modules to check for pickupV2
KolbyRKunz Apr 14, 2022
8af3669
put try/catch around message delivery, send status-request on websock…
TheTreek Apr 14, 2022
c611880
Created unit testing for MediatorRecipientService for pickupv2
KolbyRKunz Apr 15, 2022
9195ec5
merge conflicts
KolbyRKunz Apr 15, 2022
01452f6
Updated to PR requests
KolbyRKunz Apr 19, 2022
6cd31c2
Made the message receiver a property of message recipient module
KolbyRKunz Apr 20, 2022
d48dd60
Moved requestStatus into a service to make it internal for now
KolbyRKunz Apr 22, 2022
a51aa81
changed initiateImplicitPickup to openWebSocketAndPickup
KolbyRKunz Apr 27, 2022
85d4c96
initial commit for testing url shortening
KolbyRKunz Apr 28, 2022
5181d7d
updated fromURL for shortened URL
KolbyRKunz May 5, 2022
08d4106
added additional logic to handle shorten URL's that have been redirected
KolbyRKunz May 5, 2022
bb107e1
Improved url shortening
KolbyRKunz May 6, 2022
a359176
Url shortening working, RFC discussion needed
KolbyRKunz May 6, 2022
fb8e808
Merge branch 'main' into feature/shortUrl
KolbyRKunz May 20, 2022
56ca498
cloned url shortening into oob
KolbyRKunz May 20, 2022
d5be405
comments about not being able to follow RFC
KolbyRKunz May 20, 2022
c708a3c
updated query param for oob portion
KolbyRKunz May 20, 2022
f1c2ce1
Merge branch 'main' into feature/shortUrl
KolbyRKunz May 26, 2022
6dcb8f7
tested against oob and added version transformers
KolbyRKunz May 27, 2022
6731c78
formatting
KolbyRKunz May 27, 2022
3cc003e
merge conflicts
KolbyRKunz Jun 8, 2022
142d278
formatting
KolbyRKunz Jun 8, 2022
fd70c1a
initial easy fixes
KolbyRKunz Jun 9, 2022
35da251
refactor done, need to test and create unit tests
KolbyRKunz Jun 15, 2022
6007927
Json results can determine message type
KolbyRKunz Jun 16, 2022
06ed262
tests for out of band shorten invitations written
KolbyRKunz Jun 17, 2022
fb06a24
created tests for short URL
KolbyRKunz Jun 22, 2022
2316bb5
unit tests for shortenedUrls created and passing
KolbyRKunz Jun 24, 2022
6e63951
merged main
KolbyRKunz Jun 24, 2022
fe98b9a
added awaits that were removed to restore failing tests
KolbyRKunz Jun 27, 2022
2a1f496
Merge branch 'main' into feature/shortUrl
KolbyRKunz Jun 29, 2022
b62bf77
Created tests for the transform function
KolbyRKunz Jun 29, 2022
d7e3522
Merge branch 'main' into feature/shortUrl
KolbyRKunz Jul 5, 2022
6fb3672
added support for proper 302 redirects
KolbyRKunz Jul 7, 2022
9a051ec
created new method head to avoid breaking change and reverted awaits
KolbyRKunz Jul 8, 2022
4150f70
Merge branch 'main' into feature/shortUrl
KolbyRKunz Jul 8, 2022
048073c
Url shortening requested chagnes
KolbyRKunz Jul 11, 2022
54e6a80
update for check types
KolbyRKunz Jul 11, 2022
692ebdf
added access to short url parsing in the out of band module, also upd…
KolbyRKunz Jul 13, 2022
3489e11
spelling
KolbyRKunz Jul 13, 2022
26d38c8
updated connectionInvitationMessage throw tag
KolbyRKunz Jul 14, 2022
c9dcac8
Merge branch 'main' into feature/shortUrl
KolbyRKunz Jul 14, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -122,20 +122,19 @@ export class ConnectionInvitationMessage extends AgentMessage {
* @param invitationUrl invitation url containing c_i or d_m parameter
*
* @throws Error when url can not be decoded to JSON, or decoded message is not a valid `ConnectionInvitationMessage`
* @throws Error when the url does not contain c_i or d_m as parameter
* @throws Error when the url is invalid encoded url or shortened url is invalid
KolbyRKunz marked this conversation as resolved.
Show resolved Hide resolved
KolbyRKunz marked this conversation as resolved.
Show resolved Hide resolved
*/
public static fromUrl(invitationUrl: string) {
const parsedUrl = parseUrl(invitationUrl).query
const encodedInvitation = parsedUrl['c_i'] ?? parsedUrl['d_m']

if (typeof encodedInvitation === 'string') {
const invitationJson = JsonEncoder.fromBase64(encodedInvitation)
const invitation = JsonTransformer.fromJSON(invitationJson, ConnectionInvitationMessage)

return invitation
} else {
throw new AriesFrameworkError(
'InvitationUrl is invalid. It needs to contain one, and only one, of the following parameters; `c_i` or `d_m`'
'InvitationUrl is invalid. Needs to be encrypted with either c_i or d_m or must be valid shortened URL'
KolbyRKunz marked this conversation as resolved.
Show resolved Hide resolved
)
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/modules/oob/OutOfBandModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ export class OutOfBandModule {
* @param config configuration of how out-of-band invitation should be processed
* @returns out-of-band record and connection record if one has been created
*/
public receiveInvitationFromUrl(invitationUrl: string, config: ReceiveOutOfBandInvitationConfig = {}) {
public async receiveInvitationFromUrl(invitationUrl: string, config: ReceiveOutOfBandInvitationConfig = {}) {
const message = this.parseInvitation(invitationUrl)
return this.receiveInvitation(message, config)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,45 @@ import { JsonTransformer } from '../../../utils/JsonTransformer'
import { OutOfBandInvitation } from '../messages/OutOfBandInvitation'

describe('OutOfBandInvitation', () => {
describe('transformHandshakeProtocols', () => {
KolbyRKunz marked this conversation as resolved.
Show resolved Hide resolved
const defaultJson = {
'@type': 'https://didcomm.org/out-of-band/1.1/invitation',
services: ['did:sov:LjgpST2rjsoxYegQDRm7EL'],
'@id': '69212a3a-d068-4f9d-a2dd-4741bca89af3',
label: 'Faber College',
goal_code: 'issue-vc',
goal: 'To issue a Faber College Graduate credential',
}

test('Undefined handshake protocols', () => {
const json = { ...defaultJson }

const invitation = JsonTransformer.fromJSON(json, OutOfBandInvitation)

expect(invitation.handshakeProtocols).toBe(undefined)
})

test('Defined handshake protocols', () => {
const json = {
...defaultJson,
handshake_protocols: ['https://didcomm.org/connections/1.0'],
}
const invitation = JsonTransformer.fromJSON(json, OutOfBandInvitation)

expect(invitation.handshakeProtocols).toStrictEqual(['https://didcomm.org/connections/1.0'])
})

test('Legacy handshake protocols', () => {
const json = {
...defaultJson,
handshake_protocols: ['did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0'],
}
const invitation = JsonTransformer.fromJSON(json, OutOfBandInvitation)

expect(invitation.handshakeProtocols).toStrictEqual(['https://didcomm.org/connections/1.0'])
})
})

describe('toUrl', () => {
test('encode the message into the URL containing the base64 encoded invitation as the oob query parameter', async () => {
const domain = 'https://example.com/ssi'
Expand Down
2 changes: 0 additions & 2 deletions packages/core/src/modules/oob/messages/OutOfBandInvitation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ export class OutOfBandInvitation extends AgentMessage {
public static fromUrl(invitationUrl: string) {
const parsedUrl = parseUrl(invitationUrl).query
const encodedInvitation = parsedUrl['oob']

if (typeof encodedInvitation === 'string') {
const invitationJson = JsonEncoder.fromBase64(encodedInvitation)
const invitation = this.fromJson(invitationJson)
Expand Down Expand Up @@ -123,7 +122,6 @@ export class OutOfBandInvitation extends AgentMessage {
public readonly goal?: string

public readonly accept?: string[]

@Transform(({ value }) => value?.map(replaceLegacyDidSovPrefix), { toClassOnly: true })
@Expose({ name: 'handshake_protocols' })
public handshakeProtocols?: HandshakeProtocol[]
Expand Down
114 changes: 114 additions & 0 deletions packages/core/src/utils/__tests__/shortenedUrl.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import type { Response } from 'node-fetch'

// eslint-disable-next-line import/no-extraneous-dependencies
import { Headers } from 'node-fetch'
KolbyRKunz marked this conversation as resolved.
Show resolved Hide resolved

import { ConnectionInvitationMessage } from '../../modules/connections'
import { OutOfBandInvitation } from '../../modules/oob'
import { convertToNewInvitation } from '../../modules/oob/helpers'
import { JsonTransformer } from '../JsonTransformer'
import { MessageValidator } from '../MessageValidator'
import { fromShortUrl } from '../parseInvitation'

const mockOobInvite = {
'@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.0/invitation',
'@id': '764af259-8bb4-4546-b91a-924c912d0bb8',
label: 'Alice',
handshake_protocols: ['did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0'],
services: ['did:sov:MvTqVXCEmJ87usL9uQTo7v'],
}

const mockConnectionInvite = {
'@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/invitation',
'@id': '20971ef0-1029-46db-a25b-af4c465dd16b',
label: 'test',
serviceEndpoint: 'http://sour-cow-15.tun1.indiciotech.io',
recipientKeys: ['5Gvpf9M4j7vWpHyeTyvBKbjYe7qWc72kGo6qZaLHkLrd'],
}

const header = new Headers()

header.append('Content-Type', 'application/json')

const dummyHeader = new Headers()

const mockedResponseOobJson = {
status: 200,
ok: true,
json: async () => ({
'@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.0/invitation',
'@id': '764af259-8bb4-4546-b91a-924c912d0bb8',
label: 'Alice',
handshake_protocols: ['did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0'],
services: ['did:sov:MvTqVXCEmJ87usL9uQTo7v'],
}),
} as Response

mockedResponseOobJson['headers'] = header

const mockedResponseOobUrl = {
status: 200,
ok: true,
url: 'https://wonderful-rabbit-5.tun2.indiciotech.io?oob=eyJAdHlwZSI6ICJkaWQ6c292OkJ6Q2JzTlloTXJqSGlxWkRUVUFTSGc7c3BlYy9vdXQtb2YtYmFuZC8xLjAvaW52aXRhdGlvbiIsICJAaWQiOiAiNzY0YWYyNTktOGJiNC00NTQ2LWI5MWEtOTI0YzkxMmQwYmI4IiwgImxhYmVsIjogIkFsaWNlIiwgImhhbmRzaGFrZV9wcm90b2NvbHMiOiBbImRpZDpzb3Y6QnpDYnNOWWhNcmpIaXFaRFRVQVNIZztzcGVjL2Nvbm5lY3Rpb25zLzEuMCJdLCAic2VydmljZXMiOiBbImRpZDpzb3Y6TXZUcVZYQ0VtSjg3dXNMOXVRVG83diJdfQ====',
} as Response

mockedResponseOobUrl['headers'] = dummyHeader

const mockedResponseConnectionJson = {
status: 200,
ok: true,
json: async () => ({
'@type': 'did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/invitation',
'@id': '20971ef0-1029-46db-a25b-af4c465dd16b',
label: 'test',
serviceEndpoint: 'http://sour-cow-15.tun1.indiciotech.io',
recipientKeys: ['5Gvpf9M4j7vWpHyeTyvBKbjYe7qWc72kGo6qZaLHkLrd'],
}),
} as Response

mockedResponseConnectionJson['headers'] = header

const mockedResponseConnectionUrl = {
status: 200,
ok: true,
url: 'http://sour-cow-15.tun1.indiciotech.io?c_i=eyJAdHlwZSI6ICJkaWQ6c292OkJ6Q2JzTlloTXJqSGlxWkRUVUFTSGc7c3BlYy9jb25uZWN0aW9ucy8xLjAvaW52aXRhdGlvbiIsICJAaWQiOiAiMjA5NzFlZjAtMTAyOS00NmRiLWEyNWItYWY0YzQ2NWRkMTZiIiwgImxhYmVsIjogInRlc3QiLCAic2VydmljZUVuZHBvaW50IjogImh0dHA6Ly9zb3VyLWNvdy0xNS50dW4xLmluZGljaW90ZWNoLmlvIiwgInJlY2lwaWVudEtleXMiOiBbIjVHdnBmOU00ajd2V3BIeWVUeXZCS2JqWWU3cVdjNzJrR282cVphTEhrTHJkIl19',
} as Response

mockedResponseConnectionUrl['headers'] = dummyHeader

let outOfBandInvitationMock: OutOfBandInvitation
let connectionInvitationMock: ConnectionInvitationMessage
let connectionInvitationToNew: OutOfBandInvitation

beforeAll(async () => {
outOfBandInvitationMock = await JsonTransformer.fromJSON(mockOobInvite, OutOfBandInvitation)
await MessageValidator.validateSync(outOfBandInvitationMock)
connectionInvitationMock = await JsonTransformer.fromJSON(mockConnectionInvite, ConnectionInvitationMessage)
await MessageValidator.validateSync(connectionInvitationMock)
connectionInvitationToNew = convertToNewInvitation(connectionInvitationMock)
})

describe('shortUrlOobJson', () => {
test('oobToJson', async () => {
const short = await fromShortUrl(mockedResponseOobJson)
expect(short).toEqual(outOfBandInvitationMock)
})
})
describe('shortUrlOobUrl', () => {
test('oobFromUrl', async () => {
const short = await fromShortUrl(mockedResponseOobUrl)
expect(short).toEqual(outOfBandInvitationMock)
})
})
describe('shortUrlConnectionJson', () => {
test('connectionInvitationToJson', async () => {
const short = await fromShortUrl(mockedResponseConnectionJson)
expect(short).toEqual(connectionInvitationToNew)
})
})
describe('shortUrlConnectionUrl', () => {
test('connectionFromUrl', async () => {
const short = await fromShortUrl(mockedResponseConnectionUrl)
expect(short).toEqual(connectionInvitationToNew)
})
})
KolbyRKunz marked this conversation as resolved.
Show resolved Hide resolved
112 changes: 112 additions & 0 deletions packages/core/src/utils/parseInvitation.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,122 @@
import type { AgentDependencies } from '../agent/AgentDependencies'
import type { Response } from 'node-fetch'

import { parseUrl } from 'query-string'

import { AriesFrameworkError } from '../error'
import { ConnectionInvitationMessage } from '../modules/connections'
import { convertToNewInvitation } from '../modules/oob/helpers'
import { OutOfBandInvitation } from '../modules/oob/messages'

import { JsonEncoder } from './JsonEncoder'
import { JsonTransformer } from './JsonTransformer'
import { MessageValidator } from './MessageValidator'

const fetchShortUrl = async (invitationUrl: string, dependencies: AgentDependencies) => {
// eslint-disable-next-line no-restricted-globals
const abortController = new AbortController()
KolbyRKunz marked this conversation as resolved.
Show resolved Hide resolved
const id = setTimeout(() => abortController.abort(), 15000)
let response
try {
response = await dependencies.fetch(invitationUrl, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
})
} catch (error) {
throw new AriesFrameworkError('Get request failed on provided Url')
KolbyRKunz marked this conversation as resolved.
Show resolved Hide resolved
}
clearTimeout(id)
return response
}

//This currently does not follow the RFC because of issues with fetch, currently uses a janky work around
export const fromShortUrl = async (response: Response): Promise<OutOfBandInvitation> => {
KolbyRKunz marked this conversation as resolved.
Show resolved Hide resolved
if (response) {
if (response.headers.get('Content-Type') === 'application/json' && response.ok) {
const invitationJson = await response.json()
if (invitationJson['@type'].includes('out-of-band')) {
const invitation = JsonTransformer.fromJSON(invitationJson, OutOfBandInvitation)

await MessageValidator.validateSync(invitation)

return invitation
} else {
const invitation = JsonTransformer.fromJSON(invitationJson, ConnectionInvitationMessage)

await MessageValidator.validateSync(invitation)

return convertToNewInvitation(invitation)
}
} else if (response['url']) {
KolbyRKunz marked this conversation as resolved.
Show resolved Hide resolved
// The following if else is for here for trinsic shorten urls
// Because the redirect targets a deep link the automatic redirect does not occur
let parsedUrl
const location = response.headers.get('Location')
if ((response.status === 302 || response.status === 301) && location) parsedUrl = parseUrl(location).query
else parsedUrl = parseUrl(response['url']).query
KolbyRKunz marked this conversation as resolved.
Show resolved Hide resolved

if (parsedUrl['oob']) {
const encodedInvitation = parsedUrl['oob']
let invitationJson = null
if (typeof encodedInvitation === 'string') invitationJson = JsonEncoder.fromBase64(encodedInvitation)
const invitation = JsonTransformer.fromJSON(invitationJson, OutOfBandInvitation)

await MessageValidator.validateSync(invitation)

return invitation
} else {
if (parsedUrl['c_i'] || parsedUrl['d_m']) {
const encodedInvitation = parsedUrl['c_i'] ?? parsedUrl['d_m']
let invitationJson = null
if (typeof encodedInvitation === 'string') invitationJson = JsonEncoder.fromBase64(encodedInvitation)
const invitation = JsonTransformer.fromJSON(invitationJson, ConnectionInvitationMessage)

await MessageValidator.validateSync(invitation)

return convertToNewInvitation(invitation)
}
throw new AriesFrameworkError(
'InvitationUrl is invalid. Needs to be encrypted with either c_i, d_m, or oob or must be valid shortened URL'
)
}
KolbyRKunz marked this conversation as resolved.
Show resolved Hide resolved
}
}
throw new AriesFrameworkError('HTTP request time out or did not receive valid response')
}

/**
* Parses URL containing encoded invitation and returns invitation message.
*
* @param invitationUrl URL containing encoded invitation
*
* @param dependencies Agent dependicies containing fetch
*
* @returns OutOfBandInvitation
*/
export const parseInvitationShortUrl = async (
KolbyRKunz marked this conversation as resolved.
Show resolved Hide resolved
invitationUrl: string,
dependencies: AgentDependencies
): Promise<OutOfBandInvitation> => {
const parsedUrl = parseUrl(invitationUrl).query
if (parsedUrl['oob']) {
const outOfBandInvitation = OutOfBandInvitation.fromUrl(invitationUrl)
return outOfBandInvitation
} else if (parsedUrl['c_i'] || parsedUrl['d_m']) {
const invitation = ConnectionInvitationMessage.fromUrl(invitationUrl)
return convertToNewInvitation(invitation)
} else {
try {
return fromShortUrl(await fetchShortUrl(invitationUrl, dependencies))
} catch (error) {
throw new AriesFrameworkError(
'InvitationUrl is invalid. It needs to contain one, and only one, of the following parameters: `oob`, `c_i` or `d_m`, or be valid shortened URL'
)
}
}
}
/**
* Parses URL containing encoded invitation and returns invitation message.
*
Expand Down