static.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. import express from 'express'
  2. import { query } from 'express-validator'
  3. import { LRUCache } from 'lru-cache'
  4. import { basename, dirname } from 'path'
  5. import { exists, isSafePeerTubeFilenameWithoutExtension, isUUIDValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc.js'
  6. import { logger } from '@server/helpers/logger.js'
  7. import { LRU_CACHE } from '@server/initializers/constants.js'
  8. import { VideoModel } from '@server/models/video/video.js'
  9. import { VideoFileModel } from '@server/models/video/video-file.js'
  10. import { MStreamingPlaylist, MVideoFile, MVideoThumbnail } from '@server/types/models/index.js'
  11. import { HttpStatusCode } from '@peertube/peertube-models'
  12. import { areValidationErrors, checkCanAccessVideoStaticFiles, isValidVideoPasswordHeader } from './shared/index.js'
  13. type LRUValue = {
  14. allowed: boolean
  15. video?: MVideoThumbnail
  16. file?: MVideoFile
  17. playlist?: MStreamingPlaylist }
  18. const staticFileTokenBypass = new LRUCache<string, LRUValue>({
  19. max: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.MAX_SIZE,
  20. ttl: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.TTL
  21. })
  22. const ensureCanAccessVideoPrivateWebVideoFiles = [
  23. query('videoFileToken').optional().custom(exists),
  24. isValidVideoPasswordHeader(),
  25. async (req: express.Request, res: express.Response, next: express.NextFunction) => {
  26. if (areValidationErrors(req, res)) return
  27. const token = extractTokenOrDie(req, res)
  28. if (!token) return
  29. const cacheKey = token + '-' + req.originalUrl
  30. if (staticFileTokenBypass.has(cacheKey)) {
  31. const { allowed, file, video } = staticFileTokenBypass.get(cacheKey)
  32. if (allowed === true) {
  33. res.locals.onlyVideo = video
  34. res.locals.videoFile = file
  35. return next()
  36. }
  37. return res.sendStatus(HttpStatusCode.FORBIDDEN_403)
  38. }
  39. const result = await isWebVideoAllowed(req, res)
  40. staticFileTokenBypass.set(cacheKey, result)
  41. if (result.allowed !== true) return
  42. res.locals.onlyVideo = result.video
  43. res.locals.videoFile = result.file
  44. return next()
  45. }
  46. ]
  47. const ensureCanAccessPrivateVideoHLSFiles = [
  48. query('videoFileToken')
  49. .optional()
  50. .custom(exists),
  51. query('reinjectVideoFileToken')
  52. .optional()
  53. .customSanitizer(toBooleanOrNull)
  54. .isBoolean().withMessage('Should be a valid reinjectVideoFileToken boolean'),
  55. query('playlistName')
  56. .optional()
  57. .customSanitizer(isSafePeerTubeFilenameWithoutExtension),
  58. isValidVideoPasswordHeader(),
  59. async (req: express.Request, res: express.Response, next: express.NextFunction) => {
  60. if (areValidationErrors(req, res)) return
  61. const videoUUID = basename(dirname(req.originalUrl))
  62. if (!isUUIDValid(videoUUID)) {
  63. logger.debug('Path does not contain valid video UUID to serve static file %s', req.originalUrl)
  64. return res.sendStatus(HttpStatusCode.FORBIDDEN_403)
  65. }
  66. const token = extractTokenOrDie(req, res)
  67. if (!token) return
  68. const cacheKey = token + '-' + videoUUID
  69. if (staticFileTokenBypass.has(cacheKey)) {
  70. const { allowed, file, playlist, video } = staticFileTokenBypass.get(cacheKey)
  71. if (allowed === true) {
  72. res.locals.onlyVideo = video
  73. res.locals.videoFile = file
  74. res.locals.videoStreamingPlaylist = playlist
  75. return next()
  76. }
  77. return res.sendStatus(HttpStatusCode.FORBIDDEN_403)
  78. }
  79. const result = await isHLSAllowed(req, res, videoUUID)
  80. staticFileTokenBypass.set(cacheKey, result)
  81. if (result.allowed !== true) return
  82. res.locals.onlyVideo = result.video
  83. res.locals.videoFile = result.file
  84. res.locals.videoStreamingPlaylist = result.playlist
  85. return next()
  86. }
  87. ]
  88. export {
  89. ensureCanAccessVideoPrivateWebVideoFiles,
  90. ensureCanAccessPrivateVideoHLSFiles
  91. }
  92. // ---------------------------------------------------------------------------
  93. async function isWebVideoAllowed (req: express.Request, res: express.Response) {
  94. const filename = basename(req.path)
  95. const file = await VideoFileModel.loadWithVideoByFilename(filename)
  96. if (!file) {
  97. logger.debug('Unknown static file %s to serve', req.originalUrl, { filename })
  98. res.sendStatus(HttpStatusCode.FORBIDDEN_403)
  99. return { allowed: false }
  100. }
  101. const video = await VideoModel.load(file.getVideo().id)
  102. return {
  103. file,
  104. video,
  105. allowed: await checkCanAccessVideoStaticFiles({ req, res, video, paramId: video.uuid })
  106. }
  107. }
  108. async function isHLSAllowed (req: express.Request, res: express.Response, videoUUID: string) {
  109. const filename = basename(req.path)
  110. const video = await VideoModel.loadWithFiles(videoUUID)
  111. if (!video) {
  112. logger.debug('Unknown static file %s to serve', req.originalUrl, { videoUUID })
  113. res.sendStatus(HttpStatusCode.FORBIDDEN_403)
  114. return { allowed: false }
  115. }
  116. const file = await VideoFileModel.loadByFilename(filename)
  117. return {
  118. file,
  119. video,
  120. playlist: video.getHLSPlaylist(),
  121. allowed: await checkCanAccessVideoStaticFiles({ req, res, video, paramId: video.uuid })
  122. }
  123. }
  124. function extractTokenOrDie (req: express.Request, res: express.Response) {
  125. const token = req.header('x-peertube-video-password') || req.query.videoFileToken || res.locals.oauth?.token.accessToken
  126. if (!token) {
  127. return res.fail({
  128. message: 'Video password header, video file token query parameter and bearer token are all missing', //
  129. status: HttpStatusCode.FORBIDDEN_403
  130. })
  131. }
  132. return token
  133. }