103 lines
3.0 KiB
TypeScript
103 lines
3.0 KiB
TypeScript
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();
|
|
}
|
|
}
|