feat: add Queue-based server provisioning with security fixes
- Add Cloudflare Queue for async server provisioning - Producer: callback-handler.ts sends to queue - Consumer: provision-consumer.ts processes orders - DLQ: provision-dlq.ts handles failed orders with refund - Security improvements (from code review): - Store password hash instead of plaintext (SHA-256) - Exclude root_password from logs - Add retryable flag to prevent duplicate instance creation - Atomic balance deduction with db.batch() - Race condition prevention with UPDATE...WHERE status='pending' - Auto-refund on DLQ processing - Validation improvements: - OS image whitelist validation - Session required fields validation - Queue handler refactoring Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { answerCallbackQuery, editMessageText, sendMessage, sendMessageWithKeyboard } from '../../telegram';
|
||||
import { UserService } from '../../services/user-service';
|
||||
import { executeDomainRegister } from '../../domain-register';
|
||||
import { executeServerProvision } from '../../server-provision';
|
||||
import {
|
||||
getSessionForUser,
|
||||
updateSession,
|
||||
@@ -11,8 +10,16 @@ import {
|
||||
} from '../../utils/session';
|
||||
import { getServerSpec } from '../../services/cloud-spec-service';
|
||||
import { getRegionDisplay, getOSDisplayName, NUM_EMOJIS } from '../../constants/server';
|
||||
import { createLogger } from '../../utils/logger';
|
||||
import type { Env, TelegramUpdate } from '../../types';
|
||||
|
||||
const logger = createLogger('callback-handler');
|
||||
|
||||
/**
|
||||
* Allowed OS images for server provisioning
|
||||
*/
|
||||
const ALLOWED_OS_IMAGES = ['ubuntu-22.04', 'ubuntu-24.04', 'debian-12', 'centos-stream-9'];
|
||||
|
||||
/**
|
||||
* Safely parse integer with range validation
|
||||
* @param value - String to parse
|
||||
@@ -263,6 +270,12 @@ ${result.error}
|
||||
if (action === 'os') {
|
||||
const osImage = parts[3];
|
||||
|
||||
// Validation: Check if OS image is allowed
|
||||
if (!ALLOWED_OS_IMAGES.includes(osImage)) {
|
||||
await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '지원하지 않는 OS입니다.' });
|
||||
return;
|
||||
}
|
||||
|
||||
await updateSession<ServerOrderSessionData>(env.SESSION_KV, sessionId, {
|
||||
step: 'final_confirm',
|
||||
image: osImage
|
||||
@@ -272,6 +285,14 @@ ${result.error}
|
||||
|
||||
const { plan, region, provider } = session.data;
|
||||
|
||||
// Validation: Check if required session data exists
|
||||
if (!plan || !region || !provider) {
|
||||
logger.error('세션 데이터 불완전', undefined, { sessionId, plan, region, provider });
|
||||
await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '세션이 만료되었습니다. 다시 시작해주세요.' });
|
||||
await deleteSession(env.SESSION_KV, sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
// DB에서 사양 조회
|
||||
const spec = await getServerSpec(
|
||||
env.CLOUD_DB,
|
||||
@@ -362,7 +383,7 @@ ${result.error}
|
||||
return;
|
||||
}
|
||||
|
||||
// confirm: 서버 생성 실행
|
||||
// confirm: 서버 생성 요청 (Queue 전송)
|
||||
if (action === 'confirm') {
|
||||
const { orderId } = session.data;
|
||||
if (!orderId) {
|
||||
@@ -370,50 +391,35 @@ ${result.error}
|
||||
return;
|
||||
}
|
||||
|
||||
await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '서버 생성 중...' });
|
||||
await editMessageText(env.BOT_TOKEN, chatId, messageId,
|
||||
'⏳ 서버를 생성하고 있습니다... (1-3분 소요)'
|
||||
);
|
||||
await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '주문 접수 중...' });
|
||||
|
||||
const result = await executeServerProvision(env, user.id, telegramUserId, orderId);
|
||||
// Queue에 메시지 전송 (즉시 반환)
|
||||
await env.SERVER_PROVISION_QUEUE.send({
|
||||
order_id: orderId,
|
||||
user_id: user.id,
|
||||
telegram_user_id: telegramUserId,
|
||||
chat_id: chatId,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
// 사용자에게 즉시 응답
|
||||
await editMessageText(
|
||||
env.BOT_TOKEN,
|
||||
chatId,
|
||||
messageId,
|
||||
`📋 <b>서버 생성 주문 접수 완료!</b>
|
||||
|
||||
주문번호: #${orderId}
|
||||
|
||||
⏳ 서버를 생성하고 있습니다. (1-3분 소요)
|
||||
완료되면 알림을 보내드립니다.
|
||||
|
||||
💡 이 메시지를 닫아도 괜찮습니다.`,
|
||||
{ parse_mode: 'HTML' }
|
||||
);
|
||||
|
||||
// 세션 삭제
|
||||
await deleteSession(env.SESSION_KV, sessionId);
|
||||
|
||||
if (result.success) {
|
||||
await editMessageText(
|
||||
env.BOT_TOKEN,
|
||||
chatId,
|
||||
messageId,
|
||||
`✅ <b>서버 생성 완료!</b>
|
||||
|
||||
• 사양: <code>${result.plan_label}</code>
|
||||
• 리전: ${result.region}
|
||||
• IP 주소: <code>${result.ip_address}</code>
|
||||
• Root 비밀번호: <code>${result.root_password}</code>
|
||||
|
||||
📌 <b>접속 방법</b>
|
||||
<code>ssh root@${result.ip_address}</code>
|
||||
|
||||
⚠️ <b>보안 권고</b>
|
||||
1. 즉시 비밀번호를 변경하세요: <code>passwd</code>
|
||||
2. SSH 키 인증 설정을 권장합니다.
|
||||
3. 방화벽(UFW)을 활성화하세요.
|
||||
|
||||
🎉 서버가 성공적으로 생성되었습니다!`
|
||||
);
|
||||
} else {
|
||||
await editMessageText(
|
||||
env.BOT_TOKEN,
|
||||
chatId,
|
||||
messageId,
|
||||
`❌ <b>서버 생성 실패</b>
|
||||
|
||||
${result.error}
|
||||
|
||||
다시 시도하시려면 서버 주문을 요청해주세요.`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -650,7 +656,7 @@ ${result.error}
|
||||
return;
|
||||
}
|
||||
|
||||
// 서버 주문 확인
|
||||
// 서버 주문 확인 (레거시 - Queue 기반으로 전환)
|
||||
if (data.startsWith('server_order:')) {
|
||||
const parts = data.split(':');
|
||||
if (parts.length !== 2) {
|
||||
@@ -665,50 +671,33 @@ ${result.error}
|
||||
return;
|
||||
}
|
||||
|
||||
await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '서버 생성 중...' });
|
||||
await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '주문 접수 중...' });
|
||||
|
||||
// Queue에 메시지 전송 (즉시 반환)
|
||||
await env.SERVER_PROVISION_QUEUE.send({
|
||||
order_id: orderId,
|
||||
user_id: user.id,
|
||||
telegram_user_id: telegramUserId,
|
||||
chat_id: chatId,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
// 사용자에게 즉시 응답
|
||||
await editMessageText(
|
||||
env.BOT_TOKEN,
|
||||
chatId,
|
||||
messageId,
|
||||
'⏳ 서버를 생성하고 있습니다... (1-3분 소요)'
|
||||
`📋 <b>서버 생성 주문 접수 완료!</b>
|
||||
|
||||
주문번호: #${orderId}
|
||||
|
||||
⏳ 서버를 생성하고 있습니다. (1-3분 소요)
|
||||
완료되면 알림을 보내드립니다.
|
||||
|
||||
💡 이 메시지를 닫아도 괜찮습니다.`,
|
||||
{ parse_mode: 'HTML' }
|
||||
);
|
||||
|
||||
const result = await executeServerProvision(env, user.id, telegramUserId, orderId);
|
||||
|
||||
if (result.success) {
|
||||
await editMessageText(
|
||||
env.BOT_TOKEN,
|
||||
chatId,
|
||||
messageId,
|
||||
`✅ <b>서버 생성 완료!</b>
|
||||
|
||||
• 사양: <code>${result.plan_label}</code>
|
||||
• 리전: ${result.region}
|
||||
• IP 주소: <code>${result.ip_address}</code>
|
||||
• Root 비밀번호: <code>${result.root_password}</code>
|
||||
|
||||
📌 <b>접속 방법</b>
|
||||
<code>ssh root@${result.ip_address}</code>
|
||||
|
||||
⚠️ <b>보안 권고</b>
|
||||
1. 즉시 비밀번호를 변경하세요: <code>passwd</code>
|
||||
2. SSH 키 인증 설정을 권장합니다.
|
||||
3. 방화벽(UFW)을 활성화하세요.
|
||||
|
||||
🎉 서버가 성공적으로 생성되었습니다!`
|
||||
);
|
||||
} else {
|
||||
await editMessageText(
|
||||
env.BOT_TOKEN,
|
||||
chatId,
|
||||
messageId,
|
||||
`❌ <b>서버 생성 실패</b>
|
||||
|
||||
${result.error}
|
||||
|
||||
다시 시도하시려면 서버 주문을 요청해주세요.`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user