* * Goals: * - Works on older cPanel PHP web handlers. * - Supports http/https upstreams. * - Provides same-origin endpoint for browser playback. */ $GLOBALS['CHGRID_PROXY_HEADERS_SENT'] = false; $GLOBALS['CHGRID_PROXY_STATUS'] = 200; $GLOBALS['CHGRID_PROXY_UP_HEADERS'] = array(); function proxy_emit_downstream_headers() { if (!empty($GLOBALS['CHGRID_PROXY_HEADERS_SENT'])) { return; } $status = isset($GLOBALS['CHGRID_PROXY_STATUS']) ? (int) $GLOBALS['CHGRID_PROXY_STATUS'] : 200; if ($status < 100 || $status > 599) { $status = 200; } set_status($status); $h = isset($GLOBALS['CHGRID_PROXY_UP_HEADERS']) && is_array($GLOBALS['CHGRID_PROXY_UP_HEADERS']) ? $GLOBALS['CHGRID_PROXY_UP_HEADERS'] : array(); if (isset($h['content-type']) && $h['content-type'] !== '') { header('Content-Type: ' . $h['content-type']); } else { header('Content-Type: application/octet-stream'); } if (isset($h['content-length']) && $h['content-length'] !== '') { header('Content-Length: ' . $h['content-length']); } if (isset($h['accept-ranges']) && $h['accept-ranges'] !== '') { header('Accept-Ranges: ' . $h['accept-ranges']); } if (isset($h['content-range']) && $h['content-range'] !== '') { header('Content-Range: ' . $h['content-range']); } if (isset($h['cache-control']) && $h['cache-control'] !== '') { header('Cache-Control: ' . $h['cache-control']); } else { header('Cache-Control: no-store'); } $GLOBALS['CHGRID_PROXY_HEADERS_SENT'] = true; } function proxy_header_callback($ch, $line) { $trimmed = trim((string) $line); $len = strlen($line); if ($trimmed === '') { return $len; } if (stripos($trimmed, 'HTTP/') === 0) { $parts = explode(' ', $trimmed); if (isset($parts[1]) && ctype_digit($parts[1])) { $GLOBALS['CHGRID_PROXY_STATUS'] = (int) $parts[1]; } // New response block (redirect hop). Keep only final hop headers. $GLOBALS['CHGRID_PROXY_UP_HEADERS'] = array(); return $len; } $split = explode(':', $trimmed, 2); if (count($split) !== 2) { return $len; } $name = strtolower(trim($split[0])); $value = trim($split[1]); $GLOBALS['CHGRID_PROXY_UP_HEADERS'][$name] = $value; return $len; } function proxy_write_callback($ch, $chunk) { proxy_emit_downstream_headers(); $len = strlen($chunk); if ($len > 0) { echo $chunk; flush(); } return $len; } 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; } function normalize_origin($value) { $value = trim((string) $value); if ($value === '') { return ''; } $parts = parse_url($value); if ($parts === false || !isset($parts['scheme']) || !isset($parts['host'])) { return ''; } $scheme = strtolower((string) $parts['scheme']); if ($scheme !== 'http' && $scheme !== 'https') { return ''; } if (isset($parts['user']) || isset($parts['pass']) || isset($parts['query']) || isset($parts['fragment'])) { return ''; } $path = isset($parts['path']) ? (string) $parts['path'] : ''; if ($path !== '' && $path !== '/') { return ''; } $host = strtolower((string) $parts['host']); if ($host === '') { return ''; } if (strpos($host, ':') !== false && substr($host, 0, 1) !== '[') { $host = '[' . $host . ']'; } $port = isset($parts['port']) ? ':' . (int) $parts['port'] : ''; return $scheme . '://' . $host . $port; } function load_proxy_host_origin() { $fromEnv = normalize_origin(getenv('CHGRID_HOST_ORIGIN')); if ($fromEnv !== '') { return $fromEnv; } $configPath = __DIR__ . '/media_proxy.config.php'; if (!is_file($configPath)) { return ''; } $config = require $configPath; if (!is_array($config) || !isset($config['host_origin'])) { return ''; } return normalize_origin($config['host_origin']); } function load_proxy_session_check_url() { $fromEnv = trim((string) getenv('CHGRID_MEDIA_PROXY_SESSION_CHECK_URL')); if ($fromEnv !== '') { return $fromEnv; } $configPath = __DIR__ . '/media_proxy.config.php'; if (!is_file($configPath)) { return 'http://127.0.0.1:8765/auth/session/check'; } $config = require $configPath; if (!is_array($config) || !isset($config['session_check_url'])) { return 'http://127.0.0.1:8765/auth/session/check'; } $value = trim((string) $config['session_check_url']); if ($value === '') { return 'http://127.0.0.1:8765/auth/session/check'; } return $value; } function require_valid_proxy_session($sessionCheckUrl) { $cookieHeader = isset($_SERVER['HTTP_COOKIE']) ? trim((string) $_SERVER['HTTP_COOKIE']) : ''; if ($cookieHeader === '') { send_text(401, 'session required'); } if (!function_exists('curl_init')) { send_text(500, 'curl extension is required'); } $ch = curl_init(); if (!$ch) { send_text(500, 'proxy init failed'); } curl_setopt($ch, CURLOPT_URL, $sessionCheckUrl); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); curl_setopt($ch, CURLOPT_MAXREDIRS, 0); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); curl_setopt($ch, CURLOPT_TIMEOUT, 10); 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, array( 'Cookie: ' . $cookieHeader, 'X-Chgrid-Auth-Client: 1', ) ); $response = curl_exec($ch); if ($response === false) { $error = curl_error($ch); curl_close($ch); send_text(502, 'session check failed: ' . $error); } $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); if ($status !== 204) { send_text(401, 'session required'); } } function host_matches_suffix($host, $suffix) { if ($suffix === '') { return false; } if ($host === $suffix) { return true; } $needle = '.' . $suffix; if (strlen($host) < strlen($needle)) { return false; } return substr($host, -strlen($needle)) === $needle; } function parse_allowlist_suffixes($allowlistEnv) { $suffixes = array(); if ($allowlistEnv === false || trim((string) $allowlistEnv) === '') { return $suffixes; } $parts = explode(',', (string) $allowlistEnv); foreach ($parts as $part) { $suffix = strtolower(trim((string) $part)); if ($suffix !== '') { $suffixes[] = $suffix; } } return $suffixes; } function resolve_host_ips($host) { $ips = array(); $ipv4 = @gethostbynamel($host); if (is_array($ipv4)) { foreach ($ipv4 as $ip) { $ip = trim((string) $ip); if ($ip !== '') { $ips[$ip] = true; } } } if (function_exists('dns_get_record')) { $aaaa = @dns_get_record($host, DNS_AAAA); if (is_array($aaaa)) { foreach ($aaaa as $record) { if (!isset($record['ipv6'])) { continue; } $ip = trim((string) $record['ipv6']); if ($ip !== '') { $ips[$ip] = true; } } } } return array_keys($ips); } function validate_public_ip($ip) { return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false; } function validate_target_url($url, $allowlistSuffixes, &$error) { $error = ''; $parsed = parse_url($url); if ($parsed === false || !isset($parsed['scheme']) || !isset($parsed['host'])) { $error = 'invalid url'; return false; } $scheme = strtolower((string) $parsed['scheme']); if ($scheme !== 'http' && $scheme !== 'https') { $error = 'unsupported scheme'; return false; } $host = strtolower((string) $parsed['host']); if ($host === 'localhost' || $host === '127.0.0.1' || $host === '::1') { $error = 'forbidden host'; return false; } if (!empty($allowlistSuffixes)) { $allowed = false; foreach ($allowlistSuffixes as $suffix) { if (host_matches_suffix($host, $suffix)) { $allowed = true; break; } } if (!$allowed) { $error = 'host not allowed'; return false; } } $resolved = array(); if (filter_var($host, FILTER_VALIDATE_IP)) { $resolved[] = $host; } else { $resolved = resolve_host_ips($host); } if (count($resolved) === 0) { $error = 'dns resolution failed'; return false; } foreach ($resolved as $ip) { if (!validate_public_ip($ip)) { $error = 'resolved ip not allowed'; return false; } } return true; } function resolve_redirect_url($baseUrl, $location) { $location = trim((string) $location); if ($location === '') { return ''; } $target = parse_url($location); if ($target !== false && isset($target['scheme']) && isset($target['host'])) { return $location; } $base = parse_url($baseUrl); if ($base === false || !isset($base['scheme']) || !isset($base['host'])) { return ''; } $scheme = strtolower((string) $base['scheme']); $host = (string) $base['host']; $port = isset($base['port']) ? ':' . (int) $base['port'] : ''; if (strpos($location, '//') === 0) { return $scheme . ':' . $location; } $path = isset($base['path']) ? (string) $base['path'] : '/'; if ($path === '') { $path = '/'; } if ($location[0] === '/') { return $scheme . '://' . $host . $port . $location; } $dir = preg_replace('#/[^/]*$#', '/', $path); if ($dir === null || $dir === '') { $dir = '/'; } return $scheme . '://' . $host . $port . $dir . $location; } function normalize_dropbox_url($url) { $parts = parse_url($url); if ($parts === false || !isset($parts['host'])) { return $url; } $host = strtolower((string) $parts['host']); if (!host_matches_suffix($host, 'dropbox.com')) { return $url; } $query = array(); if (isset($parts['query']) && $parts['query'] !== '') { parse_str((string) $parts['query'], $query); } // Dropbox direct media playback is most reliable with raw=1. $query['raw'] = '1'; unset($query['dl']); $scheme = isset($parts['scheme']) ? (string) $parts['scheme'] : 'https'; $user = isset($parts['user']) ? (string) $parts['user'] : ''; $pass = isset($parts['pass']) ? (string) $parts['pass'] : ''; $auth = ''; if ($user !== '') { $auth = $user; if ($pass !== '') { $auth .= ':' . $pass; } $auth .= '@'; } $hostPort = (string) $parts['host']; if (isset($parts['port'])) { $hostPort .= ':' . (int) $parts['port']; } $path = isset($parts['path']) ? (string) $parts['path'] : '/'; $fragment = isset($parts['fragment']) && $parts['fragment'] !== '' ? '#' . (string) $parts['fragment'] : ''; $queryString = http_build_query($query); if ($queryString !== '') { $queryString = '?' . $queryString; } return $scheme . '://' . $auth . $hostPort . $path . $queryString . $fragment; } function resolve_safe_redirect_chain($initialUrl, $allowlistSuffixes, $requestHeaders, $maxRedirects, &$error) { $error = ''; $currentUrl = $initialUrl; for ($hop = 0; $hop <= $maxRedirects; $hop += 1) { if (!validate_target_url($currentUrl, $allowlistSuffixes, $error)) { return ''; } $GLOBALS['CHGRID_PROXY_STATUS'] = 200; $GLOBALS['CHGRID_PROXY_UP_HEADERS'] = array(); $GLOBALS['CHGRID_PROXY_HEADERS_SENT'] = false; $ch = curl_init(); if (!$ch) { $error = 'proxy init failed'; return ''; } curl_setopt($ch, CURLOPT_URL, $currentUrl); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); curl_setopt($ch, CURLOPT_MAXREDIRS, 0); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); curl_setopt($ch, CURLOPT_TIMEOUT, 20); curl_setopt($ch, CURLOPT_HEADER, false); 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); curl_setopt($ch, CURLOPT_HEADERFUNCTION, 'proxy_header_callback'); curl_setopt($ch, CURLOPT_NOBODY, true); $ok = curl_exec($ch); if ($ok === false) { $error = 'upstream fetch failed: ' . curl_error($ch); curl_close($ch); return ''; } curl_close($ch); $status = isset($GLOBALS['CHGRID_PROXY_STATUS']) ? (int) $GLOBALS['CHGRID_PROXY_STATUS'] : 200; if ($status < 300 || $status >= 400) { return $currentUrl; } if ($hop >= $maxRedirects) { $error = 'too many redirects'; return ''; } $headers = isset($GLOBALS['CHGRID_PROXY_UP_HEADERS']) ? $GLOBALS['CHGRID_PROXY_UP_HEADERS'] : array(); $location = isset($headers['location']) ? (string) $headers['location'] : ''; $nextUrl = resolve_redirect_url($currentUrl, $location); if ($nextUrl === '') { $error = 'redirect location missing or invalid'; return ''; } $currentUrl = $nextUrl; } $error = 'too many redirects'; return ''; } // Optional allowlist env var: CHGRID_MEDIA_PROXY_ALLOWLIST=dropbox.com,example.com $allowlistEnv = getenv('CHGRID_MEDIA_PROXY_ALLOWLIST'); $allowlistSuffixes = parse_allowlist_suffixes($allowlistEnv); $allowedOrigin = load_proxy_host_origin(); $sessionCheckUrl = load_proxy_session_check_url(); if ($allowedOrigin === '') { send_text(500, 'CHGRID_HOST_ORIGIN is required'); } $requestOrigin = normalize_origin(isset($_SERVER['HTTP_ORIGIN']) ? (string) $_SERVER['HTTP_ORIGIN'] : ''); if ($requestOrigin !== '' && $requestOrigin !== $allowedOrigin) { send_text(403, 'origin not allowed'); } header('Access-Control-Allow-Origin: ' . $allowedOrigin); header('Vary: Origin'); header('Access-Control-Allow-Methods: GET, HEAD, OPTIONS'); header('Access-Control-Allow-Headers: Range'); $method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET'; if ($method === 'OPTIONS') { set_status(204); exit; } if ($method !== 'GET' && $method !== 'HEAD') { send_text(405, 'method not allowed'); } require_valid_proxy_session($sessionCheckUrl); $rawUrl = isset($_GET['url']) ? trim((string) $_GET['url']) : ''; if ($rawUrl === '') { send_text(400, 'missing url query param'); } $rawUrl = normalize_dropbox_url($rawUrl); if (!function_exists('curl_init')) { send_text(500, 'curl extension is required'); } $ch = curl_init(); if (!$ch) { send_text(500, 'proxy init failed'); } $requestHeaders = array('Accept: */*', 'Connection: keep-alive'); $range = isset($_SERVER['HTTP_RANGE']) ? trim((string) $_SERVER['HTTP_RANGE']) : ''; if ($range !== '') { $requestHeaders[] = 'Range: ' . $range; } $resolveError = ''; $finalUrl = resolve_safe_redirect_chain($rawUrl, $allowlistSuffixes, $requestHeaders, 5, $resolveError); if ($finalUrl === '') { if (strpos($resolveError, 'invalid url') === 0 || strpos($resolveError, 'unsupported scheme') === 0) { send_text(400, $resolveError); } if ( strpos($resolveError, 'forbidden host') === 0 || strpos($resolveError, 'host not allowed') === 0 || strpos($resolveError, 'resolved ip not allowed') === 0 ) { send_text(403, $resolveError); } if (strpos($resolveError, 'proxy init failed') === 0) { send_text(500, $resolveError); } send_text(502, $resolveError !== '' ? $resolveError : 'redirect resolution failed'); } curl_setopt($ch, CURLOPT_URL, $finalUrl); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); curl_setopt($ch, CURLOPT_MAXREDIRS, 0); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); curl_setopt($ch, CURLOPT_TIMEOUT, 0); curl_setopt($ch, CURLOPT_HEADER, false); curl_setopt($ch, CURLOPT_RETURNTRANSFER, false); curl_setopt($ch, CURLOPT_NOSIGNAL, true); curl_setopt($ch, CURLOPT_USERAGENT, 'ChatGridMediaProxy/1.0'); curl_setopt($ch, CURLOPT_HTTPHEADER, $requestHeaders); curl_setopt($ch, CURLOPT_HEADERFUNCTION, 'proxy_header_callback'); curl_setopt($ch, CURLOPT_WRITEFUNCTION, 'proxy_write_callback'); if ($method === 'HEAD') { curl_setopt($ch, CURLOPT_NOBODY, true); } $ok = curl_exec($ch); if ($ok === false) { $err = curl_error($ch); curl_close($ch); send_text(502, 'upstream fetch failed: ' . $err); } if ($method === 'HEAD') { proxy_emit_downstream_headers(); } curl_close($ch);