121 lines
5.0 KiB
JavaScript
121 lines
5.0 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.webhookCallback = webhookCallback;
|
|
const platform_node_js_1 = require("../platform.node.js");
|
|
const frameworks_js_1 = require("./frameworks.js");
|
|
const debugErr = (0, platform_node_js_1.debug)("grammy:error");
|
|
const callbackAdapter = (update, callback, header, unauthorized = () => callback('"unauthorized"')) => ({
|
|
update: Promise.resolve(update),
|
|
respond: callback,
|
|
header,
|
|
unauthorized,
|
|
});
|
|
const adapters = { ...frameworks_js_1.adapters, callback: callbackAdapter };
|
|
/**
|
|
* Performs a constant-time comparison of two strings to prevent timing attacks.
|
|
* This function always compares all bytes regardless of early differences,
|
|
* ensuring the comparison time does not leak information about the secret.
|
|
*
|
|
* @param header The header value from the request (X-Telegram-Bot-Api-Secret-Token)
|
|
* @param token The expected secret token configured for the webhook
|
|
* @returns true if strings are equal, false otherwise
|
|
*/
|
|
function compareSecretToken(header, token) {
|
|
// If no token is configured, accept all requests
|
|
if (token === undefined) {
|
|
return true;
|
|
}
|
|
// If token is configured but no header provided, reject
|
|
if (header === undefined) {
|
|
return false;
|
|
}
|
|
// Convert strings to Uint8Array for byte-by-byte comparison
|
|
const encoder = new TextEncoder();
|
|
const headerBytes = encoder.encode(header);
|
|
const tokenBytes = encoder.encode(token);
|
|
// If lengths differ, reject
|
|
if (headerBytes.length !== tokenBytes.length) {
|
|
return false;
|
|
}
|
|
let hasDifference = 0;
|
|
// Always iterate exactly tokenBytes.length times to prevent timing attacks
|
|
// that could reveal the secret token's length. The loop time is constant
|
|
// relative to the secret token length, not the attacker's input length.
|
|
for (let i = 0; i < tokenBytes.length; i++) {
|
|
// If header is shorter than token, pad with 0 for comparison
|
|
const headerByte = i < headerBytes.length ? headerBytes[i] : 0;
|
|
const tokenByte = tokenBytes[i];
|
|
// If bytes differ, mark that we found a difference
|
|
// Using bitwise OR to maintain constant-time (no short-circuit evaluation)
|
|
hasDifference |= headerByte ^ tokenByte;
|
|
}
|
|
// Return true only if no differences were found
|
|
return hasDifference === 0;
|
|
}
|
|
function webhookCallback(bot, adapter = platform_node_js_1.defaultAdapter, onTimeout, timeoutMilliseconds, secretToken) {
|
|
if (bot.isRunning()) {
|
|
throw new Error("Bot is already running via long polling, the webhook setup won't receive any updates!");
|
|
}
|
|
else {
|
|
bot.start = () => {
|
|
throw new Error("You already started the bot via webhooks, calling `bot.start()` starts the bot with long polling and this will prevent your webhook setup from receiving any updates!");
|
|
};
|
|
}
|
|
const { onTimeout: timeout = "throw", timeoutMilliseconds: ms = 10000, secretToken: token, } = typeof onTimeout === "object"
|
|
? onTimeout
|
|
: { onTimeout, timeoutMilliseconds, secretToken };
|
|
let initialized = false;
|
|
const server = typeof adapter === "string"
|
|
? adapters[adapter]
|
|
: adapter;
|
|
return async (...args) => {
|
|
var _a;
|
|
const handler = server(...args);
|
|
if (!initialized) {
|
|
// Will dedupe concurrently incoming calls from several updates
|
|
await bot.init();
|
|
initialized = true;
|
|
}
|
|
if (!compareSecretToken(handler.header, token)) {
|
|
await handler.unauthorized();
|
|
return handler.handlerReturn;
|
|
}
|
|
let usedWebhookReply = false;
|
|
const webhookReplyEnvelope = {
|
|
async send(json) {
|
|
usedWebhookReply = true;
|
|
await handler.respond(json);
|
|
},
|
|
};
|
|
await timeoutIfNecessary(bot.handleUpdate(await handler.update, webhookReplyEnvelope), typeof timeout === "function" ? () => timeout(...args) : timeout, ms);
|
|
if (!usedWebhookReply)
|
|
(_a = handler.end) === null || _a === void 0 ? void 0 : _a.call(handler);
|
|
return handler.handlerReturn;
|
|
};
|
|
}
|
|
function timeoutIfNecessary(task, onTimeout, timeout) {
|
|
if (timeout === Infinity)
|
|
return task;
|
|
return new Promise((resolve, reject) => {
|
|
const handle = setTimeout(() => {
|
|
debugErr(`Request timed out after ${timeout} ms`);
|
|
if (onTimeout === "throw") {
|
|
reject(new Error(`Request timed out after ${timeout} ms`));
|
|
}
|
|
else {
|
|
if (typeof onTimeout === "function")
|
|
onTimeout();
|
|
resolve();
|
|
}
|
|
const now = Date.now();
|
|
task.finally(() => {
|
|
const diff = Date.now() - now;
|
|
debugErr(`Request completed ${diff} ms after timeout!`);
|
|
});
|
|
}, timeout);
|
|
task.then(resolve)
|
|
.catch(reject)
|
|
.finally(() => clearTimeout(handle));
|
|
});
|
|
}
|