186 lines
6.3 KiB
PHP
186 lines
6.3 KiB
PHP
<?php
|
|
/**
|
|
* Caterium same-origin /api and api.caterium.ru -> 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 <img src>, 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;
|