gradido/backend/src/federation/validateCommunities.ts
2025-06-17 02:27:46 +02:00

152 lines
6.2 KiB
TypeScript

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<void> {
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<void> {
// 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<DbCommunity> {
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<void> {
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)
}
}