Skip to content

Commit

Permalink
✔ Fail 2 Ban ~
Browse files Browse the repository at this point in the history
  • Loading branch information
bifeldy committed Nov 16, 2023
1 parent 8d238bd commit 62ecca7
Show file tree
Hide file tree
Showing 9 changed files with 337 additions and 6 deletions.
6 changes: 5 additions & 1 deletion src/api/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ import { QuizService } from './services/quiz.service';
import { SocketIoService } from './services/socket-io.service';
import { TaskCronJobService } from './services/task-cron-job.service';

import { CfWafService } from './scheduler/cf-waf-task.service';
import { RssFeedTasksService } from './scheduler/rss-feed-tasks.service';
import { SitemapService } from './scheduler/sitemap-tasks.service';
import { TrackerStatisticsService } from './scheduler/tracker-statistics-tasks.service';
Expand All @@ -130,6 +131,7 @@ import { BerkasService } from './repository/berkas.service';
import { DdlFileService } from './repository/ddl-file';
import { DoramaService } from './repository/dorama.service';
import { EdictService } from './repository/edict.service';
import { FailToBanService } from './repository/fail-to-ban.service';
import { FansubService } from './repository/fansub.service';
import { FansubMemberService } from './repository/fansub-member.service';
import { HirakataService } from './repository/hirakata.service';
Expand Down Expand Up @@ -272,12 +274,13 @@ import { UserService } from './repository/user.service';
SocketIoService,
TaskCronJobService,
// Service Task Schedulers
CfWafService,
RssFeedTasksService,
SitemapService,
TrackerStatisticsService,
UploadService,
VpsBillingService,
// Service Entities
// Repository Entities
AnimeService,
ApiKeyService,
AttachmentService,
Expand All @@ -286,6 +289,7 @@ import { UserService } from './repository/user.service';
DdlFileService,
DoramaService,
EdictService,
FailToBanService,
FansubService,
FansubMemberService,
HirakataService,
Expand Down
24 changes: 24 additions & 0 deletions src/api/entities/FailToBan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Entity, Column, PrimaryColumn, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm'

import { FailToBanModel } from '../../models/req-res.model';

