← Cloudflare Stream / stream / examples
Протестируйте вебхуки локально
Cloudflare Stream не может отправлять уведомления через вебхуки к localhost или локальные IP-адреса. Чтобы протестировать вебхуки в процессе локальной разработки, вам понадобится общедоступный URL, перенаправляющий запросы на ваш локальный компьютер.
В этом примере показано, как:
- Запустите Cloudflare Tunnel чтобы получить публичный URL для локального окружения.
- Зарегистрируйте этот URL как конечную точку вебхука, которая вернёт секрет подписи.
- Создайте Cloudflare Worker, который принимает события webhook Stream и проверяет их подписи.
Предварительные требования
- A Аккаунт Cloudflare ↗ с включённым Stream
- Node.js ↗ (v18 или новее)
- Wrangler CLI установлен (
npm install -g wrangler)
1. Создайте проект Worker
Создайте новый проект Worker, который будет принимать webhook-запросы:
npm create cloudflare@latest stream-webhook-handler2. Запуск Cloudflare Tunnel
Прежде чем регистрировать URL вебхука, вам нужен публичный URL, указывающий на локальную машину. В терминале запустите quick tunnel который перенаправляет запросы на порт сервера разработки Wrangler по умолчанию (8787):
npx cloudflared tunnel --url http://localhost:8787cloudflared выведет публичный URL, похожий на:
https://example-words-here.trycloudflare.comСкопируйте этот URL. Он меняется при каждом перезапуске туннеля.
3. Регистрация URL-адреса туннеля в качестве конечной точки вебхука
Используйте Stream API, чтобы указать URL-адрес туннеля в качестве URL-адреса уведомлений webhook. Ответ API включает secret поле: оно понадобится для проверки подписей webhook.
Необходимые разрешения API-токена
Хотя бы одно из следующих права доступа токена требуется:Stream Write
curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/stream/webhook" \
--request PUT \
--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
--json '{
"notificationUrl": "https://example-words-here.trycloudflare.com"
}'Ответ будет включать secret поле:
{
"result": {
"notificationUrl": "https://example-words-here.trycloudflare.com",
"modified": "2024-01-01T00:00:00.000000Z",
"secret": "85011ed3a913c6ad5f9cf6c5573cc0a7"
},
"success": true,
"errors": [],
"messages": []
}Сохраните secret значение. Вы используете его на следующем шаге.
4. Сохранение секрета вебхука для локальной разработки
Создайте .dev.vars файл в корне проекта Worker и добавьте секрет webhook из ответа API:
WEBHOOK_SECRET=85011ed3a913c6ad5f9cf6c5573cc0a7Замените значение на настоящий секрет из шага 3. Wrangler автоматически загружает .dev.vars при выполнении wrangler dev.
5. Добавление обработчика вебхука
Замените содержимое src/index.ts в вашем проекте Worker с помощью следующего кода. Этот Worker получает вебхук POST запросов, проверяет подпись, и записывает полезную нагрузку в журнал.
export interface Env {
WEBHOOK_SECRET: string;
}
async function verifyWebhookSignature(
request: Request,
secret: string,
): Promise<{ valid: boolean; body: string }> {
const signatureHeader = request.headers.get("Webhook-Signature");
if (!signatureHeader) {
return { valid: false, body: "" };
}
const body = await request.text();
// Parse "time=<unix_ts>,sig1=<hex_signature>"
const parts = Object.fromEntries(
signatureHeader.split(",").map((part) => {
const [key, value] = part.split("=");
return [key, value];
}),
);
const time = parts["time"];
const receivedSig = parts["sig1"];
if (!time || !receivedSig) {
return { valid: false, body };
}
// Build the source string: "<time>.<body>"
const sourceString = `${time}.${body}`;
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
const signature = await crypto.subtle.sign(
"HMAC",
key,
encoder.encode(sourceString),
);
const expectedSig = [...new Uint8Array(signature)]
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
// Use a timing-safe comparison.
// Do not return early when lengths differ — that leaks the expected
// signature's length through timing. Compare against self and negate instead.
const expectedBytes = encoder.encode(expectedSig);
const receivedBytes = encoder.encode(receivedSig);
const lengthsMatch = expectedBytes.byteLength === receivedBytes.byteLength;
const signaturesMatch = lengthsMatch
? crypto.subtle.timingSafeEqual(expectedBytes, receivedBytes)
: !crypto.subtle.timingSafeEqual(expectedBytes, expectedBytes);
return { valid: signaturesMatch, body };
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method !== "POST") {
return new Response("Method not allowed", { status: 405 });
}
if (!env.WEBHOOK_SECRET) {
console.error("WEBHOOK_SECRET is not set");
return new Response("Server misconfigured", { status: 500 });
}
const { valid, body } = await verifyWebhookSignature(
request,
env.WEBHOOK_SECRET,
);
if (!valid) {
console.error("Invalid webhook signature");
return new Response("Invalid signature", { status: 403 });
}
console.log("Webhook signature verified successfully");
const payload = JSON.parse(body);
console.log("Stream webhook received:", JSON.stringify(payload, null, 2));
console.log("Video UID:", payload.uid);
console.log("Status:", payload.status?.state);
console.log("Ready to stream:", payload.readyToStream);
// Add your own processing logic here — for example, update a database
// or notify a downstream service.
return new Response("OK", { status: 200 });
},
} satisfies ExportedHandler<Env>;6. Запуск локального сервера разработки
В отдельном терминале (не закрывая туннель) запустите Worker локально с помощью Wrangler:
npx wrangler devWrangler загрузит WEBHOOK_SECRET из вашего .dev.vars файл автоматически.
7. Запуск тестового события
Загрузите видео в Stream, чтобы инициировать событие webhook. После завершения обработки видео вы увидите полезную нагрузку webhook в журнале терминала, где выполняется wrangler dev, вместе с подтверждением того, что подпись была проверена.
Переход в продакшн
После завершения локального тестирования разверните Worker и обновите URL-адрес вебхука на адрес рабочей среды:
npx wrangler deployЗатем обновите подписку webhook, указав в ней URL развёрнутого Worker:
Необходимые разрешения API-токена
Хотя бы одно из следующих права доступа токена требуется:Stream Write
curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/stream/webhook" \
--request PUT \
--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
--json '{
"notificationUrl": "https://your-worker.your-subdomain.workers.dev"
}'