Skip to content

Commit

Permalink
Refactor invitation url parsing functions
Browse files Browse the repository at this point in the history
  • Loading branch information
EmiM committed Oct 18, 2023
1 parent 30baa07 commit ba678ee
Show file tree
Hide file tree
Showing 14 changed files with 119 additions and 109 deletions.
20 changes: 10 additions & 10 deletions packages/common/src/invitationCode.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { InvitationData } from '@quiet/types'
import {
argvInvitationCode,
invitationDeepUrl,
composeInvitationDeepUrl,
invitationShareUrl,
pairsToInvitationShareUrl,
retrieveInvitationCode,
composeInvitationShareUrl,
parseInvitationCodeDeepUrl,
} from './invitationCode'
import { QUIET_JOIN_PAGE, Site } from './static'

Expand All @@ -28,14 +28,14 @@ describe('Invitation code helper', () => {
'zbay://invalid',
'quiet://invalid',
'quiet://?param=invalid',
invitationDeepUrl(expectedCodes),
composeInvitationDeepUrl(expectedCodes),
])
expect(result).toEqual(expectedCodes)
})

it('builds proper invitation deep url', () => {
expect(
invitationDeepUrl({
composeInvitationDeepUrl({
pairs: [
{ peerId: 'peerID1', onionAddress: 'address1' },
{ peerId: 'peerID2', onionAddress: 'address2' },
Expand All @@ -54,7 +54,7 @@ describe('Invitation code helper', () => {
psk: '12345',
}
const expected = `${QUIET_JOIN_PAGE}#peerID1=address1&peerID2=address2&${Site.PSK_PARAM_KEY}=${pairs.psk}`
expect(pairsToInvitationShareUrl(pairs)).toEqual(expected)
expect(composeInvitationShareUrl(pairs)).toEqual(expected)
})

it('builds proper invitation share url', () => {
Expand All @@ -70,8 +70,8 @@ describe('Invitation code helper', () => {
})

it('retrieves invitation codes from deep url', () => {
const psk = 12345
const codes = retrieveInvitationCode(
const psk = '12345'
const codes = parseInvitationCodeDeepUrl(
`quiet://?${peerId1}=${address1}&${peerId2}=${address2}&${Site.PSK_PARAM_KEY}=${psk}`
)
expect(codes).toEqual({
Expand All @@ -87,9 +87,9 @@ describe('Invitation code helper', () => {
const psk = '12345'
const peerId2 = 'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLs'
const address2 = 'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd'
const codes = retrieveInvitationCode(
const parsed = parseInvitationCodeDeepUrl(
`quiet://?${peerId1}=${address1}&${peerId2}=${address2}&${Site.PSK_PARAM_KEY}=${psk}`
)
expect(codes).toEqual({ pairs: [{ peerId: peerId1, onionAddress: address1 }], psk: psk })
expect(parsed).toEqual({ pairs: [{ peerId: peerId1, onionAddress: address1 }], psk: psk })
})
})
106 changes: 42 additions & 64 deletions packages/common/src/invitationCode.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,38 @@
import { InvitationData, InvitationPair } from '@quiet/types'
import { ONION_ADDRESS_REGEX, PEER_ID_REGEX, Site } from './static'
import { ONION_ADDRESS_REGEX, PEER_ID_REGEX, QUIET_JOIN_PAGE, Site } from './static'
import { createLibp2pAddress } from './libp2p'

export const retrieveInvitationCode = (url: string): InvitationData => {
// TODO: rename to parseDeepUrl?
/**
* Extract invitation data from deep url.
* Valid format: quiet://?<peerid1>=<address1>&<peerid2>=<addresss2>&k=<psk>
*/
let data: URL
const parseDeepUrl = ({ url, expectedProtocol = `quiet:` }: { url: string; expectedProtocol?: string }) => {
let _url = url
let validUrl: URL | null = null

if (!expectedProtocol) {
expectedProtocol = `${Site.DEEP_URL_SCHEME}:`
_url = `${Site.DEEP_URL_SCHEME}://?${url}`
}

try {
data = new URL(url)
validUrl = new URL(_url)
} catch (e) {
console.error(`Could not retrieve invitation code from deep url ${url}. Reason: ${e.message}`)
throw e
}
if (!data || data.protocol !== 'quiet:') {
if (!validUrl || validUrl.protocol !== expectedProtocol) {
console.error(`Could not retrieve invitation code from deep url ${url}`)
throw new Error()
}
const params = data.searchParams
const params = validUrl.searchParams
const codes: InvitationPair[] = []
let psk = params.get(Site.PSK_PARAM_KEY)
if (!psk) throw new Error(`No psk found in invitation code ${url}`)

psk = decodeURIComponent(psk) // TODO: can be dangerous?
psk = decodeURIComponent(psk)
// Validate base64

params.delete(Site.PSK_PARAM_KEY)

params.forEach((peerId, onionAddress) => {
if (!invitationCodeValid(peerId, onionAddress)) return
params.forEach((onionAddress, peerId) => {
if (!peerDataValid({ peerId, onionAddress })) return
codes.push({
peerId,
onionAddress,
Expand All @@ -43,6 +45,21 @@ export const retrieveInvitationCode = (url: string): InvitationData => {
}
}

export const parseInvitationCodeDeepUrl = (url: string): InvitationData => {
/**
* Extract invitation data from deep url.
* Valid format: quiet://?<peerid1>=<address1>&<peerid2>=<addresss2>&k=<psk>
*/
return parseDeepUrl({ url })
}

export const parseInvitationCode = (code: string): InvitationData => {
/**
* @param code <peerId1>=<address1>&<peerId2>=<address2>&k=<psk>
*/
return parseDeepUrl({ url: code, expectedProtocol: '' })
}

export const invitationShareUrl = (peers: string[] = [], psk: string): string => {
// TODO: rename to 'composeInvitationShareUrl'
/**
Expand Down Expand Up @@ -75,9 +92,8 @@ export const invitationShareUrl = (peers: string[] = [], psk: string): string =>
pairs.push({ peerId: peerId, onionAddress: rawAddress })
}

const _url = pairsToInvitationShareUrl({ pairs: pairs, psk: psk })
const _url = composeInvitationShareUrl({ pairs: pairs, psk: psk })

// const _url = `${Site.MAIN_PAGE}${Site.JOIN_PAGE}#${pairs.join('&')}&${psk}`
console.log('invitationShareUrl', _url)
const url = new URL(_url)
return url.href
Expand All @@ -91,19 +107,16 @@ export const pairsToP2pAddresses = (pairs: InvitationPair[]): string[] => {
return addresses
}

export const pairsToInvitationShareUrl = (data: InvitationData) => {
const url = new URL(`${Site.MAIN_PAGE}${Site.JOIN_PAGE}`)
for (const pair of data.pairs) {
url.searchParams.append(pair.peerId, pair.onionAddress)
}
url.searchParams.append(Site.PSK_PARAM_KEY, data.psk)
return url.href.replace('?', '#')
export const composeInvitationShareUrl = (data: InvitationData) => {
return composeInvitationUrl(`${QUIET_JOIN_PAGE}`, data).replace('?', '#')
}

export const invitationDeepUrl = (data: InvitationData): string => {
// TODO: rename to 'composeInvitationDeepUrl'
// TODO: refactor - unify with pairsToInvitationShareUrl
const url = new URL('quiet://')
export const composeInvitationDeepUrl = (data: InvitationData): string => {
return composeInvitationUrl(`${Site.DEEP_URL_SCHEME_WITH_SEPARATOR}`, data)
}

const composeInvitationUrl = (baseUrl: string, data: InvitationData): string => {
const url = new URL(baseUrl)
for (const pair of data.pairs) {
url.searchParams.append(pair.peerId, pair.onionAddress)
}
Expand All @@ -118,7 +131,7 @@ export const argvInvitationCode = (argv: string[]): InvitationData | null => {
let invitationData: InvitationData | null = null
for (const arg of argv) {
try {
invitationData = retrieveInvitationCode(arg)
invitationData = parseInvitationCodeDeepUrl(arg)
} catch (e) {
continue
}
Expand All @@ -131,7 +144,7 @@ export const argvInvitationCode = (argv: string[]): InvitationData | null => {
return invitationData
}

export const invitationCodeValid = (peerId: string, onionAddress: string): boolean => {
export const peerDataValid = ({ peerId, onionAddress }: { peerId: string; onionAddress: string }): boolean => {
// TODO: rename to peerDataValid?
if (!peerId.match(PEER_ID_REGEX)) {
// TODO: test it more properly e.g with PeerId.createFromB58String(peerId.trim())
Expand All @@ -144,38 +157,3 @@ export const invitationCodeValid = (peerId: string, onionAddress: string): boole
}
return true
}

export const getInvitationPairs = (code: string): InvitationData => {
/**
* @param code <peerId1>=<address1>&<peerId2>=<address2>&k=<psk>
*/

// TODO: refactor - use parametrized retrieveInvitationCode instead
const elements = code.split('&')
if (elements.length <= 1) throw new Error(`Invitation link '${code}' has not enough data`)
const pairs = elements.slice(0, -1)

// TODO: Verify psk format
const _psk = elements.slice(-1)[0]
const psk = decodeURIComponent(_psk.split('=')[1]) // FIXME

const codes: InvitationPair[] = []
for (const pair of pairs) {
const [peerId, address] = pair.split('=')
if (!peerId || !address) continue
if (!invitationCodeValid(peerId, address)) continue
codes.push({
peerId: peerId,
onionAddress: address,
})
}

console.log('getInvitationPairs', {
pairs: codes,
psk: psk,
})
return {
pairs: codes,
psk: psk,
}
}
2 changes: 2 additions & 0 deletions packages/common/src/static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ export const ONION_ADDRESS_REGEX = /^[a-z0-9]{56}$/g
export const PEER_ID_REGEX = /^[a-zA-Z0-9]{46}$/g

export enum Site {
DEEP_URL_SCHEME_WITH_SEPARATOR = 'quiet://',
DEEP_URL_SCHEME = 'quiet',
DOMAIN = 'tryquiet.org',
MAIN_PAGE = 'https://tryquiet.org/',
JOIN_PAGE = 'join',
Expand Down
Binary file added packages/desktop/libssl.so
Binary file not shown.
6 changes: 3 additions & 3 deletions packages/desktop/src/main/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { autoUpdater } from 'electron-updater'
import { BrowserWindow, app, ipcMain, Menu } from 'electron'
import { waitFor } from '@testing-library/dom'
import path from 'path'
import { invitationDeepUrl } from '@quiet/common'
import { composeInvitationDeepUrl } from '@quiet/common'
import { InvitationData } from '@quiet/types'

// eslint-disable-next-line
Expand Down Expand Up @@ -254,13 +254,13 @@ describe('Invitation code', () => {

expect(mockAppOnCalls[1][0]).toBe('open-url')
const event = { preventDefault: () => {} }
mockAppOnCalls[1][1](event, invitationDeepUrl(codes))
mockAppOnCalls[1][1](event, composeInvitationDeepUrl(codes))
expect(mockWindowWebContentsSend).toHaveBeenCalledWith('invitation', { codes })
})

it('process invitation code on second-instance event', async () => {
await mockAppOnCalls[2][1]()
const commandLine = ['/tmp/.mount_Quiet-TVQc6s/quiet', invitationDeepUrl(codes)]
const commandLine = ['/tmp/.mount_Quiet-TVQc6s/quiet', composeInvitationDeepUrl(codes)]
expect(mockAppOnCalls[0][0]).toBe('second-instance')
const event = { preventDefault: () => {} }
mockAppOnCalls[0][1](event, commandLine)
Expand Down
6 changes: 3 additions & 3 deletions packages/desktop/src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { Crypto } from '@peculiar/webcrypto'
import logger from './logger'
import { DATA_DIR, DEV_DATA_DIR } from '../shared/static'
import { fork, ChildProcess } from 'child_process'
import { argvInvitationCode, getFilesData, retrieveInvitationCode } from '@quiet/common'
import { argvInvitationCode, getFilesData, parseInvitationCodeDeepUrl } from '@quiet/common'
import { updateDesktopFile, processInvitationCode } from './invitation'
const ElectronStore = require('electron-store')
ElectronStore.initRenderer()
Expand Down Expand Up @@ -147,7 +147,7 @@ app.on('open-url', (event, url) => {
event.preventDefault()
if (mainWindow) {
invitationUrl = null
const invitationCode = retrieveInvitationCode(url) // TODO: handle thrown error
const invitationCode = parseInvitationCodeDeepUrl(url) // TODO: handle thrown error
processInvitationCode(mainWindow, invitationCode)
}
})
Expand Down Expand Up @@ -474,7 +474,7 @@ app.on('ready', async () => {
throw new Error(`mainWindow is on unexpected type ${mainWindow}`)
}
if (process.platform === 'darwin' && invitationUrl) {
const invitationCode = retrieveInvitationCode(invitationUrl) // TODO: handle thrown error
const invitationCode = parseInvitationCodeDeepUrl(invitationUrl) // TODO: handle thrown error
processInvitationCode(mainWindow, invitationCode)
invitationUrl = null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { InviteLinkErrors } from '../../forms/fieldsErrors'
import { IconButton, InputAdornment } from '@mui/material'
import VisibilityOff from '@mui/icons-material/VisibilityOff'
import Visibility from '@mui/icons-material/Visibility'
import { ONION_ADDRESS_REGEX, pairsToInvitationShareUrl, parseName } from '@quiet/common'
import { ONION_ADDRESS_REGEX, composeToInvitationShareUrl, parseName } from '@quiet/common'
import { getInvitationCodes } from '@quiet/state-manager'

const PREFIX = 'PerformCommunityActionComponent'
Expand Down Expand Up @@ -221,7 +221,7 @@ export const PerformCommunityActionComponent: React.FC<PerformCommunityActionPro
useEffect(() => {
if (communityOwnership === CommunityOwnership.User && invitationCode?.length && psk) {
setFormSent(true)
setValue('name', pairsToInvitationShareUrl({ pairs: invitationCode, psk: psk }))
setValue('name', composeToInvitationShareUrl({ pairs: invitationCode, psk: psk }))
}
}, [communityOwnership, invitationCode])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import { InviteComponent } from './Tabs/Invite/Invite.component'
import { LeaveCommunityComponent } from './Tabs/LeaveCommunity/LeaveCommunityComponent'
import { Typography } from '@mui/material'
import { QRCodeComponent } from './Tabs/QRCode/QRCode.component'
import { pairsToInvitationShareUrl } from '@quiet/common'
import { composeToInvitationShareUrl } from '@quiet/common'

const invitationLink = pairsToInvitationShareUrl({
const invitationLink = composeToInvitationShareUrl({
pairs: [
{
peerId: 'QmVTkUad2Gq3MkCa8gf12R1gsWDfk2yiTEqb6YGXDG2iQ3',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import '@testing-library/jest-dom'
import React from 'react'
import { renderComponent } from '../../../../testUtils/renderComponent'
import { InviteComponent } from './Invite.component'
import { pairsToInvitationShareUrl } from '@quiet/common'
import { composeToInvitationShareUrl } from '@quiet/common'

describe('CopyLink', () => {
it('renderComponent - hidden long link', () => {
const invitationLink = pairsToInvitationShareUrl({
const invitationLink = composeToInvitationShareUrl({
pairs: [
{
peerId: 'QmVTkUad2Gq3MkCa8gf12R1gsWDfk2yiTEqb6YGXDG2iQ3',
Expand Down Expand Up @@ -127,7 +127,7 @@ describe('CopyLink', () => {
})

it('renderComponent - revealed short link', () => {
const invitationLink = pairsToInvitationShareUrl({
const invitationLink = composeToInvitationShareUrl({
pairs: [
{
peerId: 'QmVTkUad2Gq3MkCa8gf12R1gsWDfk2yiTEqb6YGXDG2iQ3',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import { withTheme } from '../../../../storybook/decorators'
import { InviteComponent, InviteComponentProps } from './Invite.component'
import { pairsToInvitationShareUrl } from '@quiet/common'
import { composeToInvitationShareUrl } from '@quiet/common'

const Template: ComponentStory<typeof InviteComponent> = args => {
return <InviteComponent {...args} />
}

export const Component = Template.bind({})
let revealInputValue = true
const invitationLink = pairsToInvitationShareUrl({
const invitationLink = composeToInvitationShareUrl({
pairs: [
{
peerId: 'QmVTkUad2Gq3MkCa8gf12R1gsWDfk2yiTEqb6YGXDG2iQ3',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { ComponentStory, ComponentMeta } from '@storybook/react'

import { withTheme } from '../../../../storybook/decorators'
import QRCodeComponent, { QRCodeProps } from './QRCode.component'
import { pairsToInvitationShareUrl } from '@quiet/common'
import { composeToInvitationShareUrl } from '@quiet/common'

const invitationLink = pairsToInvitationShareUrl({
const invitationLink = composeToInvitationShareUrl({
pairs: [
{
peerId: 'QmVTkUad2Gq3MkCa8gf12R1gsWDfk2yiTEqb6YGXDG2iQ3',
Expand Down
Loading

0 comments on commit ba678ee

Please sign in to comment.