diff --git a/backend/.env.dist b/backend/.env.dist index bf783b1d2..b238388f6 100644 --- a/backend/.env.dist +++ b/backend/.env.dist @@ -67,5 +67,4 @@ EVENT_PROTOCOL_DISABLED=false # on an hash created from this topic # FEDERATION_DHT_TOPIC=GRADIDO_HUB # FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f -# FEDERATION_COMMUNITY_ACTIVATE_ENDPOINTS=true # FEDERATION_COMMUNITY_URL=http://localhost:4000/api diff --git a/backend/.env.template b/backend/.env.template index e1e17320f..f73b87353 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -60,5 +60,4 @@ EVENT_PROTOCOL_DISABLED=$EVENT_PROTOCOL_DISABLED # Federation FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC FEDERATION_DHT_SEED=$FEDERATION_DHT_SEED -FEDERATION_COMMUNITY_ACTIVATE_ENDPOINTS=$FEDERATION_COMMUNITY_ACTIVATE_ENDPOINTS FEDERATION_COMMUNITY_URL=$FEDERATION_COMMUNITY_URL diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index e7f73c11b..698b17e67 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -120,8 +120,6 @@ if ( const federation = { FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || null, FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null, - FEDERATION_COMMUNITY_ACTIVATE_ENDPOINTS: - process.env.FEDERATION_COMMUNITY_ACTIVATE_ENDPOINTS === 'true' || false, FEDERATION_COMMUNITY_URL: process.env.FEDERATION_COMMUNITY_URL === undefined ? null diff --git a/federation/src/config/index.ts b/federation/src/config/index.ts index 7694d5e4a..05136beef 100644 --- a/federation/src/config/index.ts +++ b/federation/src/config/index.ts @@ -11,14 +11,14 @@ Decimal.set({ */ const constants = { - DB_VERSION: '0056-add_communities_table', + DB_VERSION: '0058-add_communities_table', DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info LOG_LEVEL: process.env.LOG_LEVEL || 'info', CONFIG_VERSION: { DEFAULT: 'DEFAULT', - EXPECTED: 'v13.2022-11-25', + EXPECTED: 'v14.2022-11-22', CURRENT: '', }, } @@ -40,16 +40,7 @@ const database = { DB_DATABASE: process.env.DB_DATABASE || 'gradido_community', TYPEORM_LOGGING_RELATIVE_PATH: process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.backend.log', } -/* -const klicktipp = { - KLICKTIPP: process.env.KLICKTIPP === 'true' || false, - KLICKTTIPP_API_URL: process.env.KLICKTIPP_API_URL || 'https://api.klicktipp.com', - KLICKTIPP_USER: process.env.KLICKTIPP_USER || 'gradido_test', - KLICKTIPP_PASSWORD: process.env.KLICKTIPP_PASSWORD || 'secret321', - KLICKTIPP_APIKEY_DE: process.env.KLICKTIPP_APIKEY_DE || 'SomeFakeKeyDE', - KLICKTIPP_APIKEY_EN: process.env.KLICKTIPP_APIKEY_EN || 'SomeFakeKeyEN', -} -*/ + const community = { COMMUNITY_NAME: process.env.COMMUNITY_NAME || 'Gradido Entwicklung', COMMUNITY_URL: process.env.COMMUNITY_URL || 'http://localhost/', @@ -60,42 +51,6 @@ const community = { COMMUNITY_DESCRIPTION: process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.', } -/* -const loginServer = { - LOGIN_APP_SECRET: process.env.LOGIN_APP_SECRET || '21ffbbc616fe', - LOGIN_SERVER_KEY: process.env.LOGIN_SERVER_KEY || 'a51ef8ac7ef1abf162fb7a65261acd7a', -} - -const email = { - EMAIL: process.env.EMAIL === 'true' || false, - EMAIL_TEST_MODUS: process.env.EMAIL_TEST_MODUS === 'true' || false, - EMAIL_TEST_RECEIVER: process.env.EMAIL_TEST_RECEIVER || 'stage1@gradido.net', - EMAIL_USERNAME: process.env.EMAIL_USERNAME || 'gradido_email', - EMAIL_SENDER: process.env.EMAIL_SENDER || 'info@gradido.net', - EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || 'xxx', - EMAIL_SMTP_URL: process.env.EMAIL_SMTP_URL || 'gmail.com', - EMAIL_SMTP_PORT: process.env.EMAIL_SMTP_PORT || '587', - EMAIL_LINK_VERIFICATION: - process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/checkEmail/{optin}{code}', - EMAIL_LINK_SETPASSWORD: - process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/reset-password/{optin}', - EMAIL_LINK_FORGOTPASSWORD: - process.env.EMAIL_LINK_FORGOTPASSWORD || 'http://localhost/forgot-password', - EMAIL_LINK_OVERVIEW: process.env.EMAIL_LINK_OVERVIEW || 'http://localhost/overview', - // time in minutes a optin code is valid - EMAIL_CODE_VALID_TIME: process.env.EMAIL_CODE_VALID_TIME - ? parseInt(process.env.EMAIL_CODE_VALID_TIME) || 1440 - : 1440, - // time in minutes that must pass to request a new optin code - EMAIL_CODE_REQUEST_TIME: process.env.EMAIL_CODE_REQUEST_TIME - ? parseInt(process.env.EMAIL_CODE_REQUEST_TIME) || 10 - : 10, -} - -const webhook = { - // Elopage - WEBHOOK_ELOPAGE_SECRET: process.env.WEBHOOK_ELOPAGE_SECRET || 'secret', -} const eventProtocol = { // global switch to enable writing of EventProtocol-Entries @@ -104,7 +59,7 @@ const eventProtocol = { // This is needed by graphql-directive-auth process.env.APP_SECRET = server.JWT_SECRET -*/ + // Check config version constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION || constants.CONFIG_VERSION.DEFAULT if ( @@ -122,8 +77,6 @@ const federation = { FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null, FEDERATION_PORT: process.env.FEDERATION_PORT || 5001, FEDERATION_API: process.env.FEDERATION_API || '1_0', - FEDERATION_COMMUNITY_ACTIVATE_ENDPOINTS: - process.env.FEDERATION_COMMUNITY_ACTIVATE_ENDPOINTS === 'true' || false, FEDERATION_COMMUNITY_URL: process.env.FEDERATION_COMMUNITY_URL || null, } diff --git a/federation/src/dht_node/index.test.ts b/federation/src/dht_node/index.test.ts new file mode 100644 index 000000000..235206cf8 --- /dev/null +++ b/federation/src/dht_node/index.test.ts @@ -0,0 +1,798 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ + +import { startDHT } from './index' +import DHT from '@hyperswarm/dht' +import CONFIG from '@/config' +import { logger } from '@test/testSetup' +import { Community as DbCommunity } from '@entity/Community' +import { testEnvironment, cleanDB } from '@test/helpers' + +CONFIG.FEDERATION_DHT_SEED = '64ebcb0e3ad547848fef4197c6e2332f' + +jest.mock('@hyperswarm/dht') + +const TEST_TOPIC = 'gradido_test_topic' + +const keyPairMock = { + publicKey: Buffer.from('publicKey'), + secretKey: Buffer.from('secretKey'), +} + +const serverListenSpy = jest.fn() + +const serverEventMocks: { [key: string]: any } = {} + +const serverOnMock = jest.fn().mockImplementation((key: string, callback) => { + serverEventMocks[key] = callback +}) + +const nodeCreateServerMock = jest.fn().mockImplementation(() => { + return { + on: serverOnMock, + listen: serverListenSpy, + } +}) + +const nodeAnnounceMock = jest.fn().mockImplementation(() => { + return { + finished: jest.fn(), + } +}) + +const lookupResultMock = { + token: Buffer.from(TEST_TOPIC), + from: { + id: Buffer.from('somone'), + host: '188.95.53.5', + port: 63561, + }, + to: { id: null, host: '83.53.31.27', port: 55723 }, + peers: [ + { + publicKey: Buffer.from('some-public-key'), + relayAddresses: [], + }, + ], +} + +const nodeLookupMock = jest.fn().mockResolvedValue([lookupResultMock]) + +const socketEventMocks: { [key: string]: any } = {} + +const socketOnMock = jest.fn().mockImplementation((key: string, callback) => { + socketEventMocks[key] = callback +}) + +const socketWriteMock = jest.fn() + +const nodeConnectMock = jest.fn().mockImplementation(() => { + return { + on: socketOnMock, + once: socketOnMock, + write: socketWriteMock, + } +}) + +DHT.hash.mockImplementation(() => { + return Buffer.from(TEST_TOPIC) +}) + +DHT.keyPair.mockImplementation(() => { + return keyPairMock +}) + +DHT.mockImplementation(() => { + return { + createServer: nodeCreateServerMock, + announce: nodeAnnounceMock, + lookup: nodeLookupMock, + connect: nodeConnectMock, + } +}) + +let con: any +let testEnv: any + +beforeAll(async () => { + testEnv = await testEnvironment(logger) + con = testEnv.con + await cleanDB() +}) + +afterAll(async () => { + await cleanDB() + await con.close() +}) + +describe('federation', () => { + beforeAll(() => { + jest.useFakeTimers() + }) + + describe('call startDHT', () => { + const hashSpy = jest.spyOn(DHT, 'hash') + const keyPairSpy = jest.spyOn(DHT, 'keyPair') + beforeEach(async () => { + DHT.mockClear() + jest.clearAllMocks() + await startDHT(TEST_TOPIC) + }) + + it('calls DHT.hash', () => { + expect(hashSpy).toBeCalledWith(Buffer.from(TEST_TOPIC)) + }) + + it('creates a key pair', () => { + expect(keyPairSpy).toBeCalledWith(expect.any(Buffer)) + }) + + it('initializes a new DHT object', () => { + expect(DHT).toBeCalledWith({ keyPair: keyPairMock }) + }) + + describe('DHT node', () => { + it('creates a server', () => { + expect(nodeCreateServerMock).toBeCalled() + }) + + it('listens on the server', () => { + expect(serverListenSpy).toBeCalled() + }) + + describe('timers', () => { + beforeEach(() => { + jest.runOnlyPendingTimers() + }) + + it('announces on topic', () => { + expect(nodeAnnounceMock).toBeCalledWith(Buffer.from(TEST_TOPIC), keyPairMock) + }) + + it('looks up on topic', () => { + expect(nodeLookupMock).toBeCalledWith(Buffer.from(TEST_TOPIC)) + }) + }) + + describe('server connection event', () => { + beforeEach(() => { + serverEventMocks.connection({ + remotePublicKey: Buffer.from('another-public-key'), + on: socketOnMock, + }) + }) + + it('can be triggered', () => { + expect(socketOnMock).toBeCalled() + }) + + describe('socket events', () => { + describe('on data', () => { + describe('with receiving simply a string', () => { + beforeEach(() => { + jest.clearAllMocks() + socketEventMocks.data(Buffer.from('no-json string')) + }) + + it('logs the received data', () => { + expect(logger.info).toBeCalledWith('data: no-json string') + }) + + it('logs an error of unexpected data format and structure', () => { + expect(logger.error).toBeCalledWith( + 'Error on receiving data from socket:', + new SyntaxError('Unexpected token o in JSON at position 1'), + ) + }) + }) + + describe('with receiving array of strings', () => { + beforeEach(() => { + jest.clearAllMocks() + const strArray: string[] = ['invalid type test', 'api', 'url'] + socketEventMocks.data(Buffer.from(strArray.toString())) + }) + + it('logs the received data', () => { + expect(logger.info).toBeCalledWith('data: invalid type test,api,url') + }) + + it('logs an error of unexpected data format and structure', () => { + expect(logger.error).toBeCalledWith( + 'Error on receiving data from socket:', + new SyntaxError('Unexpected token i in JSON at position 0'), + ) + }) + }) + + describe('with receiving array of string-arrays', () => { + beforeEach(async () => { + jest.clearAllMocks() + const strArray: string[][] = [ + [`api`, `url`, `invalid type in array test`], + [`wrong`, `api`, `url`], + ] + await socketEventMocks.data(Buffer.from(strArray.toString())) + }) + + it('logs the received data', () => { + expect(logger.info).toBeCalledWith( + 'data: api,url,invalid type in array test,wrong,api,url', + ) + }) + + it('logs an error of unexpected data format and structure', () => { + expect(logger.error).toBeCalledWith( + 'Error on receiving data from socket:', + new SyntaxError('Unexpected token a in JSON at position 0'), + ) + }) + }) + + describe('with receiving JSON-Array with too much entries', () => { + let jsonArray: { api: string; url: string }[] + beforeEach(async () => { + jest.clearAllMocks() + jsonArray = [ + { api: 'v1_0', url: 'too much versions at the same time test' }, + { api: 'v1_0', url: 'url2' }, + { api: 'v1_0', url: 'url3' }, + { api: 'v1_0', url: 'url4' }, + { api: 'v1_0', url: 'url5' }, + ] + await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray))) + }) + + it('logs the received data', () => { + expect(logger.info).toBeCalledWith( + 'data: [{"api":"v1_0","url":"too much versions at the same time test"},{"api":"v1_0","url":"url2"},{"api":"v1_0","url":"url3"},{"api":"v1_0","url":"url4"},{"api":"v1_0","url":"url5"}]', + ) + }) + + it('logs a warning of too much apiVersion-Definitions', () => { + expect(logger.warn).toBeCalledWith( + `received totaly wrong or too much apiVersions-Definition JSON-String: ${JSON.stringify( + jsonArray, + )}`, + ) + }) + }) + + describe('with receiving wrong but tolerated property data', () => { + let jsonArray: any[] + let result: DbCommunity[] = [] + beforeAll(async () => { + jest.clearAllMocks() + jsonArray = [ + { + wrong: 'wrong but tolerated property test', + api: 'v1_0', + url: 'url1', + }, + { + api: 'v2_0', + url: 'url2', + wrong: 'wrong but tolerated property test', + }, + ] + await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray))) + result = await DbCommunity.find() + }) + + afterAll(async () => { + await cleanDB() + }) + + it('has two Communty entries in database', () => { + expect(result).toHaveLength(2) + }) + + it('has an entry for api version v1_0', () => { + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(Number), + publicKey: expect.any(Buffer), + apiVersion: 'v1_0', + endPoint: 'url1', + lastAnnouncedAt: expect.any(Date), + createdAt: expect.any(Date), + updatedAt: null, + }), + ]), + ) + }) + + it('has an entry for api version v2_0', () => { + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(Number), + publicKey: expect.any(Buffer), + apiVersion: 'v2_0', + endPoint: 'url2', + lastAnnouncedAt: expect.any(Date), + createdAt: expect.any(Date), + updatedAt: null, + }), + ]), + ) + }) + }) + + describe('with receiving data but missing api property', () => { + let jsonArray: any[] + beforeEach(async () => { + jest.clearAllMocks() + jsonArray = [ + { test1: 'missing api proterty test', url: 'any url definition as string' }, + { api: 'some api', test2: 'missing url property test' }, + ] + await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray))) + }) + + it('logs the received data', () => { + expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`) + }) + + it('logs a warning of invalid apiVersion-Definition', () => { + expect(logger.warn).toBeCalledWith( + `received invalid apiVersion-Definition: ${JSON.stringify(jsonArray[0])}`, + ) + }) + }) + + describe('with receiving data but missing url property', () => { + let jsonArray: any[] + beforeEach(async () => { + jest.clearAllMocks() + jsonArray = [ + { api: 'some api', test2: 'missing url property test' }, + { test1: 'missing api proterty test', url: 'any url definition as string' }, + ] + await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray))) + }) + + it('logs the received data', () => { + expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`) + }) + + it('logs a warning of invalid apiVersion-Definition', () => { + expect(logger.warn).toBeCalledWith( + `received invalid apiVersion-Definition: ${JSON.stringify(jsonArray[0])}`, + ) + }) + }) + + describe('with receiving data but wrong type of api property', () => { + let jsonArray: any[] + beforeEach(async () => { + jest.clearAllMocks() + jsonArray = [ + { api: 1, url: 'wrong property type tests' }, + { api: 'urltyptest', url: 2 }, + { api: 1, url: 2 }, + ] + await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray))) + }) + + it('logs the received data', () => { + expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`) + }) + + it('logs a warning of invalid apiVersion-Definition', () => { + expect(logger.warn).toBeCalledWith( + `received invalid apiVersion-Definition: ${JSON.stringify(jsonArray[0])}`, + ) + }) + }) + + describe('with receiving data but wrong type of url property', () => { + let jsonArray: any[] + beforeEach(async () => { + jest.clearAllMocks() + jsonArray = [ + { api: 'urltyptest', url: 2 }, + { api: 1, url: 'wrong property type tests' }, + { api: 1, url: 2 }, + ] + await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray))) + }) + + it('logs the received data', () => { + expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`) + }) + + it('logs a warning of invalid apiVersion-Definition', () => { + expect(logger.warn).toBeCalledWith( + `received invalid apiVersion-Definition: ${JSON.stringify(jsonArray[0])}`, + ) + }) + }) + + describe('with receiving data but wrong type of both properties', () => { + let jsonArray: any[] + beforeEach(async () => { + jest.clearAllMocks() + jsonArray = [ + { api: 1, url: 2 }, + { api: 'urltyptest', url: 2 }, + { api: 1, url: 'wrong property type tests' }, + ] + await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray))) + }) + + it('logs the received data', () => { + expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`) + }) + + it('logs a warning of invalid apiVersion-Definition', () => { + expect(logger.warn).toBeCalledWith( + `received invalid apiVersion-Definition: ${JSON.stringify(jsonArray[0])}`, + ) + }) + }) + + describe('with receiving data but too long api string', () => { + let jsonArray: any[] + beforeEach(async () => { + jest.clearAllMocks() + jsonArray = [ + { api: 'toolong api', url: 'some valid url' }, + { + api: 'valid api', + url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic', + }, + { + api: 'toolong api', + url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic', + }, + ] + await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray))) + }) + + it('logs the received data', () => { + expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`) + }) + + it('logs a warning of invalid apiVersion-Definition', () => { + expect(logger.warn).toBeCalledWith( + `received apiVersion with content longer than max length: ${JSON.stringify( + jsonArray[0], + )}`, + ) + }) + }) + + describe('with receiving data but too long url string', () => { + let jsonArray: any[] + beforeEach(async () => { + jest.clearAllMocks() + jsonArray = [ + { + api: 'api', + url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic', + }, + { api: 'toolong api', url: 'some valid url' }, + { + api: 'toolong api', + url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic', + }, + ] + await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray))) + }) + + it('logs the received data', () => { + expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`) + }) + + it('logs a warning of invalid apiVersion-Definition', () => { + expect(logger.warn).toBeCalledWith( + `received apiVersion with content longer than max length: ${JSON.stringify( + jsonArray[0], + )}`, + ) + }) + }) + + describe('with receiving data but both properties with too long strings', () => { + let jsonArray: any[] + beforeEach(async () => { + jest.clearAllMocks() + jsonArray = [ + { + api: 'toolong api', + url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic', + }, + { + api: 'api', + url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic', + }, + { api: 'toolong api', url: 'some valid url' }, + ] + await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray))) + }) + + it('logs the received data', () => { + expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`) + }) + }) + + describe('with receiving data of exact max allowed properties length', () => { + let jsonArray: any[] + let result: DbCommunity[] = [] + beforeAll(async () => { + jest.clearAllMocks() + jsonArray = [ + { + api: 'valid api', + url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich', + }, + { + api: 'api', + url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic', + }, + { api: 'toolong api', url: 'some valid url' }, + ] + await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray))) + result = await DbCommunity.find() + }) + + afterAll(async () => { + await cleanDB() + }) + + it('has one Communty entry in database', () => { + expect(result).toHaveLength(1) + }) + + it(`has an entry with max content length for api and url`, () => { + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(Number), + publicKey: expect.any(Buffer), + apiVersion: 'valid api', + endPoint: + 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich', + lastAnnouncedAt: expect.any(Date), + createdAt: expect.any(Date), + updatedAt: null, + }), + ]), + ) + }) + }) + + describe('with receiving data of exact max allowed buffer length', () => { + let jsonArray: any[] + let result: DbCommunity[] = [] + beforeAll(async () => { + jest.clearAllMocks() + jsonArray = [ + { + api: 'valid api1', + url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich', + }, + { + api: 'valid api2', + url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich', + }, + { + api: 'valid api3', + url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich', + }, + { + api: 'valid api4', + url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich', + }, + ] + await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray))) + result = await DbCommunity.find() + }) + + afterAll(async () => { + await cleanDB() + }) + + it('has five Communty entries in database', () => { + expect(result).toHaveLength(4) + }) + + it(`has an entry 'valid api1' with max content length for api and url`, () => { + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(Number), + publicKey: expect.any(Buffer), + apiVersion: 'valid api1', + endPoint: + 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich', + lastAnnouncedAt: expect.any(Date), + createdAt: expect.any(Date), + updatedAt: null, + }), + ]), + ) + }) + + it(`has an entry 'valid api2' with max content length for api and url`, () => { + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(Number), + publicKey: expect.any(Buffer), + apiVersion: 'valid api2', + endPoint: + 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich', + lastAnnouncedAt: expect.any(Date), + createdAt: expect.any(Date), + updatedAt: null, + }), + ]), + ) + }) + + it(`has an entry 'valid api3' with max content length for api and url`, () => { + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(Number), + publicKey: expect.any(Buffer), + apiVersion: 'valid api3', + endPoint: + 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich', + lastAnnouncedAt: expect.any(Date), + createdAt: expect.any(Date), + updatedAt: null, + }), + ]), + ) + }) + + it(`has an entry 'valid api4' with max content length for api and url`, () => { + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(Number), + publicKey: expect.any(Buffer), + apiVersion: 'valid api4', + endPoint: + 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich', + lastAnnouncedAt: expect.any(Date), + createdAt: expect.any(Date), + updatedAt: null, + }), + ]), + ) + }) + }) + + describe('with receiving data longer than max allowed buffer length', () => { + let jsonArray: any[] + beforeEach(async () => { + jest.clearAllMocks() + jsonArray = [ + { + api: 'Xvalid api1', + url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich', + }, + { + api: 'valid api2', + url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich', + }, + { + api: 'valid api3', + url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich', + }, + { + api: 'valid api4', + url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich', + }, + ] + await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray))) + }) + + it('logs the received data', () => { + expect(logger.warn).toBeCalledWith( + `received more than max allowed length of data buffer: ${ + JSON.stringify(jsonArray).length + } against 1141 max allowed`, + ) + }) + }) + + describe('with proper data', () => { + let result: DbCommunity[] = [] + beforeAll(async () => { + jest.clearAllMocks() + await socketEventMocks.data( + Buffer.from( + JSON.stringify([ + { + api: 'v1_0', + url: 'http://localhost:4000/api/v1_0', + }, + { + api: 'v2_0', + url: 'http://localhost:4000/api/v2_0', + }, + ]), + ), + ) + result = await DbCommunity.find() + }) + + afterAll(async () => { + await cleanDB() + }) + + it('has two Communty entries in database', () => { + expect(result).toHaveLength(2) + }) + + it('has an entry for api version v1_0', () => { + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(Number), + publicKey: expect.any(Buffer), + apiVersion: 'v1_0', + endPoint: 'http://localhost:4000/api/v1_0', + lastAnnouncedAt: expect.any(Date), + createdAt: expect.any(Date), + updatedAt: null, + }), + ]), + ) + }) + + it('has an entry for api version v2_0', () => { + expect(result).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(Number), + publicKey: expect.any(Buffer), + apiVersion: 'v2_0', + endPoint: 'http://localhost:4000/api/v2_0', + lastAnnouncedAt: expect.any(Date), + createdAt: expect.any(Date), + updatedAt: null, + }), + ]), + ) + }) + }) + }) + + describe('on open', () => { + beforeEach(() => { + socketEventMocks.open() + }) + + it.skip('calls socket write with own api versions', () => { + expect(socketWriteMock).toBeCalledWith( + Buffer.from( + JSON.stringify([ + { + api: 'v1_0', + url: 'http://localhost:4000/api/v1_0', + }, + { + api: 'v1_1', + url: 'http://localhost:4000/api/v1_1', + }, + { + api: 'v2_0', + url: 'http://localhost:4000/api/v2_0', + }, + ]), + ), + ) + }) + }) + }) + }) + }) + }) +}) diff --git a/federation/src/dht_node/index.ts b/federation/src/dht_node/index.ts index 7d33e41e9..ebaaed5e2 100644 --- a/federation/src/dht_node/index.ts +++ b/federation/src/dht_node/index.ts @@ -1,8 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ - import DHT from '@hyperswarm/dht' -// import { Connection } from '@dbTools/typeorm' import { backendLogger as logger } from '@/server/logger' import CONFIG from '@/config' import { Community as DbCommunity } from '@entity/Community' @@ -17,7 +15,6 @@ const ERRORTIME = 240000 const ANNOUNCETIME = 30000 enum ApiVersionType { - V0_1 = 'v0_1', V1_0 = 'v1_0', V1_1 = 'v1_1', V2_0 = 'v2_0', @@ -26,52 +23,70 @@ type CommunityApi = { api: string url: string } -type CommunityApiList = { - apiVersions: CommunityApi[] -} -export const startDHT = async ( - // connection: Connection, - topic: string, -): Promise => { +export const startDHT = async (topic: string): Promise => { try { const TOPIC = DHT.hash(Buffer.from(topic)) const keyPair = DHT.keyPair(getSeed()) logger.info(`keyPairDHT: publicKey=${keyPair.publicKey.toString('hex')}`) logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`) - const apiList: CommunityApiList = { - apiVersions: Object.values(ApiVersionType).map(function (apiEnum) { - const comApi: CommunityApi = { - api: apiEnum, - url: CONFIG.FEDERATION_COMMUNITY_URL || 'not configured', - } - return comApi - }), - } - logger.debug(`ApiList: ${JSON.stringify(apiList)}`) + const ownApiVersions = Object.values(ApiVersionType).map(function (apiEnum) { + const comApi: CommunityApi = { + api: apiEnum, + url: CONFIG.FEDERATION_COMMUNITY_URL + apiEnum, + } + return comApi + }) + logger.debug(`ApiList: ${JSON.stringify(ownApiVersions)}`) const node = new DHT({ keyPair }) const server = node.createServer() server.on('connection', function (socket: any) { - // noiseSocket is E2E between you and the other peer - // pipe it somewhere like any duplex stream logger.info(`server on... with Remote public key: ${socket.remotePublicKey.toString('hex')}`) - // console.log("Local public key", noiseSocket.publicKey.toString("hex")); // same as keyPair.publicKey socket.on('data', async (data: Buffer) => { try { + if (data.length > 1141) { + logger.warn( + `received more than max allowed length of data buffer: ${data.length} against 1141 max allowed`, + ) + return + } logger.info(`data: ${data.toString('ascii')}`) - const apiVersionList: CommunityApiList = JSON.parse(data.toString('ascii')) - if (apiVersionList && apiVersionList.apiVersions) { - for (let i = 0; i < apiVersionList.apiVersions.length; i++) { - const apiVersion = apiVersionList.apiVersions[i] + const recApiVersions: CommunityApi[] = JSON.parse(data.toString('ascii')) + + // TODO better to introduce the validation by https://github.com/typestack/class-validato + if (recApiVersions && Array.isArray(recApiVersions) && recApiVersions.length < 5) { + for (const recApiVersion of recApiVersions) { + if ( + !recApiVersion.api || + typeof recApiVersion.api !== 'string' || + !recApiVersion.url || + typeof recApiVersion.url !== 'string' + ) { + logger.warn( + `received invalid apiVersion-Definition: ${JSON.stringify(recApiVersion)}`, + ) + // in a forEach-loop use return instead of continue + return + } + // TODO better to introduce the validation on entity-Level by https://github.com/typestack/class-validator + if (recApiVersion.api.length > 10 || recApiVersion.url.length > 255) { + logger.warn( + `received apiVersion with content longer than max length: ${JSON.stringify( + recApiVersion, + )}`, + ) + // in a forEach-loop use return instead of continue + return + } const variables = { - apiVersion: apiVersion.api, - endPoint: apiVersion.url, + apiVersion: recApiVersion.api, + endPoint: recApiVersion.url, publicKey: socket.remotePublicKey.toString('hex'), lastAnnouncedAt: new Date(), } @@ -86,11 +101,17 @@ export const startDHT = async ( overwrite: ['end_point', 'last_announced_at'], }) .execute() + logger.info(`federation community upserted successfully...`) } - logger.info(`federation community apiVersions stored...`) + } else { + logger.warn( + `received totaly wrong or too much apiVersions-Definition JSON-String: ${JSON.stringify( + recApiVersions, + )}`, + ) } } catch (e) { - logger.error(`Error on receiving data from socket: ${JSON.stringify(e)}`) + logger.error('Error on receiving data from socket:', e) } }) }) @@ -137,7 +158,6 @@ export const startDHT = async ( logger.info(`Found new peers: ${collectedPubKeys}`) collectedPubKeys.forEach((remotePubKey) => { - // publicKey here is keyPair.publicKey from above const socket = node.connect(Buffer.from(remotePubKey, 'hex')) // socket.once("connect", function () { @@ -154,13 +174,12 @@ export const startDHT = async ( }) socket.on('open', function () { - // noiseSocket fully open with the other peer - socket.write(Buffer.from(JSON.stringify(apiList))) + socket.write(Buffer.from(JSON.stringify(ownApiVersions))) successfulRequests.push(remotePubKey) }) }) }, POLLTIME) } catch (err) { - logger.error(err) + logger.error('DHT unexpected error:', err) } }