Files
rspade_system/app/Http/Middleware/Gatekeeper.php
root 37a6183eb4 Fix code quality violations and add VS Code extension features
Fix VS Code extension storage paths for new directory structure
Fix jqhtml compiled files missing from bundle
Fix bundle babel transformation and add rsxrealpath() function

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 00:43:05 +00:00

171 lines
5.6 KiB
PHP
Executable File

<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class Gatekeeper
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
// Check if gatekeeper is enabled
if (!config('rsx.gatekeeper.enabled')) {
return $next($request);
}
// Always allow CLI requests
if (php_sapi_name() === 'cli') {
return $next($request);
}
// Check if request is whitelisted (localhost without reverse proxy headers)
if ($this->is_whitelisted($request)) {
return $next($request);
}
// Check if user has valid authentication cookie
$cookie_name = config('rsx.gatekeeper.cookie_name', 'gatekeeper_auth');
$password = config('rsx.gatekeeper.password');
if (!$password) {
throw new \Exception('Gatekeeper enabled but no password configured. Set GATEKEEPER_PASSWORD in .env');
}
$cookie_value = $request->cookie($cookie_name);
$expected_hash = hash('sha256', $password);
// If authenticated, renew cookie and continue
if ($cookie_value === $expected_hash) {
$lifetime_hours = config('rsx.gatekeeper.cookie_lifetime_hours', 12);
$cookie = cookie($cookie_name, $expected_hash, 60 * $lifetime_hours);
$response = $next($request);
// Only add cookie to regular responses, not binary file responses
if (method_exists($response, 'withCookie')) {
return $response->withCookie($cookie);
}
return $response;
}
// Handle login POST request
if ($request->isMethod('POST') && $request->path() === '_gatekeeper/login') {
return $this->handle_login($request);
}
// Show login page
return $this->show_login_page($request);
}
/**
* Check if the request is whitelisted (localhost without reverse proxy headers)
*/
private function is_whitelisted(Request $request): bool
{
// Get the client IP
$ip = $request->ip();
// List of localhost IPs
$localhost_ips = [
'127.0.0.1',
'localhost',
'::1',
'0.0.0.0',
];
// Check if IP matches localhost patterns
$is_localhost = in_array($ip, $localhost_ips) ||
str_starts_with($ip, '127.') ||
$ip === '::1';
if (!$is_localhost) {
return false;
}
// Check for reverse proxy headers - if present, this is NOT a true localhost request
$proxy_headers = [
'HTTP_X_FORWARDED_FOR',
'HTTP_X_FORWARDED_HOST',
'HTTP_X_FORWARDED_PORT',
'HTTP_X_FORWARDED_PROTO',
'HTTP_X_FORWARDED_SERVER',
'HTTP_X_REAL_IP',
'HTTP_X_ORIGINAL_URL',
'HTTP_FORWARDED',
'HTTP_CLIENT_IP',
'HTTP_CF_CONNECTING_IP', // Cloudflare
'HTTP_TRUE_CLIENT_IP', // Cloudflare Enterprise
'HTTP_X_CLUSTER_CLIENT_IP',
'HTTP_X_FORWARDED',
'HTTP_FORWARDED_FOR',
'HTTP_VIA',
];
foreach ($proxy_headers as $header) {
if (!empty($_SERVER[$header])) {
// Reverse proxy header detected - force authentication
return false;
}
}
// Also check Laravel's request headers
if ($request->headers->has('X-Forwarded-For') ||
$request->headers->has('X-Forwarded-Host') ||
$request->headers->has('X-Forwarded-Proto') ||
$request->headers->has('X-Real-IP') ||
$request->headers->has('Forwarded')) {
return false;
}
// True localhost request without proxy headers
return true;
}
/**
* Handle login POST request
*/
private function handle_login(Request $request): Response
{
$password = config('rsx.gatekeeper.password');
$submitted = $request->input('password');
if ($submitted === $password) {
// Authentication successful
$cookie_name = config('rsx.gatekeeper.cookie_name', 'gatekeeper_auth');
$lifetime_hours = config('rsx.gatekeeper.cookie_lifetime_hours', 12);
$cookie_value = hash('sha256', $password);
$cookie = cookie($cookie_name, $cookie_value, 60 * $lifetime_hours);
// Redirect to originally requested URL or home
$redirect = $request->input('redirect', '/');
return redirect($redirect)->withCookie($cookie);
}
// Authentication failed - show login page with error
return $this->show_login_page($request, 'Invalid password. Please try again.');
}
/**
* Show the gatekeeper login page
*/
private function show_login_page(Request $request, string $error = null): Response
{
$data = [
'title' => config('rsx.gatekeeper.title', 'Development Preview'),
'subtitle' => config('rsx.gatekeeper.subtitle', 'This is a restricted development preview site. Please enter the access password to continue.'),
'logo' => config('rsx.gatekeeper.logo'),
'error' => $error,
'redirect' => $request->fullUrl(),
];
return response()->view('gatekeeper.login', $data, 403);
}
}