Slave/altcha throttle #4

Merged
nobswebdev merged 6 commits from slave/altcha-throttle into master 2026-10-06 17:55:47 +00:00
5 changed files with 86 additions and 5 deletions
Showing only changes of commit 84f8a85214 - Show all commits
+3 -2
View File
@@ -1,10 +1,11 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AltchaController } from './controllers/AltchaController'; import { AltchaController } from './controllers/AltchaController';
import { AltchaGuard } from './guards/AltchaGuard';
import { AltchaService } from './services/AltchaService'; import { AltchaService } from './services/AltchaService';
@Module({ @Module({
controllers: [AltchaController], controllers: [AltchaController],
providers: [AltchaService], providers: [AltchaService, AltchaGuard],
exports: [AltchaService] exports: [AltchaGuard]
}) })
export class AltchaModule {} export class AltchaModule {}
@@ -0,0 +1,36 @@
import { BadRequestException, CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Request } from 'express';
import { AltchaService } from '../services/AltchaService';
import { Config } from '../../../types/Config';
@Injectable()
export class AltchaGuard implements CanActivate {
constructor(
private readonly altchaService: AltchaService,
private readonly configService: ConfigService
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const { enabled } = this.configService.get('altcha') as Config['altcha'];
if (!enabled) {
return true;
}
const request = context.switchToHttp().getRequest<Request>();
const altcha = request.body?.altcha;
if (typeof altcha !== 'string' || altcha.length === 0) {
throw new BadRequestException('ALTCHA verification required');
}
const verified = await this.altchaService.verifyPayload(altcha);
if (!verified) {
throw new BadRequestException('ALTCHA verification failed');
}
return true;
}
}
@@ -0,0 +1,39 @@
import { ConfigService } from '@nestjs/config';
import { createChallenge, solveChallenge } from 'altcha-lib/v1';
import type { Payload } from 'altcha-lib/v1/types';
import { AltchaService } from './AltchaService';
const hmacKey = 'test-hmac-key';
const encodePayload = (payload: Payload): string =>
Buffer.from(JSON.stringify(payload), 'utf8').toString('base64');
describe('AltchaService', () => {
const configService = {
get: jest.fn(() => ({ enabled: true, hmacKey, maxNumber: 100_000 }))
} as unknown as ConfigService;
const service = new AltchaService(configService);
it('verifies a valid ALTCHA payload', async () => {
const challenge = await createChallenge({ hmacKey, number: 42, maxNumber: 100_000 });
const { promise } = solveChallenge(challenge.challenge, challenge.salt, challenge.algorithm, 100_000);
const solution = await promise;
expect(solution).not.toBeNull();
const payload: Payload = {
algorithm: challenge.algorithm,
challenge: challenge.challenge,
number: solution!.number,
salt: challenge.salt,
signature: challenge.signature
};
await expect(service.verifyPayload(encodePayload(payload))).resolves.toBe(true);
});
it('rejects an invalid ALTCHA payload', async () => {
await expect(service.verifyPayload('not-valid-base64-json')).resolves.toBe(false);
});
});
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { createChallenge } from 'altcha-lib/v1'; import { createChallenge, verifySolution } from 'altcha-lib/v1';
import type { Challenge } from 'altcha-lib/v1/types'; import type { Challenge } from 'altcha-lib/v1/types';
import { Config } from '../../../types/Config'; import { Config } from '../../../types/Config';
@@ -16,4 +16,10 @@ export class AltchaService {
maxNumber maxNumber
}); });
} }
async verifyPayload(altcha: string): Promise<boolean> {
const { hmacKey } = this.configService.get('altcha') as Config['altcha'];
return verifySolution(altcha, hmacKey);
}
} }
+1 -2
View File
@@ -2,7 +2,6 @@ import { Module } from '@nestjs/common';
import { StorageService } from './services/StorageService'; import { StorageService } from './services/StorageService';
@Module({ @Module({
providers: [StorageService], providers: [StorageService]
exports: [StorageService]
}) })
export class StorageModule {} export class StorageModule {}