<?php

/*
 * Chat Grid media proxy (compat-focused).
 *
 * Usage:
 *   /chgrid/media_proxy.php?url=<urlencoded-remote-url>
 *
 * 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, $allowedOrigin)
{
    $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',
            'Origin: ' . $allowedOrigin,
        )
    );

    $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, $allowedOrigin);

$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);