@Entity({ name: 'fail_to_ban' })
export class FailToBan implements FailToBanModel {

@PrimaryColumn({ type: 'text' })
ip_domain: string;

@Column({ type: 'int', default: 0 })
fail_count: number;

@Column({ type: 'text', nullable: true })
rule_id: string;

@CreateDateColumn({ type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
@Index()
created_at: number | Date;

@UpdateDateColumn({ type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
updated_at: number | Date;

}
58 changes: 53 additions & 5 deletions src/api/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { DocumentBuilder, SwaggerDocumentOptions, SwaggerModule } from '@nestjs/
import { AbstractHttpAdapter, NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import express, { urlencoded, json, Request, Response, NextFunction } from 'express';
import { Equal } from 'typeorm';

import { SocketIoAdapter } from './adapters/socket-io.adapter';
import { SocketIoClusterAdapter } from './adapters/socket-io-cluster.adapter';
Expand All @@ -33,6 +34,8 @@ import { GlobalService } from './services/global.service';
import { SocketIoService } from './services/socket-io.service';
import { ClusterMasterSlaveService } from './services/cluster-master-slave.service';
import { DiscordService } from './services/discord.service';
import { CloudflareService } from './services/cloudflare.service';
import { FailToBanService } from './repository/fail-to-ban.service';

export async function ctx(): Promise<INestApplicationContext> {
return await NestFactory.createApplicationContext(AppModule);
Expand All @@ -44,7 +47,10 @@ export async function app(httpAdapter: AbstractHttpAdapter = null): Promise<INes
httpAdapter = new ExpressAdapter(express());
}
const nestApp = await NestFactory.create(AppModule, httpAdapter);
const gs = nestApp.get(GlobalService);
const aks = nestApp.get(ApiKeyService);
const cfs = nestApp.get(CloudflareService);
const ftb = nestApp.get(FailToBanService);
const sis = nestApp.get(SocketIoService);
nestApp.setGlobalPrefix('api');
nestApp.getHttpAdapter().getInstance().set('trust proxy', true);
Expand Down Expand Up @@ -74,13 +80,55 @@ export async function app(httpAdapter: AbstractHttpAdapter = null): Promise<INes
const timeStart = new Date();
res.locals['abort-controller'] = new AbortController();
req.on('close', () => {
res.locals['abort-controller'].abort();
try {
res.locals['abort-controller'].abort();
} catch (e) {
gs.log('[REQUEST_ON_CLOSED] ❌', e, 'error');
}
});
res.on('close', async () => {
const clientOriginIpCc = aks.getOriginIpCc(req, true);
const timeEnd = Date.now() - timeStart.getTime();
const reqResInfo = `${clientOriginIpCc.origin_ip} ~ ${timeStart.toString()} ~ ${req.method} ~ ${res.statusCode} ~ ${req.originalUrl} ~ ${timeEnd} ms`;
await sis.emitToRoomOrId(CONSTANTS.socketRoomNameServerLogs, 'console-log', reqResInfo);
try {
const clientOriginIpCc = aks.getOriginIpCc(req, true);
const timeEnd = Date.now() - timeStart.getTime();
const reqResInfo = `${clientOriginIpCc.origin_ip} ~ ${timeStart.toString()} ~ ${req.method} ~ ${res.statusCode} ~ ${req.originalUrl} ~ ${timeEnd} ms`;
await sis.emitToRoomOrId(CONSTANTS.socketRoomNameServerLogs, 'console-log', reqResInfo);
if (
clientOriginIpCc.origin_ip !== environment.domain &&
clientOriginIpCc.origin_ip !== environment.domain_alt &&
clientOriginIpCc.origin_ip !== environment.ip &&
// 404 Not Found Will Redirect To Home Page
(res.statusCode === 404 || res.statusCode === 429)
) {
const failToBan = await ftb.find({
where: [
{ ip_domain: Equal(clientOriginIpCc.origin_ip) }
],
order: {
ip_domain: 'ASC'
}
});
if (failToBan.length === 0) {
const _ftb = ftb.new();
_ftb.ip_domain = clientOriginIpCc.origin_ip;
await ftb.save(_ftb);
} else if (failToBan.length === 1) {
const _ftb = failToBan[0];
_ftb.fail_count++;
const resSaveFtb = await ftb.save(_ftb);
if (resSaveFtb.fail_count >= CONSTANTS.failToBanMaxCountPerMin) {
const resBan = await cfs.createFailToBan(resSaveFtb.ip_domain);
if (resBan && resBan.status >= 200 && resBan.status < 400) {
resSaveFtb.rule_id = resBan.result.id;
await ftb.save(resSaveFtb);
}
}
} else {
throw new Error('Data Duplikat!');
}
}
} catch (e) {
gs.log('[RESPONSE_ON_CLOSED] ❌', e, 'error');
}
});
return next();
});
Expand Down
77 changes: 77 additions & 0 deletions src/api/repository/fail-to-ban.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { EntityMetadata, FindManyOptions, FindOneOptions, FindConditions, InsertResult, Repository, UpdateResult } from 'typeorm';

import { FailToBan } from '../entities/FailToBan';

import { GlobalService } from '../services/global.service';

@Injectable()
export class FailToBanService {

constructor(
@InjectRepository(FailToBan) private failToBanRepo: Repository<FailToBan>,
private gs: GlobalService
) {
//
}

new(): FailToBan {
return new FailToBan();
}

instance(): Repository<FailToBan> {
return this.failToBanRepo;
}

getMetaData(): EntityMetadata {
return this.failToBanRepo.metadata;
}

find(options: FindManyOptions<FailToBan>): Promise<FailToBan[]> {
this.gs.log('[FAIL_TO_BAN_SERVICE-FIND_ALL] 🧱', options);
return this.failToBanRepo.find(options);
}

findAndCount(options: FindManyOptions<FailToBan>): Promise<[FailToBan[], number]> {
this.gs.log('[FAIL_TO_BAN_SERVICE-FIND_AND_COUNT] 🧱', options);
return this.failToBanRepo.findAndCount(options);
}

findOneOrFail(options: FindOneOptions<FailToBan>): Promise<FailToBan> {
this.gs.log('[FAIL_TO_BAN_SERVICE-GET_BY] 🧱', options);
return this.failToBanRepo.findOneOrFail(options);
}

save<T = FailToBan | FailToBan[]>(failToBan: T): Promise<T> {
this.gs.log('[FAIL_TO_BAN_SERVICE-SAVE] 🧱', failToBan);
return this.failToBanRepo.save(failToBan);
}

count(options: FindManyOptions<FailToBan>): Promise<number> {
this.gs.log('[FAIL_TO_BAN_SERVICE-COUNT] 🧱', options);
return this.failToBanRepo.count(options);
}

remove(failToBan: FailToBan | FailToBan[]): Promise<FailToBan | FailToBan[]> {
this.gs.log('[FAIL_TO_BAN_SERVICE-REMOVE] 🧱', failToBan);
return this.failToBanRepo.remove(failToBan as any);
}

query(query: string, parameters: any = []): Promise<any> {
this.gs.log('[FAIL_TO_BAN_SERVICE-QUERY] 🧱', query);
return this.failToBanRepo.query(query, parameters);
}

update(criteria: FindConditions<FailToBan>, partialEntity: QueryDeepPartialEntity<FailToBan>): Promise<UpdateResult> {
this.gs.log('[FAIL_TO_BAN_SERVICE-UPDATE] 🧱', criteria);
return this.failToBanRepo.update(criteria, partialEntity);
}

insert(failToBan: FailToBan): Promise<InsertResult> {
this.gs.log('[FAIL_TO_BAN_SERVICE-INSERT] 🧱', failToBan);
return this.failToBanRepo.insert(failToBan);
}

}
73 changes: 73 additions & 0 deletions src/api/scheduler/cf-waf-task.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// NodeJS Library
import cluster from 'node:cluster';

import { Injectable } from '@nestjs/common';
import { Cron, CronExpression, SchedulerRegistry } from '@nestjs/schedule';

import { CONSTANTS } from '../../constants';

import { GlobalService } from '../services/global.service';
import { CloudflareService } from '../services/cloudflare.service';
import { FailToBanService } from '../repository/fail-to-ban.service';

@Injectable()
export class CfWafService {

constructor(
private sr: SchedulerRegistry,
private gs: GlobalService,
private cfs: CloudflareService,
private ftb: FailToBanService
) {
//
}

@Cron(
CronExpression.EVERY_MINUTE,
{
name: CONSTANTS.cronCloudflareBan
}
)
async unBanIp(): Promise<void> {
if (cluster.isMaster) {
const job = this.sr.getCronJob(CONSTANTS.cronCloudflareBan);
job.stop();
const startTime = new Date();
this.gs.log('[CRON_TASK_CLOUDFLARE_BAN-START] 🐾', `${startTime}`);
try {
const failToBan = await this.ftb.find({
order: {
ip_domain: 'ASC'
}
});
for (const _ftb of failToBan) {
try {
if (_ftb.rule_id) {
const diffDateMs = startTime.getTime() - new Date(_ftb.updated_at).getTime();
// Approx. Min 1 Hours IP Ban (+ 1 Min Max Cron Delay)
if (diffDateMs >= 1 * 60 * 60 * 1000) {
const resBan = await this.cfs.deleteFailToBan(_ftb.rule_id);
if (resBan && resBan.status >= 200 && resBan.status < 400) {
await this.ftb.remove(_ftb);
}
}
} else {
await this.ftb.remove(_ftb);
}
} catch (e) {
this.gs.log('[CRON_TASK_CLOUDFLARE_BAN-ERROR_DELETE] 🐾', e, 'error');
}
}
} catch (error) {
this.gs.log('[CRON_TASK_CLOUDFLARE_BAN-ERROR] 🐾', error, 'error');
}
const endTime = new Date();
const elapsedTime = endTime.getTime() - startTime.getTime();
this.gs.log('[CRON_TASK_CLOUDFLARE_BAN-END] 🐾', `${endTime} @ ${elapsedTime} ms`);
job.start();
} else {
this.sr.getCronJob(CONSTANTS.cronCloudflareBan).stop();
}
}

}
Loading

0 comments on commit 62ecca7

Please sign in to comment.