Files
rspade_system/app/RSpade/Ide/Services/auth.php
root e678b987c2 Fix unimplemented login route with # prefix
Fix IDE service routing and path normalization
Refactor IDE services and add session rotation

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 15:59:42 +00:00

194 lines
6.3 KiB
PHP
Executable File

<?php
/**
* IDE Service Authentication
*
* SECURITY-CRITICAL: This file must be executed BEFORE any IDE service logic.
* All /_ide/service/* requests must pass through this authentication check.
*
* Two authentication modes:
* 1. Session-based auth with signature verification (production/remote)
* 2. Localhost bypass (development only, strict requirements)
*/
// Base path - framework is in system/ subdirectory
// @REALPATH-EXCEPTION - Bootstrap file: runs before helpers loaded, needs PHP's realpath()
$system_path = realpath(__DIR__ . '/../../../..');
// @REALPATH-EXCEPTION - Bootstrap file: runs before helpers loaded, needs PHP's realpath()
define('IDE_AUTH_BASE_PATH', realpath($system_path . '/..')); // Project root
define('IDE_AUTH_SYSTEM_PATH', $system_path); // Framework root
// Helper to get framework paths
function ide_auth_framework_path($relative_path) {
return IDE_AUTH_SYSTEM_PATH . '/' . ltrim($relative_path, '/');
}
// JSON response helper
function ide_auth_json_response($data, $code = 200) {
http_response_code($code);
header('Content-Type: application/json');
echo json_encode($data);
exit;
}
// Error response helper
function ide_auth_error_response($message, $code = 400) {
ide_auth_json_response(['success' => false, 'error' => $message], $code);
}
// Check if IDE services are enabled
$env_file = IDE_AUTH_BASE_PATH . '/.env';
if (file_exists($env_file)) {
$env_content = file_get_contents($env_file);
// Check if production
if (preg_match('/^APP_ENV=production$/m', $env_content)) {
// Check if explicitly enabled in production
if (!preg_match('/^RSX_IDE_SERVICES_ENABLED=true$/m', $env_content)) {
ide_auth_error_response('IDE services disabled in production', 403);
}
}
}
// Parse request URI to get service
$request_uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$service_path = str_replace('/_ide/service', '', $request_uri);
$service_path = trim($service_path, '/');
// Get request body for signature verification
$request_body = file_get_contents('php://input');
// Handle authentication
$auth_data = [];
$session_header = $_SERVER['HTTP_X_SESSION'] ?? null;
$signature_header = $_SERVER['HTTP_X_SIGNATURE'] ?? null;
// Check if this is auth/create request (no auth required for session creation)
if ($service_path === 'auth/create' && $_SERVER['REQUEST_METHOD'] === 'POST') {
// Generate new auth session
$session = bin2hex(random_bytes(20));
$client_key = bin2hex(random_bytes(20));
$server_key = bin2hex(random_bytes(20));
$auth_data_new = [
'session' => $session,
'client_key' => $client_key,
'server_key' => $server_key,
'created' => time()
];
// Save auth file
$bridge_dir = ide_auth_framework_path('storage/rsx-ide-bridge');
if (!is_dir($bridge_dir)) {
mkdir($bridge_dir, 0755, true);
}
// Rotate old sessions - keep max 100 auth files
$auth_files = glob($bridge_dir . '/auth-*.json');
if (count($auth_files) >= 100) {
// Sort by modification time (oldest first)
usort($auth_files, function($a, $b) {
return filemtime($a) - filemtime($b);
});
// Delete oldest files to get down to 99 (so we can add the new one)
$files_to_delete = count($auth_files) - 99;
for ($i = 0; $i < $files_to_delete; $i++) {
unlink($auth_files[$i]);
}
}
$auth_file = $bridge_dir . '/auth-' . $session . '.json';
file_put_contents($auth_file, json_encode($auth_data_new, JSON_PRETTY_PRINT));
// Return auth data (unsigned response since client doesn't have keys yet)
http_response_code(200);
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'session' => $session,
'client_key' => $client_key,
'server_key' => $server_key
]);
exit;
}
// Authentication required for all other requests
// Handle authentication with localhost bypass
// Localhost bypass for IDE integration
$is_localhost_bypass = false;
$request_host = $_SERVER['HTTP_HOST'] ?? '';
$request_scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$remote_addr = $_SERVER['REMOTE_ADDR'] ?? '';
// Check for proxy headers
$has_proxy_headers = false;
foreach ($_SERVER as $key => $value) {
if (str_starts_with($key, 'HTTP_X_')) {
if ($key === 'HTTP_X_SESSION' || $key === 'HTTP_X_SIGNATURE') {
continue;
}
$has_proxy_headers = true;
break;
}
}
// Check if request is from loopback
$is_loopback_ip = (
!$has_proxy_headers &&
($remote_addr === '127.0.0.1' ||
$remote_addr === '::1' ||
str_starts_with($remote_addr, '127.'))
);
// Check if not in production
$is_not_production = true;
if (file_exists($env_file)) {
$env_content = file_get_contents($env_file);
if (preg_match('/^APP_ENV=production$/m', $env_content)) {
$is_not_production = false;
}
}
// All conditions must be true for bypass
if (
$request_host === 'localhost' &&
$request_scheme === 'http' &&
$is_loopback_ip &&
$is_not_production
) {
$is_localhost_bypass = true;
$auth_data = [
'session' => 'localhost-bypass',
'client_key' => '',
'server_key' => ''
];
}
if (!$is_localhost_bypass) {
// Require authentication for non-localhost
if (!$session_header || !$signature_header) {
ide_auth_error_response('Authentication required', 401);
}
// Load and validate session
$auth_file = ide_auth_framework_path('storage/rsx-ide-bridge/auth-' . $session_header . '.json');
if (!file_exists($auth_file)) {
ide_auth_error_response('Session not found', 401);
}
$auth_data = json_decode(file_get_contents($auth_file), true);
$expected_signature = sha1($request_body . $auth_data['client_key']);
if ($signature_header !== $expected_signature) {
ide_auth_error_response('Invalid signature', 401);
}
}
// Authentication passed - store auth data for handlers to use if needed
define('IDE_AUTH_PASSED', true);
define('IDE_AUTH_DATA', json_encode($auth_data));
define('IDE_AUTH_IS_LOCALHOST_BYPASS', $is_localhost_bypass);
// Suppress console_debug output for IDE service requests
// These are programmatic API calls, not user-facing pages
define('SUPPRESS_CONSOLE_DEBUG_OUTPUT', true);