AstraOS commited on
Commit
e8ccf6f
·
verified ·
1 Parent(s): 58381cb

Create service-worker-proxy_(old)v1.ts

Browse files
Files changed (1) hide show
  1. service-worker-proxy_(old)v1.ts +224 -0
service-worker-proxy_(old)v1.ts ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const CONFIG = {
2
+ TELEGRAM_API_BASE: "https://api.telegram.org",
3
+ PROXY_PATH_PREFIX: "/telegram-api",
4
+
5
+ // Retry logic
6
+ MAX_RETRIES: 3,
7
+ RETRY_DELAY_MS: 300,
8
+ RETRYABLE_STATUS_CODES: [429, 500, 502, 503, 504],
9
+
10
+ // Request timeout
11
+ REQUEST_TIMEOUT_MS: 30_000,
12
+
13
+ // Security: only allow these Telegram API methods through
14
+ // Leave empty to allow all methods
15
+ ALLOWED_METHODS: [] as string[],
16
+
17
+ // Headers to strip from incoming requests
18
+ HEADERS_TO_STRIP: [
19
+ "cf-ray", "cf-connecting-ip", "x-forwarded-for",
20
+ "x-real-ip", "cf-ipcountry", "cf-visitor",
21
+ "cf-worker", "cdn-loop", "host",
22
+ ],
23
+ };
24
+
25
+ interface ProxyResult {
26
+ response: Response;
27
+ retries: number;
28
+ durationMs: number;
29
+ }
30
+
31
+ function getClientIP(request: Request): string {
32
+ return (
33
+ request.headers.get("cf-connecting-ip") ||
34
+ request.headers.get("x-forwarded-for")?.split(",")[0].trim() ||
35
+ request.headers.get("x-real-ip") ||
36
+ "unknown"
37
+ );
38
+ }
39
+
40
+ function sleep(ms: number): Promise<void> {
41
+ return new Promise((resolve) => setTimeout(resolve, ms));
42
+ }
43
+
44
+ function extractBotMethod(pathname: string): string | null {
45
+ const match = pathname.match(/^\/bot[^/]+\/([^/?]+)/);
46
+ return match ? match[1] : null;
47
+ }
48
+
49
+ function sanitizeHeaders(headers: Headers): Headers {
50
+ const cleaned = new Headers(headers);
51
+ for (const header of CONFIG.HEADERS_TO_STRIP) {
52
+ cleaned.delete(header);
53
+ }
54
+ cleaned.set("host", "api.telegram.org");
55
+ return cleaned;
56
+ }
57
+
58
+ function corsHeaders(): Record<string, string> {
59
+ return {
60
+ "Access-Control-Allow-Origin": "*",
61
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
62
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
63
+ "Access-Control-Max-Age": "86400",
64
+ };
65
+ }
66
+
67
+ function errorResponse(
68
+ status: number,
69
+ code: string,
70
+ message: string,
71
+ extra?: Record<string, unknown>
72
+ ): Response {
73
+ return new Response(
74
+ JSON.stringify({ ok: false, error_code: status, code, description: message, ...extra }),
75
+ {
76
+ status,
77
+ headers: { "Content-Type": "application/json", ...corsHeaders() },
78
+ }
79
+ );
80
+ }
81
+
82
+ async function proxyToTelegram(
83
+ telegramUrl: string,
84
+ request: Request
85
+ ): Promise<ProxyResult> {
86
+ const startTime = Date.now();
87
+ let lastError: Error | null = null;
88
+ let retries = 0;
89
+
90
+ const cleanedHeaders = sanitizeHeaders(request.headers);
91
+ const bodyBuffer = request.body ? await request.arrayBuffer() : null;
92
+
93
+ for (let attempt = 0; attempt <= CONFIG.MAX_RETRIES; attempt++) {
94
+ if (attempt > 0) {
95
+ const delay = CONFIG.RETRY_DELAY_MS * Math.pow(2, attempt - 1);
96
+ await sleep(delay);
97
+ retries++;
98
+ }
99
+
100
+ try {
101
+ const controller = new AbortController();
102
+ const timeoutId = setTimeout(
103
+ () => controller.abort(),
104
+ CONFIG.REQUEST_TIMEOUT_MS
105
+ );
106
+
107
+ const upstreamRequest = new Request(telegramUrl, {
108
+ method: request.method,
109
+ headers: cleanedHeaders,
110
+ body: bodyBuffer,
111
+ redirect: "follow",
112
+ signal: controller.signal,
113
+ });
114
+
115
+ const response = await fetch(upstreamRequest);
116
+ clearTimeout(timeoutId);
117
+
118
+ if (
119
+ response.ok ||
120
+ (response.status >= 400 &&
121
+ response.status < 500 &&
122
+ response.status !== 429)
123
+ ) {
124
+ return { response, retries, durationMs: Date.now() - startTime };
125
+ }
126
+
127
+ if (CONFIG.RETRYABLE_STATUS_CODES.includes(response.status)) {
128
+ if (response.status === 429) {
129
+ const retryAfter = response.headers.get("Retry-After");
130
+ if (retryAfter) {
131
+ await sleep(parseInt(retryAfter) * 1000);
132
+ }
133
+ }
134
+ lastError = new Error(`Upstream returned ${response.status}`);
135
+ continue;
136
+ }
137
+
138
+ return { response, retries, durationMs: Date.now() - startTime };
139
+
140
+ } catch (err) {
141
+ lastError = err instanceof Error ? err : new Error(String(err));
142
+ if (lastError.name === "AbortError") {
143
+ throw new Error(`Request timed out after ${CONFIG.REQUEST_TIMEOUT_MS}ms`);
144
+ }
145
+ }
146
+ }
147
+
148
+ throw lastError ?? new Error("Unknown proxy error after retries");
149
+ }
150
+
151
+ export default {
152
+ async fetch(request: Request): Promise<Response> {
153
+ const startTime = Date.now();
154
+ const originalUrl = new URL(request.url);
155
+ const clientIP = getClientIP(request);
156
+ const requestId = crypto.randomUUID();
157
+
158
+ if (request.method === "OPTIONS") {
159
+ return new Response(null, { status: 204, headers: corsHeaders() });
160
+ }
161
+
162
+ if (originalUrl.pathname === "/health") {
163
+ return new Response(
164
+ JSON.stringify({ ok: true, timestamp: new Date().toISOString() }),
165
+ { headers: { "Content-Type": "application/json", ...corsHeaders() } }
166
+ );
167
+ }
168
+
169
+ if (!originalUrl.pathname.startsWith(CONFIG.PROXY_PATH_PREFIX)) {
170
+ return new Response("Telegram API Proxy - send requests to /telegram-api/bot{TOKEN}/{METHOD}", {
171
+ status: 200,
172
+ headers: { "Content-Type": "text/plain", ...corsHeaders() },
173
+ });
174
+ }
175
+
176
+ const telegramPath = originalUrl.pathname.replace(CONFIG.PROXY_PATH_PREFIX, "");
177
+ const botMethod = extractBotMethod(telegramPath);
178
+
179
+ if (
180
+ CONFIG.ALLOWED_METHODS.length > 0 &&
181
+ (!botMethod || !CONFIG.ALLOWED_METHODS.includes(botMethod))
182
+ ) {
183
+ return errorResponse(403, "METHOD_NOT_ALLOWED", `Method '${botMethod}' is not permitted`);
184
+ }
185
+
186
+ const telegramApiUrl = `${CONFIG.TELEGRAM_API_BASE}${telegramPath}${originalUrl.search}`;
187
+
188
+ try {
189
+ const { response, retries, durationMs } = await proxyToTelegram(telegramApiUrl, request);
190
+
191
+ return new Response(response.body, {
192
+ status: response.status,
193
+ statusText: response.statusText,
194
+ headers: {
195
+ ...Object.fromEntries(response.headers.entries()),
196
+ ...corsHeaders(),
197
+ "X-Proxy-Request-ID": requestId,
198
+ "X-Proxy-Duration-Ms": String(durationMs),
199
+ "X-Proxy-Retries": String(retries),
200
+ },
201
+ });
202
+
203
+ } catch (err) {
204
+ const message = err instanceof Error ? err.message : "Unknown error";
205
+ const isTimeout = message.includes("timed out");
206
+
207
+ console.error(JSON.stringify({
208
+ requestId,
209
+ clientIP,
210
+ method: request.method,
211
+ path: telegramPath,
212
+ error: message,
213
+ durationMs: Date.now() - startTime,
214
+ }));
215
+
216
+ return errorResponse(
217
+ isTimeout ? 504 : 502,
218
+ isTimeout ? "GATEWAY_TIMEOUT" : "BAD_GATEWAY",
219
+ message,
220
+ { requestId }
221
+ );
222
+ }
223
+ },
224
+ };