diff --git a/src/adoption-code/sender.ts b/src/adoption-code/sender.ts index 4f718925..f8e37954 100644 --- a/src/adoption-code/sender.ts +++ b/src/adoption-code/sender.ts @@ -24,8 +24,8 @@ export class CodeSender { return sockets.find(socket => socket.data.probe.ipAddress === ip); } - private sendToSocket (sockets: RemoteProbeSocket, code: string) { - sockets.emit('probe:adoption:code', { + private sendToSocket (socket: RemoteProbeSocket, code: string) { + socket.emit('probe:adoption:code', { code, }); } diff --git a/src/lib/adopted-probes.ts b/src/lib/adopted-probes.ts index 1ce01c86..3b16a05e 100644 --- a/src/lib/adopted-probes.ts +++ b/src/lib/adopted-probes.ts @@ -11,20 +11,20 @@ const logger = scopedLogger('adopted-probes'); const TABLE_NAME = 'adopted_probes'; -type AdoptedProbe = { +export type AdoptedProbe = { ip: string; - uuid: string; + uuid?: string; lastSyncDate: string; - tags: string[]; - isCustomCity: boolean; - status: string; - version: string; - country: string; - city: string; - latitude: number; - longitude: number; - asn: number; - network: string; + tags?: string[]; + isCustomCity: number; + status?: string; + version?: string; + country?: string; + city?: string; + latitude?: number; + longitude?: number; + asn?: number; + network?: string; } export class AdoptedProbes { @@ -80,7 +80,7 @@ export class AdoptedProbes { this.syncDashboardData() .finally(() => this.scheduleSync()) .catch(error => logger.error(error)); - }, 60_000); + }, 10_000); } async syncDashboardData () { @@ -95,7 +95,7 @@ export class AdoptedProbes { await Bluebird.map(adoptedProbes, ({ ip, lastSyncDate }) => this.updateSyncDate(ip, lastSyncDate), { concurrency: 8 }); } - private async syncProbeIds (ip: string, uuid: string) { + private async syncProbeIds (ip: string, uuid?: string) { const connectedProbe = this.connectedIpToProbe.get(ip); if (connectedProbe && connectedProbe.uuid === uuid) { // ip and uuid are synced @@ -107,7 +107,7 @@ export class AdoptedProbes { return; } - const connectedIp = this.connectedUuidToIp.get(uuid); + const connectedIp = uuid && this.connectedUuidToIp.get(uuid); if (connectedIp) { // data was found by uuid, but not found by ip, therefore ip is outdated await this.updateIp(connectedIp, uuid); diff --git a/src/lib/ws/fetch-sockets.ts b/src/lib/ws/fetch-sockets.ts index 22483ca2..a6380f7c 100644 --- a/src/lib/ws/fetch-sockets.ts +++ b/src/lib/ws/fetch-sockets.ts @@ -1,18 +1,63 @@ import config from 'config'; import { throttle, LRUOptions } from './helper/throttle.js'; import { fetchRawSockets, RemoteProbeSocket } from './server.js'; -import { adoptedProbes } from '../adopted-probes.js'; +import { type AdoptedProbe, adoptedProbes } from '../adopted-probes.js'; const throttledFetchSockets = throttle( async () => { const connected = await fetchRawSockets(); const adopted = adoptedProbes.getAdoptedIpToProbe(); - console.log('adopted', adopted); - return connected; + const withAdoptedData = addAdoptedProbesData(connected, adopted); + return withAdoptedData; }, config.get('ws.fetchSocketsCacheTTL'), ); +const addAdoptedProbesData = (connectedProbes: RemoteProbeSocket[], AdoptedIpToProbe: Map) => { + return connectedProbes.map((connected) => { + const ip = connected.data.probe.ipAddress; + const adopted = AdoptedIpToProbe.get(ip); + + if (!adopted) { + return connected; + } + + const isCustomCity = adopted.isCustomCity; + const hasUserTags = adopted.tags && adopted.tags.length; + + if (!isCustomCity && !hasUserTags) { + return connected; + } + + const result = { + ...connected, + data: { + ...connected.data, + probe: { + ...connected.data.probe, + ...(isCustomCity && { + location: { + ...connected.data.probe.location, + city: adopted.city!, + latitude: adopted.latitude!, + longitude: adopted.longitude!, + }, + }), + ...((adopted.tags && adopted.tags.length) && { + tags: [ + ...connected.data.probe.tags, + ...adopted.tags.map(tag => ({ type: 'user' as const, value: `u-baderfall-${tag}` })), + ], + }), + } }, + } as RemoteProbeSocket; + + // TODO: Update index here + + return result; + }); +}; + export const fetchSockets = async (options?: LRUOptions) => { const sockets = await throttledFetchSockets(options); return sockets; diff --git a/src/probe/builder.ts b/src/probe/builder.ts index 5272af80..32677525 100644 --- a/src/probe/builder.ts +++ b/src/probe/builder.ts @@ -58,24 +58,7 @@ export const buildProbe = async (socket: Socket): Promise => { const tags = getTags(ip, ipInfo); - // Storing index as string[][] so every category will have it's exact position in the index array across all probes - const index = [ - [ location.country ], - [ getCountryIso3ByIso2(location.country) ], - [ getCountryByIso(location.country) ], - getCountryAliases(location.country), - [ location.normalizedCity ], - location.state ? [ location.state ] : [], - location.state ? [ getStateNameByIso(location.state) ] : [], - [ location.continent ], - getContinentAliases(location.continent), - [ location.normalizedRegion ], - getRegionAliases(location.normalizedRegion), - [ `as${location.asn}` ], - tags.filter(tag => tag.type === 'system').map(tag => tag.value), - [ location.normalizedNetwork ], - getNetworkAliases(location.normalizedNetwork), - ].map(category => category.map(s => s.toLowerCase().replaceAll('-', ' '))); + const index = getIndex(location, tags); // Todo: add validation and handle missing or partial data return { @@ -100,6 +83,29 @@ export const buildProbe = async (socket: Socket): Promise => { }; }; +const getIndex = (location: ProbeLocation, tags: Tag[]) => { + // Storing index as string[][] so every category will have it's exact position in the index array across all probes + const index = [ + [ location.country ], + [ getCountryIso3ByIso2(location.country) ], + [ getCountryByIso(location.country) ], + getCountryAliases(location.country), + [ location.normalizedCity ], + location.state ? [ location.state ] : [], + location.state ? [ getStateNameByIso(location.state) ] : [], + [ location.continent ], + getContinentAliases(location.continent), + [ location.normalizedRegion ], + getRegionAliases(location.normalizedRegion), + [ `as${location.asn}` ], + tags.filter(tag => tag.type === 'system').map(tag => tag.value), + [ location.normalizedNetwork ], + getNetworkAliases(location.normalizedNetwork), + ].map(category => category.map(s => s.toLowerCase().replaceAll('-', ' '))); + + return index; +}; + const getLocation = (ipInfo: ProbeLocation): ProbeLocation => ({ continent: ipInfo.continent, region: ipInfo.region, diff --git a/test/tests/unit/ws/fetch-sockets.test.ts b/test/tests/unit/ws/fetch-sockets.test.ts index c9fc4ff6..c5fbe09c 100644 --- a/test/tests/unit/ws/fetch-sockets.test.ts +++ b/test/tests/unit/ws/fetch-sockets.test.ts @@ -3,22 +3,142 @@ import * as td from 'testdouble'; import { expect } from 'chai'; const fetchRawSockets = sinon.stub(); +const getAdoptedIpToProbe = sinon.stub(); describe('fetchSockets', () => { let fetchSockets; before(async () => { - await td.replaceEsm('../../../../src/lib/ws/server.ts', { - fetchRawSockets, - }); - + await td.replaceEsm('../../../../src/lib/ws/server.ts', { fetchRawSockets }); + await td.replaceEsm('../../../../src/lib/adopted-probes.ts', { adoptedProbes: { getAdoptedIpToProbe } }); ({ fetchSockets } = await import('../../../../src/lib/ws/fetch-sockets.js')); }); + beforeEach(() => { + sinon.resetHistory(); + }); + after(() => { td.reset(); }); + it('should apply adopted probes data to the result sockets', async () => { + fetchRawSockets.resolves([{ + data: { + probe: { + client: 'pf2pER5jLnhxTgBqAAAB', + version: '0.26.0', + nodeVersion: 'v18.17.0', + uuid: 'c873cd81-5ede-4fff-9314-5905ad2bdb58', + ipAddress: '1.1.1.1', + host: '', + location: { + continent: 'EU', + region: 'Western Europe', + normalizedRegion: 'western europe', + country: 'FR', + state: undefined, + city: 'Paris', + normalizedCity: 'paris', + asn: 12876, + latitude: 48.8534, + longitude: 2.3488, + network: 'SCALEWAY S.A.S.', + normalizedNetwork: 'scaleway s.a.s.', + }, + index: [ + [ 'fr' ], + [ 'fra' ], + [ 'france' ], + [], + [ 'paris' ], + [], + [], + [ 'eu' ], + [ 'eu', 'europe' ], + [ 'western europe' ], + [ 'western europe', 'west europe' ], + [ 'as12876' ], + [ 'datacenter network' ], + [ 'scaleway s.a.s.' ], + [], + ], + resolvers: [ 'private' ], + tags: [{ type: 'system', value: 'datacenter-network' }], + stats: { cpu: { count: 8, load: [] }, jobs: { count: 0 } }, + status: 'ready', + }, + }, + }]); + + getAdoptedIpToProbe.returns(new Map([ [ '1.1.1.1', { + ip: '1.1.1.1', + uuid: 'c873cd81-5ede-4fff-9314-5905ad2bdb58', + lastSyncDate: '2023-10-29T21:00:00.000Z', + isCustomCity: 1, + tags: [ 'user-tag' ], + status: 'ready', + version: '0.26.0', + asn: 12876, + network: 'SCALEWAY S.A.S.', + country: 'FR', + city: 'Marseille', + latitude: 43.29695, + longitude: 5.38107, + }] ])); + + const result = await fetchSockets(); + expect(result).to.deep.equal([ + { + data: { + probe: { + client: 'pf2pER5jLnhxTgBqAAAB', + version: '0.26.0', + nodeVersion: 'v18.17.0', + uuid: 'c873cd81-5ede-4fff-9314-5905ad2bdb58', + ipAddress: '1.1.1.1', + host: '', + location: { + continent: 'EU', + region: 'Western Europe', + normalizedRegion: 'western europe', + country: 'FR', + state: undefined, + city: 'Marseille', + normalizedCity: 'paris', + asn: 12876, + latitude: 43.29695, + longitude: 5.38107, + network: 'SCALEWAY S.A.S.', + normalizedNetwork: 'scaleway s.a.s.', + }, + index: [ + [ 'fr' ], + [ 'fra' ], + [ 'france' ], + [], + [ 'marseille' ], + [], + [], + [ 'eu' ], + [ 'eu', 'europe' ], + [ 'western europe' ], + [ 'western europe', 'west europe' ], + [ 'as12876' ], + [ 'datacenter network', 'user tag' ], + [ 'scaleway s.a.s.' ], + [], + ], + resolvers: [ 'private' ], + tags: [{ type: 'system', value: 'datacenter-network' }, { type: 'user', value: 'u-baderfall-user-tag' }], + stats: { cpu: { count: 8, load: [] }, jobs: { count: 0 } }, + status: 'ready', + }, + }, + }, + ]); + }); + it('multiple calls to fetchSockets should result in one socket.io fetchSockets call', async () => { expect(fetchRawSockets.callCount).to.equal(0);