refactor: migrate webhook to Hono router with auth middleware
webhook.ts:
- Convert handleWebhook() to Hono router pattern
- Create telegramAuth middleware with security validations:
- HTTP method validation (POST only)
- Content-Type validation (application/json)
- Timing-safe secret token comparison
- Timestamp validation (5-min replay attack prevention)
- Request body structure validation
- Always return 200 to Telegram (prevents retry storms)
- Structured logging with context
index.ts:
- Import webhookRouter instead of handleWebhook
- Use app.route('/webhook', webhookRouter)
Benefits:
- Consistent Hono pattern across all routes
- Reusable auth middleware
- Better separation of concerns
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Env, EmailMessage, ProvisionMessage, MessageBatch } from './types';
|
||||
import { sendMessage, setWebhook, getWebhookInfo } from './telegram';
|
||||
import { handleWebhook } from './routes/webhook';
|
||||
import { webhookRouter } from './routes/webhook';
|
||||
import { apiRouter } from './routes/api';
|
||||
import { handleHealthCheck } from './routes/health';
|
||||
import { parseBankSMS } from './services/bank-sms-parser';
|
||||
@@ -72,8 +72,8 @@ app.get('/webhook-info', async (c) => {
|
||||
// API routes - use Hono router
|
||||
app.route('/api', apiRouter);
|
||||
|
||||
// Telegram Webhook
|
||||
app.post('/webhook', (c) => handleWebhook(c.req.raw, c.env));
|
||||
// Telegram Webhook - use Hono router with middleware
|
||||
app.route('/webhook', webhookRouter);
|
||||
|
||||
// Root path
|
||||
app.get('/', (c) => {
|
||||
|
||||
@@ -1,34 +1,109 @@
|
||||
import type { Env } from '../types';
|
||||
import { validateWebhookRequest } from '../security';
|
||||
import { Hono } from 'hono';
|
||||
import { createMiddleware } from 'hono/factory';
|
||||
import type { Env, TelegramUpdate } from '../types';
|
||||
import { timingSafeEqual } from '../security';
|
||||
import { handleCallbackQuery } from './handlers/callback-handler';
|
||||
import { handleMessage } from './handlers/message-handler';
|
||||
import { createLogger } from '../utils/logger';
|
||||
|
||||
const logger = createLogger('webhook');
|
||||
|
||||
/**
|
||||
* Telegram Webhook 요청 처리
|
||||
*/
|
||||
export async function handleWebhook(request: Request, env: Env): Promise<Response> {
|
||||
const validation = await validateWebhookRequest(request, env);
|
||||
// Create Hono router
|
||||
const webhook = new Hono<{ Bindings: Env }>();
|
||||
|
||||
if (!validation.valid) {
|
||||
logger.error('검증 실패', new Error(validation.error || 'Unknown validation error'));
|
||||
return new Response(validation.error, { status: 401 });
|
||||
// Telegram webhook authentication middleware
|
||||
const telegramAuth = createMiddleware<{ Bindings: Env }>(async (c, next) => {
|
||||
// 1. HTTP Method validation
|
||||
if (c.req.method !== 'POST') {
|
||||
logger.warn('Invalid HTTP method', { method: c.req.method });
|
||||
return c.text('Method not allowed', 405);
|
||||
}
|
||||
|
||||
// 2. Content-Type validation
|
||||
const contentType = c.req.header('Content-Type');
|
||||
if (!contentType?.includes('application/json')) {
|
||||
logger.warn('Invalid content type', { contentType });
|
||||
return c.text('Invalid content type', 400);
|
||||
}
|
||||
|
||||
// 3. Secret Token validation (timing-safe)
|
||||
const secretToken = c.req.header('X-Telegram-Bot-Api-Secret-Token');
|
||||
|
||||
if (!c.env.WEBHOOK_SECRET) {
|
||||
logger.error('WEBHOOK_SECRET not configured', new Error('Missing WEBHOOK_SECRET'));
|
||||
return c.text('Server configuration error', 500);
|
||||
}
|
||||
|
||||
if (!timingSafeEqual(secretToken, c.env.WEBHOOK_SECRET)) {
|
||||
logger.warn('Invalid webhook secret token');
|
||||
return c.text('Unauthorized', 401);
|
||||
}
|
||||
|
||||
// 4. IP validation (optional warning only)
|
||||
const clientIP = c.req.header('CF-Connecting-IP');
|
||||
if (clientIP) {
|
||||
// Log for monitoring, but don't block (Cloudflare proxy may change IPs)
|
||||
logger.debug('Request from IP', { clientIP });
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
// Webhook endpoint with auth middleware
|
||||
webhook.post('/', telegramAuth, async (c) => {
|
||||
let update: TelegramUpdate;
|
||||
|
||||
try {
|
||||
update = await c.req.json<TelegramUpdate>();
|
||||
} catch (error) {
|
||||
logger.error('JSON parsing error', error as Error);
|
||||
return c.json({ ok: true }); // Return 200 to Telegram anyway
|
||||
}
|
||||
|
||||
// Validate request body structure
|
||||
if (!update || typeof update.update_id !== 'number') {
|
||||
logger.warn('Invalid update structure', { updateKeys: update ? Object.keys(update) : [] });
|
||||
return c.json({ ok: true }); // Return 200 to Telegram anyway
|
||||
}
|
||||
|
||||
// Timestamp validation (5 minutes) - replay attack prevention
|
||||
if (update.message?.date) {
|
||||
const messageTime = update.message.date * 1000;
|
||||
const now = Date.now();
|
||||
const MAX_AGE_MS = 5 * 60 * 1000;
|
||||
|
||||
if (now - messageTime > MAX_AGE_MS) {
|
||||
logger.warn('Message too old', { messageAge: Math.floor((now - messageTime) / 1000) });
|
||||
return c.json({ ok: true }); // Return 200 to Telegram anyway
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const update = validation.update!;
|
||||
|
||||
// Handle callback query (inline button clicks)
|
||||
if (update.callback_query) {
|
||||
await handleCallbackQuery(env, update.callback_query);
|
||||
return new Response('OK');
|
||||
await handleCallbackQuery(c.env, update.callback_query);
|
||||
return c.json({ ok: true });
|
||||
}
|
||||
|
||||
await handleMessage(env, update);
|
||||
return new Response('OK');
|
||||
} catch (error) {
|
||||
logger.error('메시지 처리 오류', error as Error);
|
||||
return new Response('Error', { status: 500 });
|
||||
// Handle text message
|
||||
if (update.message) {
|
||||
await handleMessage(c.env, update);
|
||||
return c.json({ ok: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Unknown update type (e.g., edited_message, channel_post)
|
||||
logger.debug('Unknown update type', { updateKeys: Object.keys(update) });
|
||||
return c.json({ ok: true });
|
||||
|
||||
} catch (error) {
|
||||
// CRITICAL: Always return 200 to Telegram to prevent retries
|
||||
logger.error('Webhook processing error', error as Error, {
|
||||
updateId: update.update_id,
|
||||
hasMessage: !!update.message,
|
||||
hasCallback: !!update.callback_query
|
||||
});
|
||||
return c.json({ ok: true });
|
||||
}
|
||||
});
|
||||
|
||||
export { webhook as webhookRouter };
|
||||
|
||||
Reference in New Issue
Block a user