Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,8 @@ OTP_RESEND_DECAY_SECONDS=30
# Telegram Bot Configuration
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
TELEGRAM_BOT_NAME=@your_bot_username_here

# Global Rate Limiter Configuration
RATE_LIMITER_ENABLED=false
RATE_LIMITER_MAX_ATTEMPTS=60
RATE_LIMITER_DECAY_MINUTES=1
1 change: 1 addition & 0 deletions app/Http/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class Kernel extends HttpKernel
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
Middleware\GlobalRateLimiter::class,
];

/**
Expand Down
171 changes: 171 additions & 0 deletions app/Http/Middleware/GlobalRateLimiter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Cache\RateLimiter;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Symfony\Component\HttpFoundation\Response;

class GlobalRateLimiter
{
/**
* The rate limiter instance.
*
* @var \Illuminate\Cache\RateLimiter
*/
protected $limiter;

/**
* Create a new rate limiter middleware instance.
*
* @param \Illuminate\Cache\RateLimiter $limiter
* @return void
*/
public function __construct(RateLimiter $limiter)
{
$this->limiter = $limiter;
}

/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return \Symfony\Component\HttpFoundation\Response
*/
public function handle(Request $request, Closure $next): Response
{
// Check if global rate limiter is enabled
if (!config('rate-limiter.enabled', false)) {
return $next($request);
}

// Check if current IP should be excluded
if ($this->shouldExcludeIp($request)) {
return $next($request);
}

// Check if current path should be excluded
if ($this->shouldExcludePath($request)) {
return $next($request);
}

// Get configuration from .env or use defaults
$maxAttempts = config('rate-limiter.max_attempts', 60);
$decayMinutes = config('rate-limiter.decay_minutes', 1);

// Generate unique key for this request based on IP
$key = $this->resolveRequestSignature($request);

// Check if the request limit has been exceeded
if ($this->limiter->tooManyAttempts($key, $maxAttempts)) {
return $this->buildResponse($key, $maxAttempts);
}

// Add hit to the limiter
$this->limiter->hit($key, $decayMinutes * 60);

$response = $next($request);

// Add headers to the response
$response->headers->set('X-RateLimit-Limit', $maxAttempts);
$response->headers->set('X-RateLimit-Remaining', max(0, $maxAttempts - $this->limiter->attempts($key)));
$response->headers->set('X-RateLimit-Reset', $this->limiter->availableIn($key));

return $response;
}

/**
* Resolve request signature.
*
* @param \Illuminate\Http\Request $request
* @return string
*/
protected function resolveRequestSignature(Request $request): string
{
// Use IP address as the signature for global rate limiting
return sha1(
'global-rate-limit:' . $request->ip()
);
}

/**
* Create a 'too many attempts' response.
*
* @param string $key
* @param int $maxAttempts
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function buildResponse(string $key, int $maxAttempts): Response
{
$seconds = $this->limiter->availableIn($key);
$request = request();

if (App::runningInConsole() || $request->expectsJson()) {
return response()->json([
'message' => 'Too many requests. Please try again later.',
'status' => 'error',
'code' => 429,
'retry_after' => $seconds,
], 429);
}

return response('Too Many Attempts.', 429, [
'Retry-After' => $seconds,
'X-RateLimit-Limit' => $maxAttempts,
'X-RateLimit-Remaining' => 0,
'X-RateLimit-Reset' => $seconds,
]);
}

/**
* Determine if the request IP should be excluded from rate limiting.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
protected function shouldExcludeIp(Request $request): bool
{
$excludeIps = config('rate-limiter.exclude_ips', []);

return in_array($request->ip(), $excludeIps);
}

/**
* Determine if the request path should be excluded from rate limiting.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
protected function shouldExcludePath(Request $request): bool
{
$excludePaths = config('rate-limiter.exclude_paths', []);
$requestPath = $request->path();

foreach ($excludePaths as $path) {
if ($this->pathMatches($path, $requestPath)) {
return true;
}
}

return false;
}

/**
* Check if the path matches the pattern.
*
* @param string $pattern
* @param string $path
* @return bool
*/
protected function pathMatches(string $pattern, string $path): bool
{
// Convert wildcard pattern to regex
$pattern = preg_quote($pattern, '#');
$pattern = str_replace('\*', '.*', $pattern);

return preg_match("#^{$pattern}$#", $path);
}
}
3 changes: 2 additions & 1 deletion catatan_rilis.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ Di rilis ini, versi 2512.0.1 berisi penambahan dan perbaikan yang diminta penggu
#### Perubahan Teknis

1. [#869](https://github.com/OpenSID/OpenKab/issues/869) Upgrade versi moment pada chart.js serta perbaikan halaman website presisi untuk kependudukan dan RTM.
2. [#876](https://github.com/OpenSID/OpenKab/issues/876) Ganti highchart dengan chartjs agar menggunakan satu library saja.
2. [#876](https://github.com/OpenSID/OpenKab/issues/876) Ganti highchart dengan chartjs agar menggunakan satu library saja.
3. [#868](https://github.com/OpenSID/OpenKab/issues/868) Penerapan rate limiting pada OpenKab untuk membantu mencegah serangan DDOS.
79 changes: 79 additions & 0 deletions config/rate-limiter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

return [
/*
|--------------------------------------------------------------------------
| Global Rate Limiter Configuration
|--------------------------------------------------------------------------
|
| This configuration controls the global rate limiting for all requests
| to your application. You may configure these values in your .env file.
|
*/

/*
|--------------------------------------------------------------------------
| Enable Global Rate Limiter
|--------------------------------------------------------------------------
|
| This option controls whether the global rate limiter is enabled or not.
| When set to false, the rate limiter will be bypassed for all requests.
|
*/
'enabled' => env('RATE_LIMITER_ENABLED', false),

/*
|--------------------------------------------------------------------------
| Maximum Attempts
|--------------------------------------------------------------------------
|
| This value controls the maximum number of requests that can be made
| within the given decay period. Once this limit is reached, subsequent
| requests will be blocked until the decay period has elapsed.
|
*/
'max_attempts' => env('RATE_LIMITER_MAX_ATTEMPTS', 60),

/*
|--------------------------------------------------------------------------
| Decay Minutes
|--------------------------------------------------------------------------
|
| This value controls the number of minutes to wait before the rate
| limiter resets. After this period, the request count will be reset
| to zero and new requests will be allowed.
|
*/
'decay_minutes' => env('RATE_LIMITER_DECAY_MINUTES', 1),

/*
|--------------------------------------------------------------------------
| Exclude Paths
|--------------------------------------------------------------------------
|
| This array contains the paths that should be excluded from the
| global rate limiting. These paths will not be subject to the
| rate limiting rules defined above.
|
*/
'exclude_paths' => [
// 'api/health',
// 'api/ping',
// 'admin/*',
],

/*
|--------------------------------------------------------------------------
| Exclude IP Addresses
|--------------------------------------------------------------------------
|
| This array contains the IP addresses that should be excluded from
| the global rate limiting. These IP addresses will not be subject
| to the rate limiting rules defined above.
|
*/
'exclude_ips' => [
// '127.0.0.1',
// '192.168.1.1',
],
];
Loading