Files
rspade_system/app/Http/Middleware/Gatekeeper.php
root f6fac6c4bc Fix bin/publish: copy docs.dist from project root
Fix bin/publish: use correct .env path for rspade_system
Fix bin/publish script: prevent grep exit code 1 from terminating script

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 02:08:33 +00:00

176 lines
5.7 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);
}
// Always allow IDE helper endpoints for VS Code extension integration
if (str_starts_with($request->path(), '_idehelper')) {
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);
}
}