import * as plugins from '../plugins.js'; import { Reception } from './classes.reception.js'; import { AbuseWindow } from './classes.abusewindow.js'; export interface IAbuseProtectionConfig { maxAttempts: number; windowMillis: number; blockDurationMillis: number; } export class AbuseProtectionManager { public receptionRef: Reception; public get db() { return this.receptionRef.db.smartdataDb; } public CAbuseWindow = plugins.smartdata.setDefaultManagerForDoc(this, AbuseWindow); constructor(receptionRefArg: Reception) { this.receptionRef = receptionRefArg; } private normalizeIdentifier(identifierArg: string) { return identifierArg.trim().toLowerCase(); } private hashIdentifier(identifierArg: string) { return plugins.smarthash.sha256FromStringSync(this.normalizeIdentifier(identifierArg)); } private createWindowId(actionArg: string, identifierArg: string) { return plugins.smarthash.sha256FromStringSync( `${actionArg}:${this.hashIdentifier(identifierArg)}` ); } private async getWindow(actionArg: string, identifierArg: string) { return this.CAbuseWindow.getInstance({ id: this.createWindowId(actionArg, identifierArg), }); } public async consumeAttempt( actionArg: string, identifierArg: string, configArg: IAbuseProtectionConfig, errorTextArg = 'Too many attempts. Please wait before trying again.' ) { const now = Date.now(); let abuseWindow = await this.getWindow(actionArg, identifierArg); if (!abuseWindow) { abuseWindow = new AbuseWindow(); abuseWindow.id = this.createWindowId(actionArg, identifierArg); abuseWindow.data.action = actionArg; abuseWindow.data.identifierHash = this.hashIdentifier(identifierArg); abuseWindow.data.createdAt = now; } if (abuseWindow.isBlocked(now)) { throw new plugins.typedrequest.TypedResponseError(errorTextArg); } if (abuseWindow.data.blockedUntil && abuseWindow.data.blockedUntil <= now) { abuseWindow.data.attemptCount = 0; abuseWindow.data.windowStartedAt = now; abuseWindow.data.blockedUntil = 0; } if ( !abuseWindow.data.windowStartedAt || abuseWindow.data.windowStartedAt + configArg.windowMillis <= now ) { abuseWindow.data.attemptCount = 0; abuseWindow.data.windowStartedAt = now; } abuseWindow.data.attemptCount += 1; abuseWindow.data.updatedAt = now; abuseWindow.data.validUntil = now + configArg.windowMillis; if (abuseWindow.data.attemptCount > configArg.maxAttempts) { abuseWindow.data.blockedUntil = now + configArg.blockDurationMillis; abuseWindow.data.validUntil = abuseWindow.data.blockedUntil; await abuseWindow.save(); throw new plugins.typedrequest.TypedResponseError(errorTextArg); } await abuseWindow.save(); } public async clearAttempts(actionArg: string, identifierArg: string) { const abuseWindow = await this.getWindow(actionArg, identifierArg); if (!abuseWindow) { return; } await abuseWindow.delete(); } }