Initial commit

This commit is contained in:
root
2026-05-09 17:28:23 +02:00
commit 9d73f82529
5575 changed files with 281989 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class AdminSecurityMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$user = Auth::user();
// Check 1: Must be authenticated
if (! $user) {
return response()->json(['error' => 'Unauthorized'], 401);
}
// Check 2: Must have admin rank
$minRank = (int) setting('min_staff_rank', 7);
if ($user->rank < $minRank) {
Log::warning('[Security] Unauthorized API access attempt', [
'user_id' => $user->id,
'username' => $user->username,
'ip' => $request->ip(),
'path' => $request->path(),
]);
return response()->json(['error' => 'Forbidden'], 403);
}
// Check 3: Audit log every API call
Log::info('[Audit] Admin API access', [
'user_id' => $user->id,
'username' => $user->username,
'method' => $request->method(),
'path' => $request->path(),
'ip' => $request->ip(),
'user_agent' => substr($request->userAgent() ?? '', 0, 200),
]);
return $next($request);
}
}
+94
View File
@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\HttpFoundation\Response;
class ApiResponseCache
{
private const int DEFAULT_TTL = 60;
public function handle(Request $request, Closure $next): Response
{
if (! $this->shouldCache($request)) {
return $next($request);
}
$cacheKey = $this->generateCacheKey($request);
if (Cache::has($cacheKey)) {
$cached = Cache::get($cacheKey);
if (is_array($cached) && isset($cached['data'], $cached['status'])) {
return response()->json($cached['data'], $cached['status'], $cached['headers'] ?? []);
}
}
$response = $next($request);
if ($response->isSuccessful()) {
$ttl = $this->getTtlFromRoute($request);
$data = json_decode((string) $response->getContent(), true);
if (is_array($data)) {
Cache::put($cacheKey, [
'data' => $data,
'status' => $response->getStatusCode(),
], $ttl);
}
}
return $response;
}
private function shouldCache(Request $request): bool
{
if (! in_array($request->method(), ['GET', 'HEAD'])) {
return false;
}
if ($request->bearerToken()) {
return false;
}
$cacheableRoutes = [
'/api/online-users',
'/api/online-count',
'/api/radio/current-dj',
'/api/radio/config',
'/api/radio/now-playing',
'/api/radio/listeners',
'/api/radio/shouts',
'/api/settings/radio',
'/api/radio/points/leaderboard',
'/api/radio/points/stats',
];
return array_any($cacheableRoutes, fn ($route) => str_starts_with($request->path(), ltrim((string) $route, '/')));
}
private function generateCacheKey(Request $request): string
{
return 'api_cache_' . md5($request->path() . ($request->getQueryString() ?? ''));
}
private function getTtlFromRoute(Request $request): int
{
$path = $request->path();
return match (true) {
str_contains($path, 'online-count') => 30,
str_contains($path, 'now-playing') => 15,
str_contains($path, 'current-dj') => 30,
str_contains($path, 'listeners') => 30,
str_contains($path, 'leaderboard') => 60,
str_contains($path, 'shouts') => 30,
default => self::DEFAULT_TTL,
};
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*/
#[\Override]
protected function redirectTo(Request $request): ?string
{
return $request->expectsJson() ? null : route('login');
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
namespace App\Http\Middleware;
use App\Models\User\Ban;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class BannedMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$authenticated = Auth::check();
$ipBan = Ban::where('ip', '=', $request->ip())
->where('ban_expire', '>', time())
->whereIn('type', ['ip', 'machine'])
->orderByDesc('id')
->exists();
if ($request->is('logout')) {
return $next($request);
}
if (! $authenticated && ! $ipBan && $request->is('banned')) {
return to_route('login');
}
if ($ipBan && ! $request->is('banned')) {
return to_route('banned.show');
}
if ($authenticated) {
$accountBan = $request->user()?->ban;
if ($accountBan && ! $request->is('banned')) {
return to_route('banned.show');
}
if (! $ipBan && ! $accountBan && $request->is('banned')) {
return to_route('me.show');
}
}
return $next($request);
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use App\Console\Commands\DDoSDetectionCommand;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class DDoSTrackingMiddleware
{
public function handle(Request $request, Closure $next): Response
{
if ((bool) setting('alert_ddos_enabled', false)) {
$ip = $request->ip();
DDoSDetectionCommand::trackRequest($ip);
}
return $next($request);
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array<int, string>
*/
#[\Override]
protected $except = [
//
];
}
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace App\Http\Middleware;
use App\Services\FindRetrosService;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
/* Credits to Kani for this */
class FindRetrosMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$findRetrosService = new FindRetrosService;
if (config('habbo.findretros.enabled') && ! $findRetrosService->checkHasVoted()) {
return redirect($findRetrosService->getRedirectUri());
}
return $next($request);
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class FixPermissionsPolicy
{
/**
* Handle an incoming request.
*
* @param Closure(Request): (Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$response->headers->remove('Permissions-Policy');
$response->headers->set('Permissions-Policy', 'fullscreen=*');
return $response;
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class ForceStaffTwoFactorMiddleware
{
public function handle(Request $request, Closure $next): Response
{
if (! Auth::check() || ! setting('force_staff_2fa')) {
return $next($request);
}
$user = $request->user();
$urls = [
'user/settings/two-factor',
'user/settings/2fa-verify',
];
if (($user->rank >= setting('min_staff_rank') && ! $user->two_factor_confirmed) && ! in_array(request()->path(), $urls)) {
return to_route('settings.two-factor');
}
return $next($request);
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace App\Http\Middleware;
use App\Models\Miscellaneous\WebsiteSetting;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class InjectPwaMeta
{
public function handle(Request $request, Closure $next): Response
{
$pwaEnabled = WebsiteSetting::where('key', 'pwa_enabled')->first()?->value === '1';
if ($pwaEnabled) {
$config = [
'name' => WebsiteSetting::where('key', 'pwa_name')->first()?->value ?? 'Epicnabbo',
'short_name' => WebsiteSetting::where('key', 'pwa_short_name')->first()?->value ?? 'Epicnabbo',
'theme_color' => WebsiteSetting::where('key', 'pwa_theme_color')->first()?->value ?? '#eeb425',
];
view()->share('pwa_enabled', true);
view()->share('pwa_config', $config);
} else {
view()->share('pwa_enabled', false);
}
return $next($request);
}
}
+180
View File
@@ -0,0 +1,180 @@
<?php
namespace App\Http\Middleware;
use App\Models\Miscellaneous\WebsiteInstallation;
use App\Services\InstallationService;
use Closure;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
class InstallationMiddleware
{
public function handle(Request $request, Closure $next)
{
if (str_starts_with($request->path(), 'api/')) {
return $next($request);
}
if (app(InstallationService::class)->isComplete()) {
if ($request->is('installation*')) {
return to_route('welcome');
}
return $next($request);
}
$this->ensureInstallationTableExists();
$installation = $this->getInstallation();
$isInstallationStepHandled = $this->handleInstallationSteps($request, $installation);
if (! $isInstallationStepHandled) {
return $this->redirectIfNotCompleted($installation);
}
return $next($request);
}
private function ensureInstallationTableExists(): void
{
if (! Schema::hasTable('website_installation')) {
Artisan::call('migrate', ['--path' => 'database/migrations/' . findMigration('website_installation')]);
// Migration executed, proceed
}
if (! Schema::hasTable('sessions')) {
Artisan::call('migrate', ['--path' => 'database/migrations/' . findMigration('sessions')]);
}
}
private function getInstallation()
{
try {
$cacheKey = 'installation_record';
$cachedInstallation = Cache::get($cacheKey);
if ($cachedInstallation) {
$installation = new WebsiteInstallation;
$installation->fill((array) $cachedInstallation);
return $installation;
}
$installation = WebsiteInstallation::query()->first();
if (! $installation) {
$installation = WebsiteInstallation::create([
'step' => 0,
'completed' => false,
'installation_key' => Str::uuid(),
'user_ip' => request()->ip(),
]);
}
if ($installation instanceof WebsiteInstallation && $installation->completed) {
Cache::rememberForever($cacheKey, fn () => $installation->toArray());
}
return $installation;
} catch (Exception $e) {
Log::error('Error fetching or creating WebsiteInstallation: ' . $e->getMessage());
abort(500, 'An error occurred while setting up installation.');
}
}
private function handleInstallationSteps(Request $request, WebsiteInstallation $installation)
{
if ($installation->completed) {
return true;
}
if ($this->isWelcomeStep($request, $installation)) {
return true;
}
if ($this->isRedirectToWelcome($request, $installation)) {
return false;
}
if ($this->isInvalidAccess($request, $installation)) {
abort(403);
}
if ($this->isInvalidStep($request)) {
return false;
}
return ! $this->isMismatchedStep($request, $installation);
}
private function isWelcomeStep(Request $request, WebsiteInstallation $installation)
{
return $installation->step === 0 && $request->getRequestUri() === '/installation';
}
private function isRedirectToWelcome(Request $request, WebsiteInstallation $installation)
{
return $installation->step === 0 && $request->getRequestUri() !== '/installation' && $request->method() !== 'POST';
}
private function isInvalidAccess(Request $request, WebsiteInstallation $installation)
{
// Skip IP check during testing
if (app()->environment('testing')) {
return false;
}
return $installation->step > 0 && $request->ip() !== $installation->user_ip;
}
private function isInvalidStep(Request $request)
{
return ! $this->isValidStep($request) && $this->isNonPostRequest($request);
}
private function isMismatchedStep(Request $request, WebsiteInstallation $installation)
{
return $this->getCurrentStep($request) !== $installation->step && $this->isNonPostRequest($request);
}
private function isValidStep(Request $request)
{
$step = $this->getCurrentStep($request);
return filter_var($step, FILTER_VALIDATE_INT) !== false;
}
private function isNonPostRequest(Request $request)
{
return $request->method() !== 'POST' || $request->is('restart-installation');
}
private function getCurrentStep(Request $request)
{
return (int) Str::after($request->path(), 'step/');
}
private function redirectToStep(int $step)
{
return to_route('installation.show-step', $step);
}
protected function redirectIfNotCompleted(WebsiteInstallation $installation)
{
if ($installation->step === 0) {
return to_route('installation.index');
}
return $this->redirectToStep($installation->step ?: 1);
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace App\Http\Middleware;
use App\Models\Miscellaneous\WebsiteLanguage;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Session;
use Symfony\Component\HttpFoundation\Response;
class LocalizationMiddleware
{
public function handle(Request $request, Closure $next): Response
{
if (Schema::hasTable('website_settings') && Session::has('locale')) {
App::setLocale(Session::get('locale'));
return $next($request);
}
$countryCode = config('habbo.site.default_language');
if (isset($_SERVER['HTTP_CF_IPCOUNTRY'])) {
$countryCode = strtolower((string) $_SERVER['HTTP_CF_IPCOUNTRY']);
} elseif (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
$countryCode = strtolower(substr((string) $_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2));
}
// If the language does not exist in the database
if (Schema::hasTable('website_languages') && WebsiteLanguage::where('country_code', '=', $countryCode)->doesntExist()) {
$defaultCountry = config('habbo.site.default_language');
App::setLocale($defaultCountry);
Session::put('locale', $defaultCountry);
return $next($request);
}
App::setLocale($countryCode);
Session::put('locale', $countryCode);
return $next($request);
}
}
+93
View File
@@ -0,0 +1,93 @@
<?php
namespace App\Http\Middleware;
use App\Models\StaffActivity;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class LogStaffActivity
{
public function handle(Request $request, Closure $next): Response
{
return $next($request);
}
public function terminate(Request $request, Response $response): void
{
if (auth()->check() && auth()->user()->rank >= (int) setting('min_staff_rank', 3)) {
$this->logRequest($request);
}
}
private function logRequest(Request $request): void
{
$user = auth()->user();
$path = $request->path();
if (str_contains($path, 'housekeeping') || str_starts_with($path, 'hk')) {
$action = $this->determineAction($request);
$description = $this->generateDescription($request);
if ($action && $description) {
StaffActivity::log(
$user->id,
$action,
$description,
$request->route()?->getName(),
null,
[
'method' => $request->method(),
'path' => $path,
'route' => $request->route()?->getName(),
],
);
}
}
}
private function determineAction(Request $request): ?string
{
$method = $request->method();
$path = $request->path();
if ($method === 'POST') {
if (str_contains($path, 'ban')) {
return 'user_ban';
} elseif (str_contains($path, 'delete')) {
return 'content_delete';
} elseif (str_contains($path, 'create') || str_contains($path, 'store')) {
return 'content_create';
} elseif (str_contains($path, 'edit') || str_contains($path, 'update')) {
return 'content_edit';
} elseif (str_contains($path, 'rank')) {
return 'rank_change';
} elseif (str_contains($path, 'settings')) {
return 'settings_update';
}
}
return null;
}
private function generateDescription(Request $request): ?string
{
$action = $this->determineAction($request);
if (! $action) {
return null;
}
return match ($action) {
'user_ban' => 'Banned user: ' . ($request->input('user_id') ?? $request->input('username') ?? 'unknown'),
'user_unban' => 'Unbanned user',
'content_delete' => 'Deleted content: ' . $request->path(),
'content_create' => 'Created new content',
'content_edit' => 'Updated content: ' . $request->path(),
'rank_change' => 'Changed user rank',
'settings_update' => 'Updated settings',
default => 'Performed action: ' . $action,
};
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class LogViewerMiddleware
{
public function handle(Request $request, Closure $next): Response
{
if (! Auth::check()) {
return to_route('login');
}
if (! hasPermission('view_server_logs')) {
abort(403);
}
return $next($request);
}
}
+76
View File
@@ -0,0 +1,76 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
use Throwable;
class MaintenanceMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$isPostRequest = $request->method() === 'POST';
$isMaintenanceRequest = $request->is('maintenance');
$maintenanceEnabled = $this->getMaintenanceEnabled();
$isPreview = $request->query('preview') === '1';
$minRank = $this->getMinMaintenanceRank();
$fortify2faRoutes = [
'two-factor.login',
'two-factor.confirm',
];
if ($maintenanceEnabled && $isPostRequest && ! Auth::check()) {
return $next($request);
}
$route = $request->route();
$routeName = $route?->getName();
$isFortify2faRoute = in_array($routeName, $fortify2faRoutes, true);
if ($maintenanceEnabled && $isFortify2faRoute) {
return $next($request);
}
$isStaff = Auth::check() && Auth::user()->rank >= $minRank;
if ($isStaff) {
if ($isMaintenanceRequest && ! $isPreview) {
return to_route('me.show');
}
return $next($request);
}
if ($maintenanceEnabled && ! $isMaintenanceRequest && ! $isPostRequest) {
return to_route('maintenance.show');
}
if (! $maintenanceEnabled && $isMaintenanceRequest && ! $isPostRequest && ! $isPreview) {
return to_route('welcome');
}
return $next($request);
}
private function getMaintenanceEnabled(): bool
{
try {
return (bool) setting('maintenance_enabled');
} catch (Throwable) {
return false;
}
}
private function getMinMaintenanceRank(): int
{
try {
return (int) setting('min_maintenance_login_rank', 1);
} catch (Throwable) {
return 1;
}
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
class PreventRequestsDuringMaintenance extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array<int, string>
*/
#[\Override]
protected $except = [
//
];
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class RealClientIpMiddleware
{
public function handle(Request $request, Closure $next)
{
$proxyHeaders = [
'HTTP_CF_CONNECTING_IP',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_REAL_IP',
'HTTP_CLIENT_IP',
'HTTP_TRUE_CLIENT_IP',
];
foreach ($proxyHeaders as $header) {
if (! empty($_SERVER[$header])) {
$ip = $_SERVER[$header];
if (str_contains((string) $ip, ',')) {
[$ip] = explode(',', (string) $ip);
}
$ip = trim((string) $ip);
if (filter_var($ip, FILTER_VALIDATE_IP)) {
// Set the real IP as REMOTE_ADDR
$request->server->set('REMOTE_ADDR', $ip);
break;
}
}
}
// Special handling for REMOTE_ADDR with multiple IPs
$remoteAddr = $_SERVER['REMOTE_ADDR'] ?? '';
if (! empty($remoteAddr) && str_contains((string) $remoteAddr, ',')) {
[$ip] = explode(',', (string) $remoteAddr);
$ip = trim($ip);
if (filter_var($ip, FILTER_VALIDATE_IP)) {
$request->server->set('REMOTE_ADDR', $ip);
}
}
return $next($request);
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param Closure(Request):((\Illuminate\Http\Response|RedirectResponse)) $next
*/
public function handle(Request $request, Closure $next, string ...$guards): Response
{
$guards = $guards === [] ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
if ($request->expectsJson()) {
return response()->json();
}
return redirect(RouteServiceProvider::HOME);
}
}
return $next($request);
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Qirolab\Theme\Theme;
use Symfony\Component\HttpFoundation\Response;
class SetThemeMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$theme = setting('theme');
if (empty($theme) || $theme === '1') {
Theme::set('atom');
} else {
Theme::set($theme);
}
return $next($request);
}
}
+54
View File
@@ -0,0 +1,54 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Config;
class SystemCheck
{
public function handle(Request $request, Closure $next)
{
$checks = [];
// 1. Check .env exists
if (! file_exists(base_path('.env'))) {
if (file_exists(base_path('.env.example'))) {
copy(base_path('.env.example'), base_path('.env'));
Artisan::call('key:generate', ['--no-interaction' => true]);
$checks[] = 'Created .env from example';
}
}
// 2. Check APP_KEY
if (empty(Config::get('app.key'))) {
Artisan::call('key:generate', ['--no-interaction' => true]);
$checks[] = 'Generated APP_KEY';
}
// 3. Check storage symlink
if (! file_exists(public_path('storage'))) {
Artisan::call('storage:link');
$checks[] = 'Created storage symlink';
}
// 4. Check required PHP extensions
$required = ['pdo_mysql', 'curl', 'mbstring', 'openssl', 'tokenizer'];
$missing = array_filter($required, fn ($ext) => ! extension_loaded($ext));
if (! empty($missing)) {
return response()->json([
'error' => 'Missing PHP extensions: ' . implode(', ', $missing),
'solution' => 'Install: apt install php-' . implode(' php-', $missing),
], 500);
}
// 5. Log any auto-fixes
if (! empty($checks)) {
\Log::info('SystemCheck auto-fixed: ' . implode(', ', $checks));
}
return $next($request);
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array<int, string>
*/
#[\Override]
protected $except = [
'current_password',
'password',
'password_confirmation',
];
}
+32
View File
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array<int, string>|string|null
*/
#[\Override]
protected $proxies;
/**
* The headers that should be used to detect proxies.
*
* @var int
*/
#[\Override]
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
}
+124
View File
@@ -0,0 +1,124 @@
<?php
namespace App\Http\Middleware;
use App\Models\Miscellaneous\WebsiteBlockedCountry;
use App\Models\Miscellaneous\WebsiteIpBlacklist;
use App\Models\Miscellaneous\WebsiteIpWhitelist;
use App\Services\IpLookupService;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class VPNCheckerMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$isEnabled = setting('vpn_block_enabled') === '1';
if (! $isEnabled) {
return $next($request);
}
if (hasPermission('bypass_vpn')) {
return $next($request);
}
$userIp = $request->ip();
if (WebsiteIpWhitelist::where('ip_address', $userIp)->exists()) {
return $next($request);
}
if (WebsiteIpBlacklist::where('ip_address', $userIp)->exists()) {
return $this->denyAccess($request);
}
$ipService = new IpLookupService('');
$countryInfo = $ipService->getCountryInfo($userIp);
if (! empty($countryInfo['country_code'])) {
$countryCode = $countryInfo['country_code'];
if (WebsiteBlockedCountry::where('country_code', $countryCode)->exists()) {
return $this->denyAccess($request);
}
$countryWhitelisted = WebsiteIpWhitelist::where('country_code', $countryCode)
->where('whitelist_country', true)
->exists();
if (! $countryWhitelisted) {
$countryBlacklisted = WebsiteIpBlacklist::where('country_code', $countryCode)
->where('blacklist_country', true)
->exists();
if ($countryBlacklisted) {
return $this->denyAccess($request);
}
}
}
$threatInfo = $ipService->checkVpnProxyTor($userIp);
if (! empty($threatInfo['asn'])) {
$asnWhitelisted = WebsiteIpWhitelist::where('asn', $threatInfo['asn'])
->where('whitelist_asn', true)
->exists();
if (! $asnWhitelisted) {
$asnBlacklisted = WebsiteIpBlacklist::where('asn', $threatInfo['asn'])
->where('blacklist_asn', true)
->exists();
if ($asnBlacklisted) {
return $this->denyAccess($request);
}
}
}
$blockVpn = setting('block_vpn') === '1';
$blockTor = setting('block_tor') === '1';
$blockMalicious = setting('block_malicious') === '1';
$shouldBlock = false;
if ($blockVpn && ($threatInfo['is_vpn'] ?? false)) {
$shouldBlock = true;
}
if ($blockTor && ($threatInfo['is_tor'] ?? false)) {
$shouldBlock = true;
}
if ($blockMalicious && ($threatInfo['is_malicious'] ?? false)) {
$shouldBlock = true;
}
if ($shouldBlock) {
WebsiteIpBlacklist::create([
'ip_address' => $userIp,
'asn' => $threatInfo['asn'] ?? null,
'country_code' => $countryInfo['country_code'] ?? null,
]);
return $this->denyAccess($request);
}
return $next($request);
}
private function denyAccess(Request $request): Response
{
if ($request->expectsJson()) {
return response()->json([
'message' => __('Your IP has been restricted - If you think this is a mistake, you can contact us on our Discord.'),
], 403);
}
return to_route('me.show')->withErrors([
'message' => __('Your IP has been restricted - If you think this is a mistake, you can contact us on our Discord.'),
]);
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Illuminate\Routing\Middleware\ValidateSignature as Middleware;
class ValidateSignature extends Middleware
{
/**
* The names of the query string parameters that should be ignored.
*
* @var array<int, string>
*/
protected $except = [
// 'fbclid',
// 'utm_campaign',
// 'utm_content',
// 'utm_medium',
// 'utm_source',
// 'utm_term',
];
}
+18
View File
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
class VerifyCsrfToken extends \Illuminate\Foundation\Http\Middleware\VerifyCsrfToken
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array<int, string>
*/
#[\Override]
protected $except = [
//
];
}