Slave/altcha throttle #4

Merged
nobswebdev merged 6 commits from slave/altcha-throttle into master 2026-10-06 17:55:47 +00:00
12 changed files with 293 additions and 18 deletions
Showing only changes of commit 87376ea37f - Show all commits
+1 -2
View File
@@ -21,8 +21,7 @@ CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
MAX_IMAGES_PER_UPLOAD=10 MAX_IMAGES_PER_UPLOAD=10
MULTER_MAX_FILE_BYTES=10485760 MULTER_MAX_FILE_BYTES=10485760
CAPTCHA_ENABLED=true ALTCHA_ENABLED=true
ALTCHA_HMAC_KEY=change-me-use-long-random-secret ALTCHA_HMAC_KEY=change-me-use-long-random-secret
ALTCHA_MAX_NUMBER=50000 ALTCHA_MAX_NUMBER=50000
+70
View File
@@ -14,6 +14,7 @@
"@nestjs/core": "^11.0.1", "@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1", "@nestjs/platform-express": "^11.0.1",
"@nestjs/typeorm": "^12.0.2", "@nestjs/typeorm": "^12.0.2",
"altcha-lib": "^2.6.0",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.15.1", "class-validator": "^0.15.1",
"pg": "^8.23.1", "pg": "^8.23.1",
@@ -3269,6 +3270,75 @@
"ajv": "^8.8.2" "ajv": "^8.8.2"
} }
}, },
"node_modules/altcha-lib": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/altcha-lib/-/altcha-lib-2.6.0.tgz",
"integrity": "sha512-mBp/KxMZmQkckWX5iM0gHqCm0qTLGhF9r8hhFjNrBpzP0qHZBadR9oafxVx9cTLHl4h2+f56N4hRIrZzvonYLQ==",
"license": "MIT",
"bin": {
"altcha-lib": "bin/cli.mjs"
},
"peerDependencies": {
"@fastify/cookie": "^11.0.0",
"@fastify/cors": "^11.0.0",
"@fastify/formbody": "^8.0.0 || ^9.0.0",
"@nestjs/common": "^10.0.0 || ^11.0.0 || ^12.0.0",
"@nestjs/core": "^10.0.0 || ^11.0.0 || ^12.0.0",
"@nestjs/platform-express": "^10.0.0 || ^11.0.0 || ^12.0.0",
"@nestjs/platform-fastify": "^10.0.0 || ^11.0.0 || ^12.0.0",
"@sveltejs/kit": "^2.0.0",
"cookie-parser": "^1.0.0",
"cors": "^2.0.0",
"express": "^4.0.0 || ^5.0.0",
"fastify": "^5.0.0",
"h3": "^2.0.0",
"hono": "^4.0.0"
},
"peerDependenciesMeta": {
"@fastify/cookie": {
"optional": true
},
"@fastify/cors": {
"optional": true
},
"@fastify/formbody": {
"optional": true
},
"@nestjs/common": {
"optional": true
},
"@nestjs/core": {
"optional": true
},
"@nestjs/platform-express": {
"optional": true
},
"@nestjs/platform-fastify": {
"optional": true
},
"@sveltejs/kit": {
"optional": true
},
"cookie-parser": {
"optional": true
},
"cors": {
"optional": true
},
"express": {
"optional": true
},
"fastify": {
"optional": true
},
"h3": {
"optional": true
},
"hono": {
"optional": true
}
}
},
"node_modules/ansi-colors": { "node_modules/ansi-colors": {
"version": "4.1.3", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
+1
View File
@@ -25,6 +25,7 @@
"@nestjs/core": "^11.0.1", "@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1", "@nestjs/platform-express": "^11.0.1",
"@nestjs/typeorm": "^12.0.2", "@nestjs/typeorm": "^12.0.2",
"altcha-lib": "^2.6.0",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.15.1", "class-validator": "^0.15.1",
"pg": "^8.23.1", "pg": "^8.23.1",
+10 -2
View File
@@ -1,7 +1,9 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService, registerAs } from '@nestjs/config'; import { ConfigModule, ConfigService, registerAs } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { getAppConfig, getPostgresConfig } from './config'; import { getAltchaConfig, getAppConfig, getPostgresConfig } from './config';
import { validate } from './config/validate';
import { AltchaModule } from './modules/altcha/AltchaModule';
import { HealthCheckModule } from './modules/healthCheck/HealthCheckModule'; import { HealthCheckModule } from './modules/healthCheck/HealthCheckModule';
import { StorageModule } from './modules/storage/StorageModule'; import { StorageModule } from './modules/storage/StorageModule';
import { UploadModule } from './modules/upload/UploadModule'; import { UploadModule } from './modules/upload/UploadModule';
@@ -11,13 +13,19 @@ import { Config } from './types/Config';
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
isGlobal: true, isGlobal: true,
load: [registerAs('postgres', getPostgresConfig), registerAs('app', getAppConfig)] validate,
load: [
registerAs('postgres', getPostgresConfig),
registerAs('app', getAppConfig),
registerAs('altcha', getAltchaConfig)
]
}), }),
TypeOrmModule.forRootAsync({ TypeOrmModule.forRootAsync({
useFactory: (configService: ConfigService) => configService.get('postgres') as Config['postgres'], useFactory: (configService: ConfigService) => configService.get('postgres') as Config['postgres'],
inject: [ConfigService] inject: [ConfigService]
}), }),
HealthCheckModule, HealthCheckModule,
AltchaModule,
StorageModule, StorageModule,
UploadModule UploadModule
] ]
+10 -1
View File
@@ -1,5 +1,6 @@
import { AppConfig } from '../types/AppConfig'; import { AppConfig } from '../types/AppConfig';
import { PostgresConfig } from '../types/Config'; import { AltchaConfig } from '../types/AltchaConfig';
import { PostgresConfig } from '../types/PostgresConfig';
import { NodeEnv } from '../types/NodeEnv'; import { NodeEnv } from '../types/NodeEnv';
const env = (key: string): string => process.env[key] || ''; const env = (key: string): string => process.env[key] || '';
@@ -23,6 +24,14 @@ export const getPostgresConfig = (): PostgresConfig => {
}; };
}; };
export const getAltchaConfig = (): AltchaConfig => {
return {
enabled: isEnabled('ALTCHA_ENABLED'),
hmacKey: env('ALTCHA_HMAC_KEY'),
maxNumber: envInt('ALTCHA_MAX_NUMBER')
};
};
export const getAppConfig = (): AppConfig => { export const getAppConfig = (): AppConfig => {
const rawOrigins = env('CORS_ORIGINS'); const rawOrigins = env('CORS_ORIGINS');
+129
View File
@@ -0,0 +1,129 @@
import { plainToClass } from 'class-transformer';
import { IsBoolean, IsEnum, IsNotEmpty, IsNumber, IsString, Max, Min, validateSync } from 'class-validator';
import { NodeEnv } from '../types/NodeEnv';
class EnvironmentVariables {
@IsNotEmpty()
@IsEnum(NodeEnv)
NODE_ENV: NodeEnv;
@IsNotEmpty()
@IsNumber()
@Min(1)
@Max(65535)
BACKEND_PORT: number;
@IsNotEmpty()
@IsString()
POSTGRES_USER: string;
@IsNotEmpty()
@IsString()
POSTGRES_PASSWORD: string;
@IsNotEmpty()
@IsString()
POSTGRES_DB: string;
@IsNotEmpty()
@IsString()
POSTGRES_HOST: string;
@IsNotEmpty()
@IsNumber()
@Min(1)
@Max(65535)
POSTGRES_PORT: number;
@IsNotEmpty()
@IsBoolean()
POSTGRES_MIGRATIONS_RUN: boolean;
@IsNotEmpty()
@IsString()
CORS_ORIGINS: string;
@IsNotEmpty()
@IsNumber()
@Min(1)
@Max(100)
MAX_IMAGES_PER_UPLOAD: number;
@IsNotEmpty()
@IsNumber()
@Min(1)
@Max(50 * 1024 * 1024)
MULTER_MAX_FILE_BYTES: number;
@IsNotEmpty()
@IsBoolean()
ALTCHA_ENABLED: boolean;
@IsNotEmpty()
@IsString()
ALTCHA_HMAC_KEY: string;
@IsNotEmpty()
@IsNumber()
@Min(1)
@Max(1_000_000)
ALTCHA_MAX_NUMBER: number;
@IsNotEmpty()
@IsNumber()
@Min(1)
@Max(3650)
NEVER_MODE_INACTIVITY_DAYS: number;
@IsNotEmpty()
@IsNumber()
@Min(1000)
@Max(3_600_000)
THROTTLE_UPLOAD_TTL_MS: number;
@IsNotEmpty()
@IsNumber()
@Min(1)
@Max(10_000)
THROTTLE_UPLOAD_LIMIT: number;
@IsNotEmpty()
@IsNumber()
@Min(1000)
@Max(3_600_000)
THROTTLE_HOTLINK_PER_IP_TTL_MS: number;
@IsNotEmpty()
@IsNumber()
@Min(1)
@Max(1_000_000)
THROTTLE_HOTLINK_PER_IP_LIMIT: number;
@IsNotEmpty()
@IsNumber()
@Min(1000)
@Max(3_600_000)
THROTTLE_HOTLINK_PER_ID_TTL_MS: number;
@IsNotEmpty()
@IsNumber()
@Min(1)
@Max(1_000_000)
THROTTLE_HOTLINK_PER_ID_LIMIT: number;
}
export const validate = (config: Record<string, unknown>) => {
const validatedConfig = plainToClass(EnvironmentVariables, config, {
enableImplicitConversion: true
});
const errors = validateSync(validatedConfig, {
skipMissingProperties: false
});
if (errors.length > 0) {
throw new Error(errors.toString());
}
return validatedConfig;
};
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { AltchaController } from './controllers/AltchaController';
import { AltchaService } from './services/AltchaService';
@Module({
controllers: [AltchaController],
providers: [AltchaService],
exports: [AltchaService]
})
export class AltchaModule {}
@@ -0,0 +1,23 @@
import { Controller, Get, NotFoundException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AltchaService } from '../services/AltchaService';
import { Config } from '../../../types/Config';
@Controller('altcha')
export class AltchaController {
constructor(
private readonly altchaService: AltchaService,
private readonly configService: ConfigService
) {}
@Get('challenge')
async getChallenge() {
const { enabled } = this.configService.get('altcha') as Config['altcha'];
if (!enabled) {
throw new NotFoundException();
}
return this.altchaService.createChallenge();
}
}
@@ -0,0 +1,19 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { createChallenge } from 'altcha-lib/v1';
import type { Challenge } from 'altcha-lib/v1/types';
import { Config } from '../../../types/Config';
@Injectable()
export class AltchaService {
constructor(private readonly configService: ConfigService) {}
async createChallenge(): Promise<Challenge> {
const { hmacKey, maxNumber } = this.configService.get('altcha') as Config['altcha'];
return createChallenge({
hmacKey,
maxNumber
});
}
}
+5
View File
@@ -0,0 +1,5 @@
export type AltchaConfig = {
enabled: boolean;
hmacKey: string;
maxNumber: number;
};
+3 -13
View File
@@ -1,19 +1,9 @@
import { AltchaConfig } from './AltchaConfig';
import { AppConfig } from './AppConfig'; import { AppConfig } from './AppConfig';
import { PostgresConfig } from './PostgresConfig';
export interface PostgresConfig {
type: 'postgres';
url: string;
host: string;
username: string;
password: string;
port: number;
database: string;
entities: string[];
migrations: string[];
migrationsRun: boolean;
}
export interface Config { export interface Config {
app: AppConfig; app: AppConfig;
altcha: AltchaConfig;
postgres: PostgresConfig; postgres: PostgresConfig;
} }
+12
View File
@@ -0,0 +1,12 @@
export interface PostgresConfig {
type: 'postgres';
url: string;
host: string;
username: string;
password: string;
port: number;
database: string;
entities: string[];
migrations: string[];
migrationsRun: boolean;
}