From dcdd4bb20b26a6830512df7498130fc0781378f0 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 14 Oct 2023 12:50:48 -0500 Subject: [PATCH] Update:HLS router request validation, smooth out transcode reset logic --- server/objects/Stream.js | 22 +++++++------ server/routers/HlsRouter.js | 65 +++++++++++++++++++++++++++---------- 2 files changed, 60 insertions(+), 27 deletions(-) diff --git a/server/objects/Stream.js b/server/objects/Stream.js index 115bb96e..2ee66182 100644 --- a/server/objects/Stream.js +++ b/server/objects/Stream.js @@ -101,7 +101,6 @@ class Stream extends EventEmitter { return 'mpegts' } get segmentBasename() { - if (this.hlsSegmentType === 'fmp4') return 'output-%d.m4s' return 'output-%d.ts' } get segmentStartNumber() { @@ -142,19 +141,21 @@ class Stream extends EventEmitter { async checkSegmentNumberRequest(segNum) { const segStartTime = segNum * this.segmentLength - if (this.startTime > segStartTime) { - Logger.warn(`[STREAM] Segment #${segNum} Request @${secondsToTimestamp(segStartTime)} is before start time (${secondsToTimestamp(this.startTime)}) - Reset Transcode`) - await this.reset(segStartTime - (this.segmentLength * 2)) + if (this.segmentStartNumber > segNum) { + Logger.warn(`[STREAM] Segment #${segNum} Request is before starting segment number #${this.segmentStartNumber} - Reset Transcode`) + await this.reset(segStartTime - (this.segmentLength * 5)) return segStartTime } else if (this.isTranscodeComplete) { return false } - const distanceFromFurthestSegment = segNum - this.furthestSegmentCreated - if (distanceFromFurthestSegment > 10) { - Logger.info(`Segment #${segNum} requested is ${distanceFromFurthestSegment} segments from latest (${secondsToTimestamp(segStartTime)}) - Reset Transcode`) - await this.reset(segStartTime - (this.segmentLength * 2)) - return segStartTime + if (this.furthestSegmentCreated) { + const distanceFromFurthestSegment = segNum - this.furthestSegmentCreated + if (distanceFromFurthestSegment > 10) { + Logger.info(`Segment #${segNum} requested is ${distanceFromFurthestSegment} segments from latest (${secondsToTimestamp(segStartTime)}) - Reset Transcode`) + await this.reset(segStartTime - (this.segmentLength * 5)) + return segStartTime + } } return false @@ -171,7 +172,7 @@ class Stream extends EventEmitter { var files = await fs.readdir(this.streamPath) files.forEach((file) => { var extname = Path.extname(file) - if (extname === '.ts' || extname === '.m4s') { + if (extname === '.ts') { var basename = Path.basename(file, extname) var num_part = basename.split('-')[1] var part_num = Number(num_part) @@ -251,6 +252,7 @@ class Stream extends EventEmitter { Logger.info(`[STREAM] START STREAM - Num Segments: ${this.numSegments}`) this.ffmpeg = Ffmpeg() + this.furthestSegmentCreated = 0 var adjustedStartTime = Math.max(this.startTime - this.maxSeekBackTime, 0) var trackStartTime = await writeConcatFile(this.tracks, this.concatFilesPath, adjustedStartTime) diff --git a/server/routers/HlsRouter.js b/server/routers/HlsRouter.js index d4f1bc60..711e360a 100644 --- a/server/routers/HlsRouter.js +++ b/server/routers/HlsRouter.js @@ -27,28 +27,60 @@ class HlsRouter { return Number(num_part) } - async streamFileRequest(req, res) { - var streamId = req.params.stream - var fullFilePath = Path.join(this.playbackSessionManager.StreamsPath, streamId, req.params.file) + /** + * Ensure filepath is inside streamDir + * Used to prevent arbitrary file reads + * @see https://nodejs.org/api/path.html#pathrelativefrom-to + * + * @param {string} streamDir + * @param {string} filepath + * @returns {boolean} + */ + validateStreamFilePath(streamDir, filepath) { + const relative = Path.relative(streamDir, filepath) + return relative && !relative.startsWith('..') && !Path.isAbsolute(relative) + } - var exists = await fs.pathExists(fullFilePath) - if (!exists) { + /** + * GET /hls/:stream/:file + * File must have extname .ts or .m3u8 + * + * @param {express.Request} req + * @param {express.Response} res + */ + async streamFileRequest(req, res) { + const streamId = req.params.stream + // Ensure stream is open + const stream = this.playbackSessionManager.getStream(streamId) + if (!stream) { + Logger.error(`[HlsRouter] Stream "${streamId}" does not exist`) + return res.sendStatus(404) + } + + // Ensure stream filepath is valid + const streamDir = Path.join(this.playbackSessionManager.StreamsPath, streamId) + const fullFilePath = Path.join(streamDir, req.params.file) + if (!this.validateStreamFilePath(streamDir, fullFilePath)) { + Logger.error(`[HlsRouter] Invalid file parameter "${req.params.file}"`) + return res.sendStatus(400) + } + + const fileExt = Path.extname(req.params.file) + if (fileExt !== '.ts' && fileExt !== '.m3u8') { + Logger.error(`[HlsRouter] Invalid file parameter "${req.params.file}" extname. Must be .ts or .m3u8`) + return res.sendStatus(400) + } + + if (!(await fs.pathExists(fullFilePath))) { Logger.warn('File path does not exist', fullFilePath) - var fileExt = Path.extname(req.params.file) - if (fileExt === '.ts' || fileExt === '.m4s') { - var segNum = this.parseSegmentFilename(req.params.file) - var stream = this.playbackSessionManager.getStream(streamId) - if (!stream) { - Logger.error(`[HlsRouter] Stream ${streamId} does not exist`) - return res.sendStatus(500) - } + if (fileExt === '.ts') { + const segNum = this.parseSegmentFilename(req.params.file) if (stream.isResetting) { Logger.info(`[HlsRouter] Stream ${streamId} is currently resetting`) - return res.sendStatus(404) } else { - var startTimeForReset = await stream.checkSegmentNumberRequest(segNum) + const startTimeForReset = await stream.checkSegmentNumberRequest(segNum) if (startTimeForReset) { // HLS.js will restart the stream at the new time Logger.info(`[HlsRouter] Resetting Stream - notify client @${startTimeForReset}s`) @@ -56,13 +88,12 @@ class HlsRouter { startTime: startTimeForReset, streamId: stream.id }) - return res.sendStatus(500) } } } + return res.sendStatus(404) } - // Logger.info('Sending file', fullFilePath) res.sendFile(fullFilePath) } }