Skip to content

Commit

Permalink
fix: make sure local peer's address in in invitation link (#2268)
Browse files Browse the repository at this point in the history
* fix: make sure local peer's address is in the invitation link

* fix: running e2e user profile test on ci
  • Loading branch information
EmiM authored Feb 5, 2024
1 parent 11ae203 commit 53f1ec9
Show file tree
Hide file tree
Showing 16 changed files with 150 additions and 101 deletions.
1 change: 1 addition & 0 deletions .github/workflows/e2e-win.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ jobs:
with:
timeout_minutes: 25
max_attempts: 3
shell: bash
command: cd packages/e2e-tests && npm run test userProfile.test.ts

- name: Run invitation link test - Includes 2 separate application clients
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
[unreleased]

# Fixes:

* Make sure address of the inviting peer is in the invitation link

[2.1.1]

# Fixes:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ describe('ConnectionsManagerService', () => {
})

it('launches community on init if its data exists in local db', async () => {
const remotePeer = createLibp2pAddress(
'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd',
'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE'
)
const launchCommunityPayload: InitCommunityPayload = {
id: community.id,
peerId: userIdentity.peerId,
Expand All @@ -102,12 +106,7 @@ describe('ConnectionsManagerService', () => {
key: userIdentity.userCsr?.userKey,
CA: [communityRootCa],
},
peers: [
createLibp2pAddress(
'y7yczmugl2tekami7sbdz5pfaemvx7bahwthrdvcbzw5vex2crsr26qd',
'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE'
),
],
peers: [remotePeer],
}

await localDbService.put(LocalDBKeys.COMMUNITY, launchCommunityPayload)
Expand All @@ -116,7 +115,11 @@ describe('ConnectionsManagerService', () => {
const launchCommunitySpy = jest.spyOn(connectionsManagerService, 'launchCommunity').mockResolvedValue()

await connectionsManagerService.init()
expect(launchCommunitySpy).toHaveBeenCalledWith(launchCommunityPayload)

const localPeerAddress = createLibp2pAddress(userIdentity.hiddenService.onionAddress, userIdentity.peerId.id)
const updatedLaunchCommunityPayload = { ...launchCommunityPayload, peers: [localPeerAddress, remotePeer] }

expect(launchCommunitySpy).toHaveBeenCalledWith(updatedLaunchCommunityPayload)
})

it('does not launch community on init if its data does not exist in local db', async () => {
Expand Down
8 changes: 4 additions & 4 deletions packages/backend/src/nest/libp2p/libp2p.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { mplex } from '@libp2p/mplex'
import { multiaddr } from '@multiformats/multiaddr'
import { Inject, Injectable } from '@nestjs/common'
import { createLibp2pAddress, createLibp2pListenAddress } from '@quiet/common'
import { ConnectionProcessInfo, PeerId, SocketActionTypes } from '@quiet/types'
import { ConnectionProcessInfo, type NetworkDataPayload, PeerId, SocketActionTypes } from '@quiet/types'
import crypto from 'crypto'
import { EventEmitter } from 'events'
import { Agent } from 'https'
Expand Down Expand Up @@ -188,12 +188,12 @@ export class Libp2pService extends EventEmitter {

this.connectedPeers.delete(remotePeerId)
this.logger(`${localPeerId} is connected to ${this.connectedPeers.size} peers`)

this.emit(Libp2pEvents.PEER_DISCONNECTED, {
const peerStat: NetworkDataPayload = {
peer: remotePeerId,
connectionDuration,
lastSeen: connectionEndTime,
})
}
this.emit(Libp2pEvents.PEER_DISCONNECTED, peerStat)
})

await this.processInChunksService.process()
Expand Down
15 changes: 11 additions & 4 deletions packages/backend/src/nest/local-db/local-db.service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Inject, Injectable } from '@nestjs/common'
import { Level } from 'level'
import { NetworkStats } from '@quiet/types'
import { filterAndSortPeers } from '@quiet/common'
import { InitCommunityPayload, NetworkStats } from '@quiet/types'
import { createLibp2pAddress, filterAndSortPeers } from '@quiet/common'
import { LEVEL_DB } from '../const'
import { LocalDBKeys, LocalDbStatus } from './local-db.types'
import Logger from '../common/logger'
import { create } from 'mock-fs/lib/filesystem'

@Injectable()
export class LocalDbService {
Expand Down Expand Up @@ -35,7 +36,7 @@ export class LocalDbService {
try {
data = await this.db.get(key)
} catch (e) {
this.logger.error(`Getting '${key}'`, e)
this.logger(`Getting '${key}'`, e)
return null
}
return data
Expand Down Expand Up @@ -74,7 +75,13 @@ export class LocalDbService {
public async getSortedPeers(peers: string[] = []): Promise<string[]> {
const peersStats = (await this.get(LocalDBKeys.PEERS)) || {}
const stats: NetworkStats[] = Object.values(peersStats)
return filterAndSortPeers(peers, stats)
const community: InitCommunityPayload = await this.get(LocalDBKeys.COMMUNITY)
if (!community) {
return filterAndSortPeers(peers, stats)
}
const localPeerAddress = createLibp2pAddress(community.hiddenService.onionAddress, community.peerId.id)
this.logger('Local peer', localPeerAddress)
return filterAndSortPeers(peers, stats, localPeerAddress)
}

public async putOwnerOrbitDbIdentity(id: string): Promise<void> {
Expand Down
14 changes: 13 additions & 1 deletion packages/common/src/libp2p.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { filterAndSortPeers } from './sortPeers'

describe('filterValidAddresses', () => {
it('filters out invalid addresses', () => {
const localAddress =
'/dns4/f3lupwnhaqplbn4djaut5rtipwmlotlb57flfvjzgexek2yezlpjddid.onion/tcp/443/ws/p2p/Qmd35TsAvtskei8zWY3A65ifNWcY4x4SdqkQDHMkH5xPF9'
const valid = [
'/dns4/gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad.onion/tcp/443/ws/p2p/QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE',
'/dns4/gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad.onion/tcp/80/ws/p2p/QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE',
Expand All @@ -15,6 +17,16 @@ describe('filterValidAddresses', () => {
'/dns4/gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrj.onion/tcp/443/ws/p2p/QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE',
'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbK',
]
expect(filterAndSortPeers(addresses, [])).toEqual(valid)
expect(filterAndSortPeers(addresses, [], localAddress)).toEqual([localAddress, ...valid])
})

it('sets local address as first without duplicating it', () => {
const localAddress =
'/dns4/gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad.onion/tcp/80/ws/p2p/QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE'
const addresses = [
localAddress,
'/dns4/gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad.onion/tcp/443/ws/p2p/QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSE',
]
expect(filterAndSortPeers(addresses, [], localAddress)).toEqual([localAddress, addresses[1]])
})
})
17 changes: 13 additions & 4 deletions packages/common/src/sortPeers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ This is the very simple algorithm for evaluating the most wanted peers.
2. Two sorted arrays are created - one sorted by last seen and other by most uptime shared.
3. Arrays are merged taking one element from list one and one element from the second list. Duplicates are ommited
4. We end up with mix of last seen and most uptime descending array of peers, the it is enchanced to libp2p address.
5. Prioritize local peer if given
*/
export const filterAndSortPeers = (peersAddresses: string[], stats: NetworkStats[]): string[] => {
export const filterAndSortPeers = (
peersAddresses: string[],
stats: NetworkStats[],
localPeerAddress?: string
): string[] => {
peersAddresses = filterValidAddresses(peersAddresses)
const lastSeenSorted = [...stats].sort((a, b) => {
return b.lastSeen - a.lastSeen
Expand Down Expand Up @@ -44,8 +49,12 @@ export const filterAndSortPeers = (peersAddresses: string[], stats: NetworkStats
})
})

return peerList
.concat(peersAddresses)
.filter(address => address !== null)
return [
...new Set([
localPeerAddress, // Set local peer as first
...peerList.concat(peersAddresses),
]),
]
.filter(address => address !== null && address !== '')
.filter(isDefined)
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { FC, useState } from 'react'
import { useSelector } from 'react-redux'
import { communities } from '@quiet/state-manager'
import { connection } from '@quiet/state-manager'
import { InviteComponent } from './Invite.component'

export const Invite: FC = () => {
const invitationLink = useSelector(communities.selectors.invitationUrl)
const invitationLink = useSelector(connection.selectors.invitationUrl)

const [revealInputValue, setRevealInputValue] = useState<boolean>(false)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React from 'react'
import { useSelector } from 'react-redux'
import { communities } from '@quiet/state-manager'
import { connection } from '@quiet/state-manager'

import { QRCodeComponent } from './QRCode.component'
import { Site } from '@quiet/common'

export const QRCode: React.FC = () => {
const invitationLink = useSelector(communities.selectors.invitationUrl) || Site.MAIN_PAGE
const invitationLink = useSelector(connection.selectors.invitationUrl) || Site.MAIN_PAGE
return <QRCodeComponent value={invitationLink} />
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Share } from 'react-native'

import Clipboard from '@react-native-clipboard/clipboard'

import { communities } from '@quiet/state-manager'
import { connection } from '@quiet/state-manager'

import { navigationSelectors } from '../../../store/navigation/navigation.selectors'

Expand All @@ -21,7 +21,7 @@ export const InvitationContextMenu: FC = () => {
const dispatch = useDispatch()

const screen = useSelector(navigationSelectors.currentScreen)
const invitationLink = useSelector(communities.selectors.invitationUrl)
const invitationLink = useSelector(connection.selectors.invitationUrl)

const invitationContextMenu = useContextMenu(MenuName.Invitation)

Expand Down
4 changes: 2 additions & 2 deletions packages/mobile/src/screens/QRCode/QRCode.screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { FC, useCallback, useRef } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import Share from 'react-native-share'
import SVG from 'react-native-svg'
import { communities } from '@quiet/state-manager'
import { connection } from '@quiet/state-manager'
import { navigationActions } from '../../store/navigation/navigation.slice'
import { ScreenNames } from '../../const/ScreenNames.enum'

Expand All @@ -14,7 +14,7 @@ export const QRCodeScreen: FC = () => {

const svgRef = useRef<SVG>()

const invitationLink = useSelector(communities.selectors.invitationUrl) || Site.MAIN_PAGE
const invitationLink = useSelector(connection.selectors.invitationUrl) || Site.MAIN_PAGE

const handleBackButton = useCallback(() => {
dispatch(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { type Store } from '@reduxjs/toolkit'
import { getFactory } from '../../utils/tests/factories'
import { prepareStore } from '../../utils/tests/prepareStore'
import { connectionSelectors } from './connection.selectors'
import { type communitiesActions } from '../communities/communities.slice'
import { communitiesActions } from '../communities/communities.slice'
import { connectionActions } from './connection.slice'
import { type FactoryGirl } from 'factory-girl'
import { type Community } from '@quiet/types'
import { createLibp2pAddress, invitationShareUrl } from '@quiet/common'

describe('communitiesSelectors', () => {
setupCrypto()
Expand All @@ -31,7 +32,7 @@ describe('communitiesSelectors', () => {
],
})

// This peer should be first in the list as it is the most revently seen one.
// This peer should be first in the list as it is the most recently seen one.
store.dispatch(
connectionActions.updateNetworkData({
peer: 'Qmd35TsAvtskei8zWY3A65ifNWcY4x4SdqkQDHMkH5xPF9',
Expand Down Expand Up @@ -91,4 +92,47 @@ describe('communitiesSelectors', () => {

expect(socketIOSecret2).toEqual(secret)
})

it('invitationUrl selector does not break if there is no community', () => {
const { store } = prepareStore()
const invitationUrl = connectionSelectors.invitationUrl(store.getState())
expect(invitationUrl).toEqual('')
})

it('invitationUrl selector returns proper url', async () => {
const { store } = prepareStore()
const factory = await getFactory(store)
const peerList = [
createLibp2pAddress(
'gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad',
'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSA'
),
]
const psk = '12345'
const ownerOrbitDbIdentity = 'testOwnerOrbitDbIdentity'
await factory.create<ReturnType<typeof communitiesActions.addNewCommunity>['payload']>('Community', {
peerList,
ownerOrbitDbIdentity,
})
store.dispatch(communitiesActions.savePSK(psk))
const selectorInvitationUrl = connectionSelectors.invitationUrl(store.getState())
const expectedUrl = invitationShareUrl(peerList, psk, ownerOrbitDbIdentity)
expect(expectedUrl).not.toEqual('')
expect(selectorInvitationUrl).toEqual(expectedUrl)
})

it('invitationUrl selector returns empty string if state lacks psk', async () => {
const { store } = prepareStore()
const factory = await getFactory(store)
await factory.create<ReturnType<typeof communitiesActions.addNewCommunity>['payload']>('Community', {
peerList: [
createLibp2pAddress(
'gloao6h5plwjy4tdlze24zzgcxll6upq2ex2fmu2ohhyu4gtys4nrjad',
'QmZoiJNAvCffeEHBjk766nLuKVdkxkAT7wfFJDPPLsbKSA'
),
],
})
const invitationUrl = connectionSelectors.invitationUrl(store.getState())
expect(invitationUrl).toEqual('')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { StoreKeys } from '../store.keys'
import { createSelector } from 'reselect'
import { type CreatedSelectors, type StoreState } from '../store.types'
import { allUsers, areCertificatesLoaded } from '../users/users.selectors'
import { communitiesSelectors } from '../communities/communities.selectors'
import { peersStatsAdapter } from './connection.adapter'
import { connectedPeers, isCurrentCommunityInitialized } from '../network/network.selectors'
import { type NetworkStats } from './connection.types'
import { type User } from '../users/users.types'
import { filterAndSortPeers } from '@quiet/common'
import { filterAndSortPeers, invitationShareUrl } from '@quiet/common'
import { areMessagesLoaded, areChannelsLoaded } from '../publicChannels/publicChannels.selectors'
import { identitySelectors } from '../identity/identity.selectors'
import { communitiesSelectors } from '../communities/communities.selectors'

const connectionSlice: CreatedSelectors[StoreKeys.Connection] = (state: StoreState) => state[StoreKeys.Connection]

Expand All @@ -22,21 +23,38 @@ export const connectionProcess = createSelector(connectionSlice, reducerState =>

export const socketIOSecret = createSelector(connectionSlice, reducerState => reducerState.socketIOSecret)

const peerStats = createSelector(connectionSlice, reducerState => {
let stats: NetworkStats[]
if (reducerState.peersStats === undefined) {
stats = []
} else {
stats = peersStatsAdapter.getSelectors().selectAll(reducerState.peersStats)
}
return stats
})

export const peerList = createSelector(
connectionSlice,
communitiesSelectors.currentCommunity,
(reducerState, community) => {
identitySelectors.currentPeerAddress,
peerStats,
(community, localPeerAddress, stats) => {
if (!community) return []
const arr = [...(community.peerList || [])]

let stats: NetworkStats[]
if (reducerState.peersStats === undefined) {
stats = []
} else {
stats = peersStatsAdapter.getSelectors().selectAll(reducerState.peersStats)
}
const arr = [...(community.peerList || [])]
return filterAndSortPeers(arr, stats, localPeerAddress)
}
)

return filterAndSortPeers(arr, stats)
export const invitationUrl = createSelector(
communitiesSelectors.psk,
communitiesSelectors.ownerOrbitDbIdentity,
peerList,
(communityPsk, ownerOrbitDbIdentity, sortedPeerList) => {
if (!sortedPeerList || sortedPeerList?.length === 0) return ''
if (!communityPsk) return ''
if (!ownerOrbitDbIdentity) return ''
const initialPeers = sortedPeerList.slice(0, 3)
return invitationShareUrl(initialPeers, communityPsk, ownerOrbitDbIdentity)
}
)

Expand Down Expand Up @@ -70,6 +88,7 @@ export const connectionSelectors = {
lastConnectedTime,
connectedPeersMapping,
peerList,
invitationUrl,
torBootstrapProcess,
connectionProcess,
isTorInitialized,
Expand Down
Loading

0 comments on commit 53f1ec9

Please sign in to comment.