1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
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);
|