123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184 |
- import express from 'express'
- import { query } from 'express-validator'
- import { LRUCache } from 'lru-cache'
- import { basename, dirname } from 'path'
- import { exists, isSafePeerTubeFilenameWithoutExtension, isUUIDValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc.js'
- import { logger } from '@server/helpers/logger.js'
- import { LRU_CACHE } from '@server/initializers/constants.js'
- import { VideoModel } from '@server/models/video/video.js'
- import { VideoFileModel } from '@server/models/video/video-file.js'
- import { MStreamingPlaylist, MVideoFile, MVideoThumbnail } from '@server/types/models/index.js'
- import { HttpStatusCode } from '@peertube/peertube-models'
- import { areValidationErrors, checkCanAccessVideoStaticFiles, isValidVideoPasswordHeader } from './shared/index.js'
- type LRUValue = {
- allowed: boolean
- video?: MVideoThumbnail
- file?: MVideoFile
- playlist?: MStreamingPlaylist }
- const staticFileTokenBypass = new LRUCache<string, LRUValue>({
- max: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.MAX_SIZE,
- ttl: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.TTL
- })
- const ensureCanAccessVideoPrivateWebVideoFiles = [
- query('videoFileToken').optional().custom(exists),
- isValidVideoPasswordHeader(),
- async (req: express.Request, res: express.Response, next: express.NextFunction) => {
- if (areValidationErrors(req, res)) return
- const token = extractTokenOrDie(req, res)
- if (!token) return
- const cacheKey = token + '-' + req.originalUrl
- if (staticFileTokenBypass.has(cacheKey)) {
- const { allowed, file, video } = staticFileTokenBypass.get(cacheKey)
- if (allowed === true) {
- res.locals.onlyVideo = video
- res.locals.videoFile = file
- return next()
- }
- return res.sendStatus(HttpStatusCode.FORBIDDEN_403)
- }
- const result = await isWebVideoAllowed(req, res)
- staticFileTokenBypass.set(cacheKey, result)
- if (result.allowed !== true) return
- res.locals.onlyVideo = result.video
- res.locals.videoFile = result.file
- return next()
- }
- ]
- const ensureCanAccessPrivateVideoHLSFiles = [
- query('videoFileToken')
- .optional()
- .custom(exists),
- query('reinjectVideoFileToken')
- .optional()
- .customSanitizer(toBooleanOrNull)
- .isBoolean().withMessage('Should be a valid reinjectVideoFileToken boolean'),
- query('playlistName')
- .optional()
- .customSanitizer(isSafePeerTubeFilenameWithoutExtension),
- isValidVideoPasswordHeader(),
- async (req: express.Request, res: express.Response, next: express.NextFunction) => {
- if (areValidationErrors(req, res)) return
- const videoUUID = basename(dirname(req.originalUrl))
- if (!isUUIDValid(videoUUID)) {
- logger.debug('Path does not contain valid video UUID to serve static file %s', req.originalUrl)
- return res.sendStatus(HttpStatusCode.FORBIDDEN_403)
- }
- const token = extractTokenOrDie(req, res)
- if (!token) return
- const cacheKey = token + '-' + videoUUID
- if (staticFileTokenBypass.has(cacheKey)) {
- const { allowed, file, playlist, video } = staticFileTokenBypass.get(cacheKey)
- if (allowed === true) {
- res.locals.onlyVideo = video
- res.locals.videoFile = file
- res.locals.videoStreamingPlaylist = playlist
- return next()
- }
- return res.sendStatus(HttpStatusCode.FORBIDDEN_403)
- }
- const result = await isHLSAllowed(req, res, videoUUID)
- staticFileTokenBypass.set(cacheKey, result)
- if (result.allowed !== true) return
- res.locals.onlyVideo = result.video
- res.locals.videoFile = result.file
- res.locals.videoStreamingPlaylist = result.playlist
- return next()
- }
- ]
- export {
- ensureCanAccessVideoPrivateWebVideoFiles,
- ensureCanAccessPrivateVideoHLSFiles
- }
- // ---------------------------------------------------------------------------
- async function isWebVideoAllowed (req: express.Request, res: express.Response) {
- const filename = basename(req.path)
- const file = await VideoFileModel.loadWithVideoByFilename(filename)
- if (!file) {
- logger.debug('Unknown static file %s to serve', req.originalUrl, { filename })
- res.sendStatus(HttpStatusCode.FORBIDDEN_403)
- return { allowed: false }
- }
- const video = await VideoModel.load(file.getVideo().id)
- return {
- file,
- video,
- allowed: await checkCanAccessVideoStaticFiles({ req, res, video, paramId: video.uuid })
- }
- }
- async function isHLSAllowed (req: express.Request, res: express.Response, videoUUID: string) {
- const filename = basename(req.path)
- const video = await VideoModel.loadWithFiles(videoUUID)
- if (!video) {
- logger.debug('Unknown static file %s to serve', req.originalUrl, { videoUUID })
- res.sendStatus(HttpStatusCode.FORBIDDEN_403)
- return { allowed: false }
- }
- const file = await VideoFileModel.loadByFilename(filename)
- return {
- file,
- video,
- playlist: video.getHLSPlaylist(),
- allowed: await checkCanAccessVideoStaticFiles({ req, res, video, paramId: video.uuid })
- }
- }
- function extractTokenOrDie (req: express.Request, res: express.Response) {
- const token = req.header('x-peertube-video-password') || req.query.videoFileToken || res.locals.oauth?.token.accessToken
- if (!token) {
- return res.fail({
- message: 'Video password header, video file token query parameter and bearer token are all missing', //
- status: HttpStatusCode.FORBIDDEN_403
- })
- }
- return token
- }
|