Files

103 lines
3.0 KiB
TypeScript
Raw Permalink Normal View History

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();
}
}