INTEGRITY Документация

Протестируйте вебхуки локально

Cloudflare Stream не может отправлять уведомления через вебхуки к localhost или локальные IP-адреса. Чтобы протестировать вебхуки в процессе локальной разработки, вам понадобится общедоступный URL, перенаправляющий запросы на ваш локальный компьютер.

В этом примере показано, как:

  1. Запустите Cloudflare Tunnel чтобы получить публичный URL для локального окружения.
  2. Зарегистрируйте этот URL как конечную точку вебхука, которая вернёт секрет подписи.
  3. Создайте Cloudflare Worker, который принимает события webhook Stream и проверяет их подписи.

Предварительные требования

1. Создайте проект Worker

Создайте новый проект Worker, который будет принимать webhook-запросы:

npm create cloudflare@latest stream-webhook-handler

2. Запуск Cloudflare Tunnel

Прежде чем регистрировать URL вебхука, вам нужен публичный URL, указывающий на локальную машину. В терминале запустите quick tunnel который перенаправляет запросы на порт сервера разработки Wrangler по умолчанию (8787):

npx cloudflared tunnel --url http://localhost:8787

cloudflared выведет публичный URL, похожий на:

https://example-words-here.trycloudflare.com

Скопируйте этот URL. Он меняется при каждом перезапуске туннеля.

3. Регистрация URL-адреса туннеля в качестве конечной точки вебхука

Используйте Stream API, чтобы указать URL-адрес туннеля в качестве URL-адреса уведомлений webhook. Ответ API включает secret поле: оно понадобится для проверки подписей webhook.

Необходимые разрешения API-токена

Хотя бы одно из следующих права доступа токена требуется:
Создание webhook
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:

.dev.vars
WEBHOOK_SECRET=85011ed3a913c6ad5f9cf6c5573cc0a7

Замените значение на настоящий секрет из шага 3. Wrangler автоматически загружает .dev.vars при выполнении wrangler dev.

5. Добавление обработчика вебхука

Замените содержимое src/index.ts в вашем проекте Worker с помощью следующего кода. Этот Worker получает вебхук POST запросов, проверяет подпись, и записывает полезную нагрузку в журнал.

src/index.ts
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 dev

Wrangler загрузит WEBHOOK_SECRET из вашего .dev.vars файл автоматически.

7. Запуск тестового события

Загрузите видео в Stream, чтобы инициировать событие webhook. После завершения обработки видео вы увидите полезную нагрузку webhook в журнале терминала, где выполняется wrangler dev, вместе с подтверждением того, что подпись была проверена.

Переход в продакшн

После завершения локального тестирования разверните Worker и обновите URL-адрес вебхука на адрес рабочей среды:

npx wrangler deploy

Затем обновите подписку webhook, указав в ней URL развёрнутого Worker:

Необходимые разрешения API-токена

Хотя бы одно из следующих права доступа токена требуется:
Создание webhook
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"
	}'