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); } }