. Keeps the script // self-contained — no files/ directory needed. if (isset($_GET['asset'])) { header('Cache-Control: public, max-age=3600'); switch ((string) $_GET['asset']) { case 'index.css': header('Content-Type: text/css; charset=utf-8'); echo phproxy_index_css(); exit; case 'panel.css': header('Content-Type: text/css; charset=utf-8'); echo phproxy_panel_css(); exit; case 'netcheck.js': header('Content-Type: application/javascript; charset=utf-8'); echo phproxy_netcheck_js(); exit; } http_response_code(404); exit; } // --- JSON API DISPATCHER (?api=fetch) ----------------------------------- // POST with a JSON body — proxy makes the HTTP request and returns either // JSON (default) or the raw upstream response. // // Body schema: // { // "url": "https://example.com/" (required), // "method": "GET" | "POST" | ... (default: GET), // "headers": { "Name": "value", ... }, // "cookies": { "name": "value", ... }, // "body": "string", // "timeout": 30, // "follow_redirects": false, // "max_redirects": 5, // "verify_ssl": true, // "return": "json" | "raw" (default: json) // } // Helper used by the new network-check APIs (dns / portcheck / cert). $phproxy_api_validate_host = function (string $host): bool { if ($host === '' || strlen($host) > 253) return false; if (!preg_match('/^[a-zA-Z0-9._:-]+$/', $host)) return false; $block = '#^127\.|192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|^localhost$|^::1$|^0+\.0+\.0+\.0+$#i'; if (preg_match($block, $host)) return false; return true; }; // --- DNS LOOKUP API (?api=dns&host=example.com&type=A) ----------------- if (isset($_GET['api']) && $_GET['api'] === 'dns') { header('Content-Type: application/json; charset=utf-8'); $host = (string) ($_GET['host'] ?? ''); $type = strtoupper((string) ($_GET['type'] ?? 'A')); if (!$phproxy_api_validate_host($host)) { http_response_code(400); echo json_encode(['ok' => false, 'error' => 'Invalid or blacklisted host']); exit; } $types = [ 'A' => DNS_A, 'AAAA' => DNS_AAAA, 'MX' => DNS_MX, 'TXT' => DNS_TXT, 'NS' => DNS_NS, 'SOA' => DNS_SOA, 'CAA' => DNS_CAA, 'CNAME' => DNS_CNAME,'PTR' => DNS_PTR, 'SRV' => DNS_SRV, 'ANY' => DNS_ANY, ]; if (!isset($types[$type])) { http_response_code(400); echo json_encode(['ok' => false, 'error' => 'Unknown record type', 'supported' => array_keys($types)]); exit; } $started = microtime(true); $records = @dns_get_record($host, $types[$type]); $ms = (int) ((microtime(true) - $started) * 1000); if ($records === false) { echo json_encode(['ok' => false, 'host' => $host, 'type' => $type, 'error' => 'Lookup failed', 'duration_ms' => $ms]); exit; } echo json_encode(['ok' => true, 'host' => $host, 'type' => $type, 'records' => $records, 'duration_ms' => $ms], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); exit; } // --- PORT CHECK API (?api=portcheck&host=example.com&port=443) --------- // `port` accepts a single port (443), an inclusive range (80-443) or a // comma-separated mix (22,80,443,8000-8010). Hard cap is 128 ports per // request; default per-port timeout is 1.0s, override with ?timeout=… // (clamped 0.1–5.0). Response always carries a `results` array. if (isset($_GET['api']) && $_GET['api'] === 'portcheck') { header('Content-Type: application/json; charset=utf-8'); $host = (string) ($_GET['host'] ?? ''); $port_spec = (string) ($_GET['port'] ?? ''); $timeout = (float) ($_GET['timeout'] ?? 1.0); if ($timeout < 0.1) $timeout = 0.1; if ($timeout > 5.0) $timeout = 5.0; if (!$phproxy_api_validate_host($host)) { http_response_code(400); echo json_encode(['ok' => false, 'error' => 'Invalid or blacklisted host']); exit; } $ports = phproxy_parse_port_spec($port_spec); if ($ports === false) { http_response_code(400); echo json_encode(['ok' => false, 'error' => 'Invalid port spec. Examples: 443, 80-443, 22,80,443,8000-8010', 'received' => $port_spec]); exit; } if (count($ports) > 128) { http_response_code(400); echo json_encode(['ok' => false, 'error' => 'Too many ports requested (max 128)', 'requested' => count($ports)]); exit; } // Streaming mode: emit NDJSON — one event per line, flushed immediately, // so the UI can show partial progress as each port comes back. Triggered // by ?stream=1. Default remains the batched JSON envelope so existing // CLI consumers don't break. $stream = !empty($_GET['stream']); if ($stream) { while (ob_get_level() > 0) @ob_end_clean(); @ob_implicit_flush(true); header('Content-Type: application/x-ndjson; charset=utf-8'); header('X-Accel-Buffering: no'); // nginx hint header('Cache-Control: no-cache, no-store'); $emit = function (array $obj): void { echo json_encode($obj, JSON_UNESCAPED_SLASHES), "\n"; @flush(); }; $emit([ 'event' => 'start', 'host' => $host, 'port_spec' => $port_spec, 'count' => count($ports), 'ports' => $ports, 'timeout_s' => $timeout, ]); $scan_started = microtime(true); $open = $closed = 0; foreach ($ports as $p) { if (connection_aborted()) break; $t0 = microtime(true); $sock = @stream_socket_client("tcp://$host:$p", $errno, $errstr, $timeout); $ms = (int) ((microtime(true) - $t0) * 1000); if ($sock === false) { $closed++; $emit(['event' => 'result', 'port' => $p, 'reachable' => false, 'error' => $errstr ?: 'connect failed', 'errno' => (int) $errno, 'latency_ms' => $ms]); } else { fclose($sock); $open++; $emit(['event' => 'result', 'port' => $p, 'reachable' => true, 'latency_ms' => $ms]); } } $emit([ 'event' => 'end', 'open' => $open, 'closed' => $closed, 'duration_ms' => (int) ((microtime(true) - $scan_started) * 1000), ]); exit; } // Batched mode (default) — one JSON envelope at the end. $scan_started = microtime(true); $results = []; foreach ($ports as $p) { $t0 = microtime(true); $sock = @stream_socket_client("tcp://$host:$p", $errno, $errstr, $timeout); $ms = (int) ((microtime(true) - $t0) * 1000); if ($sock === false) { $results[] = ['port' => $p, 'reachable' => false, 'error' => $errstr ?: 'connect failed', 'errno' => (int) $errno, 'latency_ms' => $ms]; } else { fclose($sock); $results[] = ['port' => $p, 'reachable' => true, 'latency_ms' => $ms]; } } $duration = (int) ((microtime(true) - $scan_started) * 1000); $open = 0; foreach ($results as $r) if (!empty($r['reachable'])) $open++; echo json_encode([ 'ok' => true, 'host' => $host, 'port_spec' => $port_spec, 'count' => count($results), 'open' => $open, 'closed' => count($results) - $open, 'timeout_s' => $timeout, 'duration_ms' => $duration, 'results' => $results, ], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); exit; } /** * Parse a port spec — single, range, or comma list — into a sorted, * deduped array of ints. Returns false on any syntax error or out-of-range. * "443" → [443] * "80-443" → [80, 81, …, 443] * "22,80,443" → [22, 80, 443] * "80,8000-8010" → [80, 8000, …, 8010] */ function phproxy_parse_port_spec(string $spec): array|false { $spec = trim($spec); if ($spec === '') return false; $set = []; foreach (explode(',', $spec) as $chunk) { $chunk = trim($chunk); if ($chunk === '') continue; if (strpos($chunk, '-') !== false) { $parts = explode('-', $chunk, 2); $a = (int) trim($parts[0]); $b = (int) trim($parts[1]); if ($a < 1 || $a > 65535 || $b < 1 || $b > 65535 || $b < $a) return false; for ($i = $a; $i <= $b; $i++) $set[$i] = true; } else { if (!ctype_digit($chunk)) return false; $p = (int) $chunk; if ($p < 1 || $p > 65535) return false; $set[$p] = true; } if (count($set) > 1024) return false; // hard ceiling to prevent memory abuse; dispatcher rejects >128 with a clearer error } if (empty($set)) return false; ksort($set); return array_keys($set); } // --- SSL CERT INSPECTOR API (?api=cert&host=example.com&port=443) ------ if (isset($_GET['api']) && $_GET['api'] === 'cert') { header('Content-Type: application/json; charset=utf-8'); $host = (string) ($_GET['host'] ?? ''); $port = (int) ($_GET['port'] ?? 443); if (!$phproxy_api_validate_host($host)) { http_response_code(400); echo json_encode(['ok' => false, 'error' => 'Invalid or blacklisted host']); exit; } if ($port < 1 || $port > 65535) { http_response_code(400); echo json_encode(['ok' => false, 'error' => 'Invalid port (1–65535)']); exit; } // First handshake — let OpenSSL negotiate freely so we capture the // server's preferred TLS version + cipher AND the peer cert chain. $ctx = stream_context_create([ 'ssl' => [ 'capture_peer_cert' => true, 'capture_peer_cert_chain' => true, 'verify_peer' => false, 'verify_peer_name' => false, 'SNI_enabled' => true, 'peer_name' => $host, ], ]); $started = microtime(true); $sock = @stream_socket_client("ssl://$host:$port", $errno, $errstr, 10, STREAM_CLIENT_CONNECT, $ctx); $ms = (int) ((microtime(true) - $started) * 1000); if ($sock === false) { echo json_encode(['ok' => false, 'host' => $host, 'port' => $port, 'error' => $errstr ?: 'SSL handshake failed', 'errno' => (int) $errno, 'latency_ms' => $ms]); exit; } $meta_main = stream_get_meta_data($sock); $params = stream_context_get_params($sock); fclose($sock); $leaf = $params['options']['ssl']['peer_certificate'] ?? null; $chain = $params['options']['ssl']['peer_certificate_chain'] ?? []; if (!$leaf) { echo json_encode(['ok' => false, 'host' => $host, 'port' => $port, 'error' => 'No peer certificate received']); exit; } $parse_cert = function ($cert) { $p = @openssl_x509_parse($cert); if (!is_array($p)) return null; // SAN entries — flatten DNS:/IP Address: prefixes for display $san = []; if (!empty($p['extensions']['subjectAltName'])) { foreach (explode(',', $p['extensions']['subjectAltName']) as $entry) { $entry = trim($entry); if (str_starts_with($entry, 'DNS:')) $san[] = substr($entry, 4); elseif (str_starts_with($entry, 'IP Address:')) $san[] = substr($entry, 11); elseif ($entry !== '') $san[] = $entry; } } // Public key — type, size, curve (EC), and the public-key PEM block $key_type = ''; $key_bits = 0; $key_curve = ''; $pub_pem = ''; $pub = @openssl_pkey_get_public($cert); if ($pub !== false) { $details = @openssl_pkey_get_details($pub); if (is_array($details)) { $key_bits = (int) ($details['bits'] ?? 0); $pub_pem = (string) ($details['key'] ?? ''); $type_const = $details['type'] ?? -1; if ($type_const === OPENSSL_KEYTYPE_RSA) $key_type = 'RSA'; elseif ($type_const === OPENSSL_KEYTYPE_DSA) $key_type = 'DSA'; elseif ($type_const === OPENSSL_KEYTYPE_DH) $key_type = 'DH'; elseif (defined('OPENSSL_KEYTYPE_EC') && $type_const === OPENSSL_KEYTYPE_EC) { $key_type = 'EC'; $key_curve = (string) ($details['ec']['curve_name'] ?? ''); } } } // Fingerprints — colon-separated like every cert tool prints them $fmt_fp = function (string $hex): string { $hex = strtoupper($hex); return implode(':', str_split($hex, 2)); }; $fp_sha256 = @openssl_x509_fingerprint($cert, 'sha256') ?: ''; $fp_sha1 = @openssl_x509_fingerprint($cert, 'sha1') ?: ''; $fp_md5 = @openssl_x509_fingerprint($cert, 'md5') ?: ''; // Cert in PEM form (raw, full) $pem = ''; @openssl_x509_export($cert, $pem); $valid_from = $p['validFrom_time_t'] ?? 0; $valid_to = $p['validTo_time_t'] ?? 0; return [ 'version' => (int) ($p['version'] ?? 0) + 1, // X.509 reports 0/1/2; humans say v1/v2/v3 'serial_hex' => $p['serialNumberHex'] ?? '', 'serial_dec' => (string) ($p['serialNumber'] ?? ''), 'subject' => $p['name'] ?? '', 'subject_parts' => $p['subject'] ?? [], // full DN as keyed array (CN, O, OU, C, ST, L, …) 'subject_cn' => $p['subject']['CN'] ?? '', 'subject_org' => $p['subject']['O'] ?? '', 'issuer_dn' => isset($p['issuer']) ? phproxy_dn_join($p['issuer']) : '', 'issuer_parts' => $p['issuer'] ?? [], 'issuer_cn' => $p['issuer']['CN'] ?? '', 'issuer_org' => $p['issuer']['O'] ?? '', 'valid_from' => $valid_from ? gmdate('Y-m-d\TH:i:s\Z', $valid_from) : '', 'valid_to' => $valid_to ? gmdate('Y-m-d\TH:i:s\Z', $valid_to) : '', 'days_left' => $valid_to ? (int) floor(($valid_to - time()) / 86400) : 0, 'san' => $san, 'sig_algorithm' => $p['signatureTypeSN'] ?? '', 'sig_algorithm_long' => $p['signatureTypeLN'] ?? '', 'sig_oid' => isset($p['signatureTypeNID']) ? (string) $p['signatureTypeNID'] : '', 'purposes' => $p['purposes'] ?? [], 'extensions' => $p['extensions'] ?? [], // all extensions, raw text per OpenSSL 'key_type' => $key_type, 'key_bits' => $key_bits, 'key_curve' => $key_curve, 'public_key_pem' => $pub_pem, 'fingerprints' => [ 'sha256' => $fp_sha256 ? $fmt_fp($fp_sha256) : '', 'sha1' => $fp_sha1 ? $fmt_fp($fp_sha1) : '', 'md5' => $fp_md5 ? $fmt_fp($fp_md5) : '', ], 'pem' => $pem, ]; }; $cert_info = $parse_cert($leaf); $chain_info = []; foreach ($chain as $c) { $ci = $parse_cert($c); if ($ci !== null) $chain_info[] = $ci; } // Probe TLS versions individually. For each, force the version via // crypto_method and record which the server actually accepts plus // the cipher it negotiates. ~4 extra ~100-500ms handshakes. $tls_probes = [ 'TLSv1.3' => defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') ? STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT : 0, 'TLSv1.2' => defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT') ? STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT : 0, 'TLSv1.1' => defined('STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT') ? STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT : 0, 'TLSv1.0' => defined('STREAM_CRYPTO_METHOD_TLSv1_CLIENT') ? STREAM_CRYPTO_METHOD_TLSv1_CLIENT : 0, ]; $tls_results = []; foreach ($tls_probes as $label => $method) { if ($method === 0) { $tls_results[] = ['version' => $label, 'supported' => false, 'error' => 'OpenSSL on this PHP build cannot speak ' . $label]; continue; } $pctx = stream_context_create([ 'ssl' => [ 'verify_peer' => false, 'verify_peer_name' => false, 'SNI_enabled' => true, 'peer_name' => $host, 'crypto_method' => $method, ], ]); $pt0 = microtime(true); $psock = @stream_socket_client("ssl://$host:$port", $en, $es, 5, STREAM_CLIENT_CONNECT, $pctx); $pms = (int) ((microtime(true) - $pt0) * 1000); if ($psock === false) { $tls_results[] = ['version' => $label, 'supported' => false, 'error' => $es ?: 'handshake refused', 'latency_ms' => $pms]; continue; } $pmeta = stream_get_meta_data($psock); @fclose($psock); $crypto = $pmeta['crypto'] ?? []; $tls_results[] = [ 'version' => $label, 'supported' => true, 'cipher_name' => (string) ($crypto['cipher_name'] ?? ''), 'cipher_bits' => (int) ($crypto['cipher_bits'] ?? 0), 'cipher_version' => (string) ($crypto['cipher_version'] ?? ''), 'protocol' => (string) ($crypto['protocol'] ?? ''), 'latency_ms' => $pms, ]; } // Negotiated handshake summary (what the server picked when given free choice). $main_crypto = $meta_main['crypto'] ?? []; echo json_encode([ 'ok' => true, 'host' => $host, 'port' => $port, 'negotiated' => [ 'protocol' => (string) ($main_crypto['protocol'] ?? ''), 'cipher_name' => (string) ($main_crypto['cipher_name'] ?? ''), 'cipher_bits' => (int) ($main_crypto['cipher_bits'] ?? 0), 'cipher_version' => (string) ($main_crypto['cipher_version'] ?? ''), ], 'tls_versions' => $tls_results, 'leaf' => $cert_info, 'chain' => $chain_info, 'chain_length' => count($chain_info), 'duration_ms' => $ms, ], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); exit; } // --- IP INFO API (?api=ipinfo[&ip=1.2.3.4]) ------------------------------ // Returns information about the client, the server, and the server's // outgoing IP — plus reverse DNS, the X-Forwarded-For chain, request // headers and host info. Pure local PHP — no external lookups, no // library, no API key. Geo / ASN / city are intentionally not included. if (isset($_GET['api']) && $_GET['api'] === 'ipinfo') { header('Content-Type: application/json; charset=utf-8'); $started = microtime(true); // If the caller passed ?ip=…, that's the single IP we describe. $custom_ip = trim((string) ($_GET['ip'] ?? '')); if ($custom_ip !== '' && !filter_var($custom_ip, FILTER_VALIDATE_IP)) { http_response_code(400); echo json_encode(['ok' => false, 'error' => 'Invalid IP address', 'received' => $custom_ip]); exit; } $remote = (string) ($_SERVER['REMOTE_ADDR'] ?? ''); $xff_raw = (string) ($_SERVER['HTTP_X_FORWARDED_FOR'] ?? ''); $xff_chain = []; if ($xff_raw !== '') { foreach (explode(',', $xff_raw) as $h) { $h = trim($h); if ($h !== '' && filter_var($h, FILTER_VALIDATE_IP)) $xff_chain[] = $h; } } // Prefer the first XFF entry (original client) when behind a reverse proxy. $client_ip = !empty($xff_chain) ? $xff_chain[0] : $remote; // Server's own primary IP via gethostbyname(hostname). Often the // internal Docker IP — useful but not "what the internet sees". $hostname = gethostname() ?: ''; $server_ip = ''; if ($hostname !== '') { $resolved = @gethostbyname($hostname); if ($resolved !== false && $resolved !== $hostname) $server_ip = $resolved; } // Real outgoing IP — what the internet actually sees when this server // makes outbound HTTP calls. UDP "connect" trick: no packets sent, // but the kernel picks the outgoing interface so the local socket // name gives us the right answer. $outgoing_ip = phproxy_outgoing_ip(); $shape = function (string $ip): array { if ($ip === '') return ['ip' => '']; $is_priv = !filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE); return [ 'ip' => $ip, 'family' => str_contains($ip, ':') ? 'IPv6' : 'IPv4', 'is_private' => (bool) $is_priv, 'reverse' => $is_priv ? '' : (@gethostbyaddr($ip) ?: ''), ]; }; if ($custom_ip !== '') { echo json_encode([ 'ok' => true, 'lookup' => $shape($custom_ip), 'duration_ms' => (int) ((microtime(true) - $started) * 1000), ], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); exit; } echo json_encode([ 'ok' => true, 'client' => $shape($client_ip), 'server' => $shape($server_ip), 'outgoing' => $shape($outgoing_ip), 'forwarded' => array_map($shape, $xff_chain), 'request' => [ 'method' => $_SERVER['REQUEST_METHOD'] ?? '', 'protocol' => isset($_SERVER['HTTPS']) || ($_SERVER['SERVER_PORT'] ?? '') == '443' ? 'HTTPS' : 'HTTP', 'remote_addr' => $remote, 'host_header' => $_SERVER['HTTP_HOST'] ?? '', 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'accept_language' => $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '', 'accept' => $_SERVER['HTTP_ACCEPT'] ?? '', 'referer' => $_SERVER['HTTP_REFERER'] ?? '', 'dnt' => $_SERVER['HTTP_DNT'] ?? '', 'sec_gpc' => $_SERVER['HTTP_SEC_GPC'] ?? '', 'sec_ch_ua' => $_SERVER['HTTP_SEC_CH_UA'] ?? '', 'sec_ch_ua_mobile'=> $_SERVER['HTTP_SEC_CH_UA_MOBILE']?? '', 'sec_ch_ua_platform' => $_SERVER['HTTP_SEC_CH_UA_PLATFORM'] ?? '', ], 'host' => [ 'hostname' => $hostname, 'php_version' => PHP_VERSION, 'php_os' => PHP_OS, 'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? '', 'server_port' => $_SERVER['SERVER_PORT'] ?? '', ], 'duration_ms' => (int) ((microtime(true) - $started) * 1000), ], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); exit; } /** * Build a single-line DN string from the keyed array openssl_x509_parse() * returns. Order matches what `openssl x509 -text` prints. */ function phproxy_dn_join(array $parts): string { $order = ['C', 'ST', 'L', 'O', 'OU', 'CN', 'emailAddress', 'serialNumber']; $out = []; foreach ($order as $k) { if (isset($parts[$k]) && $parts[$k] !== '') $out[] = "$k=" . (is_array($parts[$k]) ? implode('+', $parts[$k]) : $parts[$k]); } foreach ($parts as $k => $v) { if (in_array($k, $order, true)) continue; if ($v === '' || $v === null) continue; $out[] = "$k=" . (is_array($v) ? implode('+', $v) : $v); } return implode(', ', $out); } /** * Determine the outgoing IPv4 address this server uses when reaching out * to the wider internet. UDP "connect" trick: no packets are actually * sent, but the kernel picks the outgoing interface so stream_socket_get_name * on the local side returns the right answer. */ function phproxy_outgoing_ip(): string { $sock = @stream_socket_client('udp://1.1.1.1:53', $en, $es, 1); if ($sock === false) { $sock = @stream_socket_client('udp://8.8.8.8:53', $en, $es, 1); } if ($sock === false) return ''; $name = @stream_socket_get_name($sock, false); // local side @fclose($sock); if (!is_string($name) || $name === '') return ''; // "1.2.3.4:54321" or "[::1]:54321" — strip port. if (preg_match('/^\[([^\]]+)\]:\d+$/', $name, $m)) return $m[1]; if (preg_match('/^([0-9.]+):\d+$/', $name, $m)) return $m[1]; return $name; } if (isset($_GET['api']) && $_GET['api'] === 'fetch') { // Helper for any JSON error response in this dispatcher $api_json_err = function (int $status, string $msg, array $extra = []): void { http_response_code($status); header('Content-Type: application/json; charset=utf-8'); echo json_encode(['ok' => false, 'error' => $msg] + $extra, JSON_UNESCAPED_SLASHES); exit; }; if ($_SERVER['REQUEST_METHOD'] !== 'POST') $api_json_err(405, 'POST required'); $raw_in = file_get_contents('php://input') ?: ''; $req_in = json_decode($raw_in, true); if (!is_array($req_in) || empty($req_in['url'])) $api_json_err(400, 'Invalid JSON body or missing "url"'); // SSRF guard — refuse the same blacklisted host ranges the browser flow // blocks (loopback / RFC1918). Replicate the regex here to fail fast. $api_host_block = '#^127\.|192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|localhost$|^::1$#i'; $api_url_host = (string) (@parse_url((string) $req_in['url'], PHP_URL_HOST) ?: ''); if ($api_url_host === '' || preg_match($api_host_block, $api_url_host)) { $api_json_err(400, 'Target host blacklisted or unparseable', ['host' => $api_url_host]); } $api_result = phproxy_api_fetch($req_in); if (($req_in['return'] ?? 'json') === 'raw') { // Emit the upstream response verbatim. Drop hop-by-hop and encoding // headers we've already decoded for the client. if (isset($api_result['status'])) http_response_code($api_result['status']); $first_ct = true; foreach (($api_result['headers'] ?? []) as $hname => $hvalue) { $lhname = strtolower($hname); if (in_array($lhname, ['transfer-encoding', 'content-encoding', 'content-length', 'connection'], true)) continue; // For Content-Type, replace any previous (default Apache sets one); // for everything else, append. $replace = ($lhname === 'content-type' && $first_ct); header($hname . ': ' . $hvalue, $replace); if ($lhname === 'content-type') $first_ct = false; } echo $api_result['body_raw'] ?? ''; exit; } // JSON envelope. UTF-8 bodies go in directly, binary gets base64'd. header('Content-Type: application/json; charset=utf-8'); $body_raw = $api_result['body_raw'] ?? ''; if ($body_raw !== '' && mb_check_encoding($body_raw, 'UTF-8')) { $api_result['body'] = $body_raw; $api_result['body_encoding'] = 'utf8'; } else { $api_result['body'] = base64_encode($body_raw); $api_result['body_encoding'] = 'base64'; } unset($api_result['body_raw']); echo json_encode($api_result, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); exit; } // // CONFIGURABLE OPTIONS // $_config = [ 'url_var_name' => '_proxurl', 'flags_var_name' => '_proxfl', 'get_form_name' => '_proxgfn', 'basic_auth_var_name' => '_proxba', 'site_name' => 'PHProxy', 'max_file_size' => -1, 'allow_hotlinking' => 0, 'upon_hotlink' => 1, 'compress_output' => 0, ]; // NOTE on order: new flags MUST be appended to the head of $_flags so the // bitfield positions of existing flags stay stable (the cookie stores the // flag bitfield as a left-padded binary string — adding to the tail would // shift the existing flags and break every saved cookie). $_flags = [ // new in v1.3.x — anonymity seed URL encryption (on by default) 'encrypt_url' => 1, // new in v1.3.0 (privacy / blocking) 'strip_tracking' => 0, 'send_gpc' => 0, 'send_dnt' => 0, 'block_media' => 0, 'block_fonts' => 0, 'block_3p' => 0, 'strip_iframes' => 0, // original flags (positions preserved) 'include_form' => 1, 'remove_scripts' => 1, 'accept_cookies' => 1, 'show_images' => 1, 'show_referer' => 1, 'rotate13' => 0, 'base64_encode' => 1, 'strip_meta' => 0, 'strip_title' => 1, 'session_cookies' => 1, ]; $_frozen_flags = [ 'encrypt_url' => 0, 'strip_tracking' => 0, 'send_gpc' => 0, 'send_dnt' => 0, 'block_media' => 0, 'block_fonts' => 0, 'block_3p' => 0, 'strip_iframes' => 0, 'include_form' => 0, 'remove_scripts' => 0, 'accept_cookies' => 0, 'show_images' => 0, 'show_referer' => 0, 'rotate13' => 0, 'base64_encode' => 0, 'strip_meta' => 0, 'strip_title' => 0, 'session_cookies' => 0, ]; $_labels = [ 'encrypt_url' => ['Encrypted (rotating key)', 'AES-CTR encrypt URLs with a 1-hour session seed; old logs go unusable'], 'strip_tracking' => ['Strip tracking params', 'Drop utm_*, fbclid, gclid and friends from URLs'], 'send_gpc' => ['Send Sec-GPC: 1', 'Global Privacy Control signal'], 'send_dnt' => ['Send DNT: 1', 'Do-Not-Track header'], 'block_media' => ['Block media', 'Remove