import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity, FederatedCommunityLoggingView, } from 'database' import { IsNull } from 'typeorm' import { FederationClient as V1_0_FederationClient } from '@/federation/client/1_0/FederationClient' import { PublicCommunityInfo } from '@/federation/client/1_0/model/PublicCommunityInfo' import { FederationClientFactory } from '@/federation/client/FederationClientFactory' import { LogError } from '@/server/LogError' import { backendLogger as logger } from '@/server/logger' import { startCommunityAuthentication } from './authenticateCommunities' import { PublicCommunityInfoLoggingView } from './client/1_0/logging/PublicCommunityInfoLogging.view' import { ApiVersionType } from './enum/apiVersionType' import { generateKeyPair, exportSPKI, exportPKCS8 } from 'jose' // import { CONFIG } from '@/config/' export async function startValidateCommunities(timerInterval: number): Promise { if (Number.isNaN(timerInterval) || timerInterval <= 0) { throw new LogError('FEDERATION_VALIDATE_COMMUNITY_TIMER is not a positive number') } logger.info( `Federation: startValidateCommunities loop with an interval of ${timerInterval} ms...`, ) // delete all foreign federated community entries to avoid increasing validation efforts and log-files await DbFederatedCommunity.delete({ foreign: true }) // TODO: replace the timer-loop by an event-based communication to verify announced foreign communities // better to use setTimeout twice than setInterval once -> see https://javascript.info/settimeout-setinterval setTimeout(async function run() { await validateCommunities() setTimeout(run, timerInterval) }, timerInterval) } export async function validateCommunities(): Promise { // search all foreign federated communities which are still not verified or have not been verified since last dht-announcement const dbFederatedCommunities: DbFederatedCommunity[] = await DbFederatedCommunity.createQueryBuilder() .where({ foreign: true, verifiedAt: IsNull() }) .orWhere('verified_at < last_announced_at') .getMany() logger.debug(`Federation: found ${dbFederatedCommunities.length} dbCommunities`) for (const dbCom of dbFederatedCommunities) { logger.debug('Federation: dbCom', new FederatedCommunityLoggingView(dbCom)) const apiValueStrings: string[] = Object.values(ApiVersionType) logger.debug(`suppported ApiVersions=`, apiValueStrings) if (!apiValueStrings.includes(dbCom.apiVersion)) { logger.debug( 'Federation: dbCom with unsupported apiVersion', dbCom.endPoint, dbCom.apiVersion, ) continue } try { const client = FederationClientFactory.getInstance(dbCom) if (client instanceof V1_0_FederationClient) { const pubKey = await client.getPublicKey() if (pubKey && pubKey === dbCom.publicKey.toString('hex')) { await DbFederatedCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() }) logger.debug(`Federation: verified community with:`, dbCom.endPoint) const pubComInfo = await client.getPublicCommunityInfo() if (pubComInfo) { await writeForeignCommunity(dbCom, pubComInfo) await startCommunityAuthentication(dbCom) logger.debug(`Federation: write publicInfo of community: name=${pubComInfo.name}`) } else { logger.debug('Federation: missing result of getPublicCommunityInfo') } } else { logger.debug( 'Federation: received not matching publicKey:', pubKey, dbCom.publicKey.toString('hex'), ) } } } catch (err) { logger.error(`Error:`, err) } } } export async function writeJwtKeyPairInHomeCommunity(): Promise { logger.debug(`Federation: writeJwtKeyPairInHomeCommunity`) try { // check for existing homeCommunity entry let homeCom = await DbCommunity.findOne({ where: { foreign: false } }) if (homeCom) { if (!homeCom.publicJwtKey && !homeCom.privateJwtKey) { // Generate key pair using jose library const keyPair = await generateKeyPair('RS256'); logger.debug(`Federation: writeJwtKeyPairInHomeCommunity generated keypair=`, keyPair); // Convert keys to PEM format const publicKeyPem = await exportSPKI(keyPair.publicKey); const privateKeyPem = await exportPKCS8(keyPair.privateKey); logger.debug(`Federation: writeJwtKeyPairInHomeCommunity publicKey=`, publicKeyPem); logger.debug(`Federation: writeJwtKeyPairInHomeCommunity privateKey=`, privateKeyPem); homeCom.publicJwtKey = publicKeyPem; logger.debug(`Federation: writeJwtKeyPairInHomeCommunity publicJwtKey.length=`, homeCom.publicJwtKey.length); homeCom.privateJwtKey = privateKeyPem; logger.debug(`Federation: writeJwtKeyPairInHomeCommunity privateJwtKey.length=`, homeCom.privateJwtKey.length); await DbCommunity.save(homeCom) logger.debug(`Federation: writeJwtKeyPairInHomeCommunity done`) } else { logger.debug(`Federation: writeJwtKeyPairInHomeCommunity: keypair already exists`) } } else { throw new Error(`Error! A HomeCommunity-Entry still not exist! Please start the DHT-Modul first.`) } return homeCom } catch (err) { throw new Error(`Error writing JwtKeyPair in HomeCommunity-Entry: ${err}`) } } async function writeForeignCommunity( dbCom: DbFederatedCommunity, pubInfo: PublicCommunityInfo, ): Promise { if (!dbCom || !pubInfo || !(dbCom.publicKey.toString('hex') === pubInfo.publicKey)) { const pubInfoView = new PublicCommunityInfoLoggingView(pubInfo) logger.error( `Error in writeForeignCommunity: missmatching parameters or publicKey. pubInfo:${pubInfoView.toString( true, )}`, ) } else { let com = await DbCommunity.findOneBy({ publicKey: dbCom.publicKey }) if (!com) { com = DbCommunity.create() } com.creationDate = pubInfo.creationDate com.description = pubInfo.description com.foreign = true com.name = pubInfo.name com.publicKey = dbCom.publicKey com.publicJwtKey = pubInfo.publicJwtKey com.url = dbCom.endPoint await DbCommunity.save(com) } }