2026-02-22 02:18:02 -05:00
|
|
|
<?php
|
|
|
|
|
|
2026-02-22 02:43:43 -05:00
|
|
|
/*
|
|
|
|
|
* Chat Grid media proxy (compat-focused).
|
2026-02-22 02:18:02 -05:00
|
|
|
*
|
|
|
|
|
* Usage:
|
2026-02-22 02:43:43 -05:00
|
|
|
* /chgrid/media_proxy.php?url=<urlencoded-remote-url>
|
2026-02-22 02:18:02 -05:00
|
|
|
*
|
2026-02-22 02:43:43 -05:00
|
|
|
* Goals:
|
|
|
|
|
* - Works on older cPanel PHP web handlers.
|
|
|
|
|
* - Supports http/https upstreams.
|
|
|
|
|
* - Provides same-origin endpoint for browser playback.
|
2026-02-22 02:18:02 -05:00
|
|
|
*/
|
|
|
|
|
|
2026-02-22 02:43:43 -05:00
|
|
|
function set_status($code)
|
|
|
|
|
{
|
|
|
|
|
$map = array(
|
|
|
|
|
200 => 'OK',
|
|
|
|
|
204 => 'No Content',
|
|
|
|
|
400 => 'Bad Request',
|
|
|
|
|
403 => 'Forbidden',
|
|
|
|
|
405 => 'Method Not Allowed',
|
|
|
|
|
500 => 'Internal Server Error',
|
|
|
|
|
502 => 'Bad Gateway'
|
|
|
|
|
);
|
|
|
|
|
$text = isset($map[$code]) ? $map[$code] : 'OK';
|
|
|
|
|
header('HTTP/1.1 ' . (int) $code . ' ' . $text);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function send_text($code, $message)
|
|
|
|
|
{
|
|
|
|
|
set_status($code);
|
|
|
|
|
header('Content-Type: text/plain; charset=utf-8');
|
|
|
|
|
echo $message . "\n";
|
|
|
|
|
exit;
|
|
|
|
|
}
|
2026-02-22 02:18:02 -05:00
|
|
|
|
2026-02-22 02:38:06 -05:00
|
|
|
function host_matches_suffix($host, $suffix)
|
2026-02-22 02:24:32 -05:00
|
|
|
{
|
|
|
|
|
if ($suffix === '') {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if ($host === $suffix) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
$needle = '.' . $suffix;
|
|
|
|
|
if (strlen($host) < strlen($needle)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return substr($host, -strlen($needle)) === $needle;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 02:43:43 -05:00
|
|
|
header('Access-Control-Allow-Origin: *');
|
|
|
|
|
header('Access-Control-Allow-Methods: GET, HEAD, OPTIONS');
|
|
|
|
|
header('Access-Control-Allow-Headers: Range');
|
2026-02-22 02:18:02 -05:00
|
|
|
|
2026-02-22 02:43:43 -05:00
|
|
|
$method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET';
|
|
|
|
|
if ($method === 'OPTIONS') {
|
|
|
|
|
set_status(204);
|
2026-02-22 02:18:02 -05:00
|
|
|
exit;
|
|
|
|
|
}
|
2026-02-22 02:43:43 -05:00
|
|
|
if ($method !== 'GET' && $method !== 'HEAD') {
|
|
|
|
|
send_text(405, 'method not allowed');
|
|
|
|
|
}
|
2026-02-22 02:18:02 -05:00
|
|
|
|
|
|
|
|
$rawUrl = isset($_GET['url']) ? trim((string) $_GET['url']) : '';
|
|
|
|
|
if ($rawUrl === '') {
|
2026-02-22 02:43:43 -05:00
|
|
|
send_text(400, 'missing url query param');
|
2026-02-22 02:18:02 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$parsed = parse_url($rawUrl);
|
2026-02-22 02:43:43 -05:00
|
|
|
if ($parsed === false || !isset($parsed['scheme']) || !isset($parsed['host'])) {
|
|
|
|
|
send_text(400, 'invalid url');
|
2026-02-22 02:18:02 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$scheme = strtolower((string) $parsed['scheme']);
|
|
|
|
|
if ($scheme !== 'http' && $scheme !== 'https') {
|
2026-02-22 02:43:43 -05:00
|
|
|
send_text(400, 'unsupported scheme');
|
2026-02-22 02:18:02 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$host = strtolower((string) $parsed['host']);
|
|
|
|
|
if ($host === 'localhost' || $host === '127.0.0.1' || $host === '::1') {
|
2026-02-22 02:43:43 -05:00
|
|
|
send_text(403, 'forbidden host');
|
2026-02-22 02:18:02 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-22 02:43:43 -05:00
|
|
|
// Optional allowlist env var: CHGRID_MEDIA_PROXY_ALLOWLIST=dropbox.com,example.com
|
2026-02-22 02:18:02 -05:00
|
|
|
$allowlistEnv = getenv('CHGRID_MEDIA_PROXY_ALLOWLIST');
|
|
|
|
|
if ($allowlistEnv !== false && trim($allowlistEnv) !== '') {
|
|
|
|
|
$allowed = false;
|
2026-02-22 02:43:43 -05:00
|
|
|
$parts = explode(',', (string) $allowlistEnv);
|
|
|
|
|
foreach ($parts as $part) {
|
|
|
|
|
$suffix = strtolower(trim((string) $part));
|
2026-02-22 02:18:02 -05:00
|
|
|
if ($suffix === '') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-02-22 02:24:32 -05:00
|
|
|
if (host_matches_suffix($host, $suffix)) {
|
2026-02-22 02:18:02 -05:00
|
|
|
$allowed = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!$allowed) {
|
2026-02-22 02:43:43 -05:00
|
|
|
send_text(403, 'host not allowed');
|
2026-02-22 02:18:02 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 02:43:43 -05:00
|
|
|
// Basic SSRF guard for IPv4.
|
2026-02-22 02:18:02 -05:00
|
|
|
$resolved = @gethostbynamel($host);
|
|
|
|
|
if ($resolved === false || count($resolved) === 0) {
|
2026-02-22 02:43:43 -05:00
|
|
|
send_text(502, 'dns resolution failed');
|
2026-02-22 02:18:02 -05:00
|
|
|
}
|
|
|
|
|
foreach ($resolved as $ip) {
|
|
|
|
|
$ok = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
|
|
|
|
|
if ($ok === false) {
|
2026-02-22 02:43:43 -05:00
|
|
|
send_text(403, 'resolved ip not allowed');
|
2026-02-22 02:18:02 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 02:24:32 -05:00
|
|
|
if (!function_exists('curl_init')) {
|
2026-02-22 02:43:43 -05:00
|
|
|
send_text(500, 'curl extension is required');
|
2026-02-22 02:24:32 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-22 02:43:43 -05:00
|
|
|
$ch = curl_init();
|
|
|
|
|
if (!$ch) {
|
|
|
|
|
send_text(500, 'proxy init failed');
|
2026-02-22 02:18:02 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-22 02:43:43 -05:00
|
|
|
$requestHeaders = array('Accept: */*', 'Connection: keep-alive');
|
|
|
|
|
$range = isset($_SERVER['HTTP_RANGE']) ? trim((string) $_SERVER['HTTP_RANGE']) : '';
|
|
|
|
|
if ($range !== '') {
|
|
|
|
|
$requestHeaders[] = 'Range: ' . $range;
|
|
|
|
|
}
|
2026-02-22 02:18:02 -05:00
|
|
|
|
2026-02-22 02:43:43 -05:00
|
|
|
curl_setopt($ch, CURLOPT_URL, $rawUrl);
|
|
|
|
|
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
|
|
|
|
curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
|
|
|
|
|
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
|
|
|
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 45);
|
|
|
|
|
curl_setopt($ch, CURLOPT_HEADER, true);
|
|
|
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
|
|
|
curl_setopt($ch, CURLOPT_NOSIGNAL, true);
|
|
|
|
|
curl_setopt($ch, CURLOPT_USERAGENT, 'ChatGridMediaProxy/1.0');
|
|
|
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, $requestHeaders);
|
|
|
|
|
if ($method === 'HEAD') {
|
|
|
|
|
curl_setopt($ch, CURLOPT_NOBODY, true);
|
|
|
|
|
}
|
2026-02-22 02:18:02 -05:00
|
|
|
|
2026-02-22 02:43:43 -05:00
|
|
|
$response = curl_exec($ch);
|
|
|
|
|
if ($response === false) {
|
|
|
|
|
$err = curl_error($ch);
|
|
|
|
|
curl_close($ch);
|
|
|
|
|
send_text(502, 'upstream fetch failed: ' . $err);
|
2026-02-22 02:18:02 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-22 02:43:43 -05:00
|
|
|
$status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
|
|
|
|
if ($status < 100 || $status > 599) {
|
|
|
|
|
$status = 200;
|
2026-02-22 02:18:02 -05:00
|
|
|
}
|
2026-02-22 02:43:43 -05:00
|
|
|
$headerSize = (int) curl_getinfo($ch, CURLINFO_HEADER_SIZE);
|
|
|
|
|
curl_close($ch);
|
2026-02-22 02:18:02 -05:00
|
|
|
|
2026-02-22 02:43:43 -05:00
|
|
|
$rawHeaders = substr($response, 0, $headerSize);
|
|
|
|
|
$body = substr($response, $headerSize);
|
|
|
|
|
$headerBlocks = preg_split("/\r\n\r\n|\n\n/", trim($rawHeaders));
|
|
|
|
|
$lastHeaderBlock = '';
|
|
|
|
|
if (is_array($headerBlocks) && count($headerBlocks) > 0) {
|
|
|
|
|
$lastHeaderBlock = $headerBlocks[count($headerBlocks) - 1];
|
|
|
|
|
}
|
2026-02-22 02:18:02 -05:00
|
|
|
|
2026-02-22 02:43:43 -05:00
|
|
|
set_status($status);
|
2026-02-22 02:18:02 -05:00
|
|
|
|
2026-02-22 02:43:43 -05:00
|
|
|
$contentType = '';
|
|
|
|
|
$contentLength = '';
|
|
|
|
|
$acceptRanges = '';
|
|
|
|
|
$contentRange = '';
|
|
|
|
|
$cacheControl = '';
|
2026-02-22 02:18:02 -05:00
|
|
|
|
2026-02-22 02:43:43 -05:00
|
|
|
$lines = preg_split("/\r\n|\n/", $lastHeaderBlock);
|
|
|
|
|
if (is_array($lines)) {
|
|
|
|
|
foreach ($lines as $line) {
|
|
|
|
|
$line = trim($line);
|
|
|
|
|
if ($line === '' || stripos($line, 'HTTP/') === 0) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$split = explode(':', $line, 2);
|
|
|
|
|
if (count($split) !== 2) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$name = strtolower(trim($split[0]));
|
|
|
|
|
$value = trim($split[1]);
|
|
|
|
|
if ($name === 'content-type') $contentType = $value;
|
|
|
|
|
if ($name === 'content-length') $contentLength = $value;
|
|
|
|
|
if ($name === 'accept-ranges') $acceptRanges = $value;
|
|
|
|
|
if ($name === 'content-range') $contentRange = $value;
|
|
|
|
|
if ($name === 'cache-control') $cacheControl = $value;
|
2026-02-22 02:18:02 -05:00
|
|
|
}
|
2026-02-22 02:43:43 -05:00
|
|
|
}
|
2026-02-22 02:18:02 -05:00
|
|
|
|
2026-02-22 02:43:43 -05:00
|
|
|
if ($contentType !== '') {
|
|
|
|
|
header('Content-Type: ' . $contentType);
|
|
|
|
|
} else {
|
|
|
|
|
header('Content-Type: application/octet-stream');
|
|
|
|
|
}
|
|
|
|
|
if ($contentLength !== '') header('Content-Length: ' . $contentLength);
|
|
|
|
|
if ($acceptRanges !== '') header('Accept-Ranges: ' . $acceptRanges);
|
|
|
|
|
if ($contentRange !== '') header('Content-Range: ' . $contentRange);
|
|
|
|
|
if ($cacheControl !== '') {
|
|
|
|
|
header('Cache-Control: ' . $cacheControl);
|
|
|
|
|
} else {
|
|
|
|
|
header('Cache-Control: no-store');
|
2026-02-22 02:18:02 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-22 02:43:43 -05:00
|
|
|
if ($method !== 'HEAD') {
|
|
|
|
|
echo $body;
|
|
|
|
|
}
|
2026-02-22 02:18:02 -05:00
|
|
|
|