prtooltest / telegram-relay /worker.mjs
Dink0's picture
Fix Telegram delivery with an authenticated free-tier relay (#1)
a81d1fc
Raw History Blame Contribute Delete
4.25 kB
const allowedMethods = new Set(["getMe", "getChat", "getChatMember", "sendMessage", "editMessageText"]);
const maxBytes = 65536;
function reply(status, description) {
return Response.json({ ok: false, description }, {
status,
headers: { "Cache-Control": "no-store" },
});
}
async function authorized(actual, expected) {
const encoder = new TextEncoder();
const [actualHash, expectedHash] = await Promise.all([
crypto.subtle.digest("SHA-256", encoder.encode(actual)),
crypto.subtle.digest("SHA-256", encoder.encode(expected)),
]);
const actualBytes = new Uint8Array(actualHash);
const expectedBytes = new Uint8Array(expectedHash);
let difference = 0;
for (let index = 0; index < actualBytes.length; index += 1) {
difference |= actualBytes[index] ^ expectedBytes[index];
}
return difference === 0;
}
async function readBody(request) {
if (!request.body) throw new Error("empty_body");
const reader = request.body.getReader();
const chunks = [];
let size = 0;
while (true) {
const { value, done } = await reader.read();
if (done) break;
size += value.byteLength;
if (size > maxBytes) {
await reader.cancel();
throw new Error("payload_too_large");
}
chunks.push(value);
}
const body = new Uint8Array(size);
let offset = 0;
for (const chunk of chunks) {
body.set(chunk, offset);
offset += chunk.byteLength;
}
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(body));
}
export default {
async fetch(request, environment) {
const url = new URL(request.url);
if (url.pathname !== "/telegram" || url.search || request.method !== "POST") {
return reply(404, "not_found");
}
if (!/^\d+:[A-Za-z0-9_-]+$/.test(environment.TELEGRAM_BOT_TOKEN || "")
|| !/^[A-Za-z0-9_-]{32,}$/.test(environment.RELAY_SECRET || "")
|| !/^-?\d+$/.test(environment.TELEGRAM_CHAT_ID || "")) {
return reply(503, "configuration_required");
}
if (!await authorized(request.headers.get("Authorization") || "", "Bearer " + environment.RELAY_SECRET)) {
return reply(401, "unauthorized");
}
let input;
try {
input = await readBody(request);
} catch (error) {
return reply(error.message === "payload_too_large" ? 413 : 400, "invalid_payload");
}
if (!input || typeof input !== "object" || !allowedMethods.has(input.method)
|| !input.payload || typeof input.payload !== "object" || Array.isArray(input.payload)) {
return reply(400, "invalid_method_or_payload");
}
if (input.method === "getMe") {
input.payload = {};
} else if (String(input.payload.chat_id) !== environment.TELEGRAM_CHAT_ID) {
return reply(403, "chat_not_allowed");
}
try {
const response = await fetch(
"https://api.telegram.org/bot" + environment.TELEGRAM_BOT_TOKEN + "/" + input.method,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(input.payload),
redirect: "manual",
signal: AbortSignal.timeout(8000),
},
);
if (response.status >= 300 && response.status < 400) return reply(502, "telegram_unexpected_redirect");
const body = await response.text();
let data;
try {
data = JSON.parse(body);
} catch {
return reply(502, "telegram_upstream_non_json_http_" + response.status);
}
if (!data || typeof data.ok !== "boolean") return reply(502, "telegram_invalid_response");
return new Response(body, {
status: response.status,
headers: { "Content-Type": "application/json", "Cache-Control": "no-store" },
});
} catch (error) {
if (typeof AbortSignal.timeout !== "function") return reply(502, "telegram_timeout_api_unavailable");
if (String(error.message).includes("Network connection lost")) return reply(502, "telegram_network_connection_lost");
if (String(error.message).includes("resolve")) return reply(502, "telegram_dns_error");
const category = ["TimeoutError", "AbortError", "TypeError"].includes(error.name) ? error.name : "unknown";
return reply(502, "telegram_upstream_" + category);
}
},
};