Newer
Older
smart-home-server / webclient / proxy.php
<?php
// proxy.php

// =========================
// Настройки
// =========================

// Куда проксировать (без завершающего /)
$upstream_base_url = 'http://192.168.1.31';

// Какие пути разрешены (белый список) — подстрой под себя
$allowed_prefixes = [
	'/api/v1/',
];

// Кто может обращаться (CORS)
$allowed_origins = [
	'http://localhost:5173',
	'http://127.0.0.1:5173',
	// 'https://your-frontend-domain.com',
];

function send_json_error($code, $message, $extra = []) {
	http_response_code($code);
	header('Content-Type: application/json; charset=utf-8');
	echo json_encode(array_merge([
		'status' => false,
		'message' => $message,
	], $extra), JSON_UNESCAPED_UNICODE);
	exit;
}

function get_request_headers_lower() {
	$headers = [];
	foreach (getallheaders() as $k => $v) {
		$headers[strtolower($k)] = $v;
	}
	return $headers;
}

function cors_headers($origin, $allowed_origins) {
	if ($origin && in_array($origin, $allowed_origins, true)) {
		header("Access-Control-Allow-Origin: {$origin}");
		header("Vary: Origin");
	} else {
		// Если хочешь разрешить всем — раскомментируй (но лучше белый список)
		header("Access-Control-Allow-Origin: *");
	}

	header("Access-Control-Allow-Credentials: true");
	header("Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS");
	header("Access-Control-Allow-Headers: Authorization, Content-Type, Accept, X-Requested-With");
	header("Access-Control-Max-Age: 86400");
}

// =========================
// CORS preflight
// =========================
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
cors_headers($origin, $allowed_origins);

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
	http_response_code(204);
	exit;
}

// =========================
// Валидация входа
// =========================

// Ожидаем ?path=/api/v1/...
$path = $_GET['path'] ?? '';
if (!$path || $path[0] !== '/') {
	send_json_error(400, 'Missing or invalid "path" parameter. Example: ?path=/api/v1/scripts/actions/list');
}

// белый список путей
$ok = false;
foreach ($allowed_prefixes as $p) {
	if (str_starts_with($path, $p)) {
		$ok = true;
		break;
	}
}
if (!$ok) {
	send_json_error(403, 'Path not allowed', ['path' => $path]);
}

// Собираем URL апстрима + query string (кроме path)
$query = $_GET;
unset($query['path']);
$qs = http_build_query($query);
$upstream_url = rtrim($upstream_base_url, '/') . $path . ($qs ? ('?' . $qs) : '');

// =========================
// Проксирование через cURL
// =========================
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$headers_in = get_request_headers_lower();

$ch = curl_init($upstream_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_HEADER, true); // чтобы разделить headers/body
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);

// Проксируем body для методов кроме GET/HEAD
$body = file_get_contents('php://input');
if (!in_array($method, ['GET', 'HEAD'], true)) {
	curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
}

// Заголовки, которые прокидываем
$forward_headers = [];

// Authorization
if (!empty($headers_in['authorization'])) {
	$forward_headers[] = 'Authorization: ' . $headers_in['authorization'];
}

// Content-Type
if (!empty($headers_in['content-type'])) {
	$forward_headers[] = 'Content-Type: ' . $headers_in['content-type'];
}

// Accept
if (!empty($headers_in['accept'])) {
	$forward_headers[] = 'Accept: ' . $headers_in['accept'];
}

// Можно добавить свой заголовок, например X-Proxy
$forward_headers[] = 'X-Proxy: php';

curl_setopt($ch, CURLOPT_HTTPHEADER, $forward_headers);

$response = curl_exec($ch);
if ($response === false) {
	$err = curl_error($ch);
	curl_close($ch);
	send_json_error(502, 'Upstream request failed', ['details' => $err]);
}

$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
curl_close($ch);

$raw_headers = substr($response, 0, $header_size);
$resp_body = substr($response, $header_size);

// =========================
// Отдаём ответ клиенту
// =========================
http_response_code($http_code);

// Проксируем часть заголовков ответа (без CORS/опасных)
$lines = preg_split("/\r\n|\n|\r/", trim($raw_headers));
foreach ($lines as $line) {
	if (stripos($line, 'HTTP/') === 0) continue;

	$pos = strpos($line, ':');
	if ($pos === false) continue;

	$name = trim(substr($line, 0, $pos));
	$value = trim(substr($line, $pos + 1));

	$name_l = strtolower($name);

	// не прокидываем hop-by-hop и то, что конфликтует
	if (in_array($name_l, ['transfer-encoding', 'content-length', 'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailers', 'upgrade'], true)) {
		continue;
	}

	// CORS мы уже выставили сами
	if (str_starts_with($name_l, 'access-control-')) {
		continue;
	}

	header($name . ': ' . $value, false);
}

echo $resp_body;