Supabase HTTP proxy. * Forwards only REST/Auth/Storage/Edge Functions HTTP traffic. * Browser updates use authenticated HTTP polling; this is not a WebSocket proxy. * Upstream host is a fixed constant - never derived from request input (no open-proxy risk). */ const UPSTREAM = 'https://usfjwhztqoopzzfmfbis.supabase.co'; const ALLOWED_PREFIXES = ['/rest/v1/', '/auth/v1/', '/storage/v1/', '/functions/v1/']; // The same source is deployed to the dedicated API host and app /api/index.php. // Never accept another Host or derive the upstream host from browser input. $requestHost = strtolower(explode(':', $_SERVER['HTTP_HOST'] ?? '')[0]); $path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/'; $query = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_QUERY); if ($requestHost === 'app.caterium.ru' && $path === '/api/index.php' && is_string($_GET['__caterium_path'] ?? null)) { $path = $_GET['__caterium_path']; $query = implode('&', array_filter(explode('&', $query ?? ''), static function ($part) { return urldecode(explode('=', $part, 2)[0]) !== '__caterium_path'; })); } elseif ($requestHost !== 'api.caterium.ru') { http_response_code(404); exit; } const ALLOWED_ORIGINS = [ 'https://app.caterium.ru', 'https://caterium.ru', 'https://www.caterium.ru', ]; const FORWARD_REQUEST_HEADERS = [ 'authorization', 'apikey', 'content-type', 'prefer', 'range', 'x-client-info', 'x-supabase-api-version', 'accept-profile', 'content-profile', 'x-upsert', 'cache-control', ]; const STRIP_RESPONSE_HEADERS = [ 'transfer-encoding', 'connection', 'content-encoding', 'content-length', 'cache-control', 'expires', 'pragma', 'set-cookie', ]; function send_cors_headers(): void { $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; if (in_array($origin, ALLOWED_ORIGINS, true)) { header('Access-Control-Allow-Origin: ' . $origin); header('Vary: Origin'); } header('Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE, OPTIONS'); header('Access-Control-Allow-Headers: ' . implode(', ', FORWARD_REQUEST_HEADERS)); header('Access-Control-Max-Age: 86400'); } function request_headers(): array { if (function_exists('getallheaders')) { $raw = getallheaders(); if (is_array($raw)) { return $raw; } } // Fallback for environments without getallheaders(). $out = []; foreach ($_SERVER as $key => $value) { if (strpos($key, 'HTTP_') === 0) { $name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($key, 5))))); $out[$name] = $value; } } if (isset($_SERVER['CONTENT_TYPE'])) { $out['Content-Type'] = $_SERVER['CONTENT_TYPE']; } return $out; } send_cors_headers(); header('Cache-Control: private, no-store'); header('X-Content-Type-Options: nosniff'); if (($_SERVER['REQUEST_METHOD'] ?? '') === 'OPTIONS') { http_response_code(204); exit; } $allowed = false; foreach (ALLOWED_PREFIXES as $prefix) { if (strpos($path, $prefix) === 0) { $allowed = true; break; } } if (!$allowed || strpos(rawurldecode($path), '..') !== false || preg_match('/[\\\\?#\x00-\x20]/', $path)) { http_response_code(404); header('Content-Type: application/json'); echo json_encode(['error' => 'not_found', 'message' => 'Path not proxied.']); exit; } $upstreamUrl = UPSTREAM . $path . ($query !== null && $query !== '' ? '?' . $query : ''); $incoming = request_headers(); $incomingLower = []; foreach ($incoming as $name => $value) { $incomingLower[strtolower($name)] = $value; } $forwardHeaders = []; foreach (FORWARD_REQUEST_HEADERS as $name) { if (isset($incomingLower[$name]) && $incomingLower[$name] !== '') { $forwardHeaders[] = $name . ': ' . $incomingLower[$name]; } } $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; $body = ($method === 'GET' || $method === 'HEAD') ? null : file_get_contents('php://input'); $ch = curl_init($upstreamUrl); curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $forwardHeaders, CURLOPT_RETURNTRANSFER => true, CURLOPT_HEADER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => 30, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_NOBODY => $method === 'HEAD', ]); if ($body !== null && $body !== '') { curl_setopt($ch, CURLOPT_POSTFIELDS, $body); } $response = curl_exec($ch); if ($response === false) { http_response_code(502); header('Content-Type: application/json'); echo json_encode(['error' => 'upstream_unreachable']); curl_close($ch); exit; } $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $rawHeaders = substr($response, 0, $headerSize); $respBody = substr($response, $headerSize); curl_close($ch); http_response_code($statusCode); $responseContentType = ''; foreach (preg_split('/\r\n/', $rawHeaders) as $line) { if ($line === '' || stripos($line, 'HTTP/') === 0) { continue; } $colon = strpos($line, ':'); if ($colon === false) { continue; } $headerName = strtolower(trim(substr($line, 0, $colon))); if ($headerName === 'content-type') { $responseContentType = trim(substr($line, $colon + 1)); } if (in_array($headerName, STRIP_RESPONSE_HEADERS, true) || strpos($headerName, 'access-control-') === 0) { continue; } // Backend HTTP endpoints do not require external redirects. Keep any // upstream redirect on the dedicated API host, including signed downloads. if ($headerName === 'location') $line = str_replace(UPSTREAM, 'https://api.caterium.ru', $line); header($line, false); } // Supabase returns absolute upstream URLs inside some JSON bodies (e.g. Storage // createSignedUrl). Rewrite those to our public host so the browser's follow-up // request (an , a download link, ...) is proxied too, not sent to // *.supabase.co directly. Only touch text/JSON bodies - never binary payloads. if (stripos($responseContentType, 'application/json') !== false || stripos($responseContentType, 'text/') !== false) { $respBody = str_replace(UPSTREAM, 'https://api.caterium.ru', $respBody); } echo $respBody;