File size: 4,245 Bytes
a81d1fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
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);
    }
  },
};