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 03:09:17 -05:00
|
|
|
$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;
|
|
|
|
|
}
|
|
|
|
|
|
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-03-08 20:51:50 -04:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-08 21:26:07 -04:00
|
|
|
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']);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-08 21:44:27 -04:00
|
|
|
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');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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 21:07:01 -05:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 20:04:23 -05:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 21:07:01 -05:00
|
|
|
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 '';
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-08 20:51:50 -04:00
|
|
|
// Optional allowlist env var: CHGRID_MEDIA_PROXY_ALLOWLIST=dropbox.com,example.com
|
|
|
|
|
$allowlistEnv = getenv('CHGRID_MEDIA_PROXY_ALLOWLIST');
|
|
|
|
|
$allowlistSuffixes = parse_allowlist_suffixes($allowlistEnv);
|
2026-03-08 21:26:07 -04:00
|
|
|
$allowedOrigin = load_proxy_host_origin();
|
2026-03-08 21:44:27 -04:00
|
|
|
$sessionCheckUrl = load_proxy_session_check_url();
|
2026-03-08 20:51:50 -04:00
|
|
|
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');
|
2026-02-22 02:43:43 -05:00
|
|
|
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
|
|
|
|
2026-03-08 21:44:27 -04:00
|
|
|
require_valid_proxy_session($sessionCheckUrl);
|
|
|
|
|
|
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
|
|
|
}
|
2026-02-24 20:04:23 -05:00
|
|
|
$rawUrl = normalize_dropbox_url($rawUrl);
|
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 21:07:01 -05:00
|
|
|
$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);
|
2026-02-22 02:43:43 -05:00
|
|
|
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
|
2026-02-22 03:09:17 -05:00
|
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 0);
|
|
|
|
|
curl_setopt($ch, CURLOPT_HEADER, false);
|
|
|
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
|
2026-02-22 02:43:43 -05:00
|
|
|
curl_setopt($ch, CURLOPT_NOSIGNAL, true);
|
|
|
|
|
curl_setopt($ch, CURLOPT_USERAGENT, 'ChatGridMediaProxy/1.0');
|
|
|
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, $requestHeaders);
|
2026-02-22 03:09:17 -05:00
|
|
|
curl_setopt($ch, CURLOPT_HEADERFUNCTION, 'proxy_header_callback');
|
|
|
|
|
curl_setopt($ch, CURLOPT_WRITEFUNCTION, 'proxy_write_callback');
|
2026-02-22 02:43:43 -05:00
|
|
|
if ($method === 'HEAD') {
|
|
|
|
|
curl_setopt($ch, CURLOPT_NOBODY, true);
|
|
|
|
|
}
|
2026-02-22 02:18:02 -05:00
|
|
|
|
2026-02-22 03:09:17 -05:00
|
|
|
$ok = curl_exec($ch);
|
|
|
|
|
if ($ok === false) {
|
2026-02-22 02:43:43 -05:00
|
|
|
$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 03:09:17 -05:00
|
|
|
if ($method === 'HEAD') {
|
|
|
|
|
proxy_emit_downstream_headers();
|
2026-02-22 02:18:02 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-22 03:09:17 -05:00
|
|
|
curl_close($ch);
|