diff options
| author | sillylaird <sillyfanboy@gmail.com> | 2026-09-03 00:33:59 +0000 |
|---|---|---|
| committer | sillylaird <sillyfanboy@gmail.com> | 2026-09-03 00:33:59 +0000 |
| commit | 898b52edcb47bcb3e9d6106e74ca73e74ea01e70 (patch) | |
| tree | 85c6ee5ad58b860144551184d4cf86b560c62b91 /gemini-proxy.php | |
| download | www-main.tar.gz www-main.zip | |
Diffstat (limited to '')
| -rw-r--r-- | gemini-proxy.php | 123 |
1 files changed, 123 insertions, 0 deletions
diff --git a/gemini-proxy.php b/gemini-proxy.php new file mode 100644 index 0000000..6125c73 --- /dev/null +++ b/gemini-proxy.php @@ -0,0 +1,123 @@ +<?php +/* gemini-proxy.php — Gemini protocol proxy for wired.os + Fetches a gemini:// URL and returns JSON { status, meta, body, url } */ + +require_once __DIR__ . '/partials/proxy_helpers.php'; + +header('Content-Type: application/json; charset=utf-8'); +header('Access-Control-Allow-Origin: *'); + +if (!proxy_rate_limit('gemini', 30, 60)) { + http_response_code(429); + echo json_encode(['error' => 'rate limited']); + exit; +} + +$url = trim($_GET['url'] ?? ''); + +if (!$url) { + http_response_code(400); + echo json_encode(['error' => 'url required']); + exit; +} + +if (!preg_match('#^gemini://#i', $url)) { + http_response_code(400); + echo json_encode(['error' => 'gemini:// URLs only']); + exit; +} + +if (strlen($url) > 1024) { + http_response_code(400); + echo json_encode(['error' => 'URL too long (max 1024)']); + exit; +} + +$parsed = parse_url($url); +$host = $parsed['host'] ?? ''; +$port = isset($parsed['port']) ? (int)$parsed['port'] : 1965; + +if (!$host) { + http_response_code(400); + echo json_encode(['error' => 'invalid host']); + exit; +} + +if ($port < 1 || $port > 65535) { + http_response_code(400); + echo json_encode(['error' => 'invalid port']); + exit; +} + +// SSRF guard: reject hosts that resolve to loopback / RFC1918 / link-local / +// reserved ranges, so this endpoint can't be used to probe the internal network. +if (!proxy_host_is_safe($host)) { + http_response_code(400); + echo json_encode(['error' => 'host not allowed']); + exit; +} + +$ctx = stream_context_create([ + 'ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'allow_self_signed' => true, + ], +]); + +$socket = @stream_socket_client( + "ssl://{$host}:{$port}", + $errno, $errstr, 10, + STREAM_CLIENT_CONNECT, + $ctx +); + +if (!$socket) { + http_response_code(502); + echo json_encode(['error' => "connect failed ({$errno}): {$errstr}"]); + exit; +} + +stream_set_timeout($socket, 10); +fwrite($socket, $url . "\r\n"); + +$raw = ''; +$cap = 4 * 1024 * 1024; +while (!feof($socket)) { + $chunk = fread($socket, 8192); + if ($chunk === false) break; + $raw .= $chunk; + if (strlen($raw) >= $cap) { $raw = substr($raw, 0, $cap); break; } +} +fclose($socket); + +$eol = strpos($raw, "\r\n"); +if ($eol === false) { + http_response_code(502); + echo json_encode(['error' => 'malformed response (no CRLF)']); + exit; +} + +$header = substr($raw, 0, $eol); +$body = substr($raw, $eol + 2); + +if (!preg_match('#^(\d{2})\s*(.*)$#s', $header, $m)) { + http_response_code(502); + echo json_encode(['error' => 'malformed header line']); + exit; +} + +$status = (int)$m[1]; +$meta = trim($m[2]); + +// Ensure body is valid UTF-8 for JSON encoding +if (!mb_check_encoding($body, 'UTF-8')) { + $body = mb_convert_encoding($body, 'UTF-8', 'ISO-8859-1'); +} + +echo json_encode([ + 'status' => $status, + 'meta' => $meta, + 'body' => $body, + 'url' => $url, +], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); |
