From f286638b195d6b7bcdea682c69bc793a8375e287 Mon Sep 17 00:00:00 2001 From: sebastjanartic <45803536-sebastjanartic@users.noreply.replit.com> Date: Mon, 4 Aug 2025 19:13:59 +0000 Subject: [PATCH] Improve video access security by adding signed URL support from Bunny.net Implements signed URLs for secure video and thumbnail delivery using the BUNNY_SECURITY_KEY environment variable in bunny.ts. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 50814a1e-92e4-4968-856f-7bc7eedf5e8f Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/50814a1e-92e4-4968-856f-7bc7eedf5e8f/3tWpY1N --- server/bunny.ts | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/server/bunny.ts b/server/bunny.ts index 3e25a26..3877b46 100644 --- a/server/bunny.ts +++ b/server/bunny.ts @@ -1,4 +1,5 @@ import { type Video, type InsertVideo } from "@shared/schema"; +import crypto from 'crypto'; interface BunnyVideo { guid: string; @@ -22,11 +23,13 @@ export class BunnyService { private apiKey: string; private libraryId: string; private hostname: string; + private securityKey: string; constructor() { this.apiKey = process.env.BUNNY_API_KEY!; this.libraryId = process.env.BUNNY_LIBRARY_ID!; this.hostname = process.env.BUNNY_HOSTNAME!; + this.securityKey = process.env.BUNNY_SECURITY_KEY || ''; // CDN security key for signing URLs if (!this.apiKey || !this.libraryId || !this.hostname) { throw new Error("Missing Bunny.net configuration"); @@ -51,18 +54,32 @@ export class BunnyService { return response.json(); } - private bunnyVideoToVideo(bunnyVideo: BunnyVideo): Video { - // Use proxy endpoint for thumbnails to handle private access - const thumbnailUrl = `/thumbnail/${bunnyVideo.guid}`; + private generateSignedUrl(path: string, expiryHours: number = 1): string { + if (!this.securityKey) { + // If no security key, return iframe embed as fallback + const videoId = path.split('/')[1]; + return `https://iframe.mediadelivery.net/embed/${this.libraryId}/${videoId}?controls=true&autoplay=false`; + } - // Try direct CDN URL first - some videos might be accessible - const directUrl = `https://${this.hostname}/${bunnyVideo.guid}/playlist.m3u8`; + const expireTimestamp = Math.floor(Date.now() / 1000) + (expiryHours * 3600); + const tokenContent = `${this.securityKey}${path}${expireTimestamp}`; - // Fallback iframe embed for private videos - const iframeUrl = `https://iframe.mediadelivery.net/embed/${this.libraryId}/${bunnyVideo.guid}?controls=true&autoplay=false`; + const hash = crypto.createHash('md5').update(tokenContent).digest(); + const token = Buffer.from(hash).toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); + + return `https://${this.hostname}${path}?token=${token}&expires=${expireTimestamp}`; + } + + private bunnyVideoToVideo(bunnyVideo: BunnyVideo): Video { + // Generate signed URLs for private video access + const videoPath = `/${bunnyVideo.guid}/playlist.m3u8`; + const thumbnailPath = `/${bunnyVideo.guid}/${bunnyVideo.thumbnailFileName || 'thumbnail.jpg'}`; - // Use direct URL for HLS streaming - const videoUrl = directUrl; + const videoUrl = this.generateSignedUrl(videoPath); + const thumbnailUrl = this.generateSignedUrl(thumbnailPath); return { id: bunnyVideo.guid,