]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/middlewares/validators/static.ts
Put private videos under a specific subdirectory
[github/Chocobozzz/PeerTube.git] / server / middlewares / validators / static.ts
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, isUUIDValid } from '@server/helpers/custom-validators/misc'
6 import { logger } from '@server/helpers/logger'
7 import { LRU_CACHE } from '@server/initializers/constants'
8 import { VideoModel } from '@server/models/video/video'
9 import { VideoFileModel } from '@server/models/video/video-file'
10 import { HttpStatusCode } from '@shared/models'
11 import { areValidationErrors, checkCanAccessVideoStaticFiles } from './shared'
12
13 const staticFileTokenBypass = new LRUCache<string, boolean>({
14 max: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.MAX_SIZE,
15 ttl: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.TTL
16 })
17
18 const ensureCanAccessVideoPrivateWebTorrentFiles = [
19 query('videoFileToken').optional().custom(exists),
20
21 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
22 if (areValidationErrors(req, res)) return
23
24 const token = extractTokenOrDie(req, res)
25 if (!token) return
26
27 const cacheKey = token + '-' + req.originalUrl
28
29 if (staticFileTokenBypass.has(cacheKey)) {
30 const allowedFromCache = staticFileTokenBypass.get(cacheKey)
31
32 if (allowedFromCache === true) return next()
33
34 return res.sendStatus(HttpStatusCode.FORBIDDEN_403)
35 }
36
37 const allowed = await isWebTorrentAllowed(req, res)
38
39 staticFileTokenBypass.set(cacheKey, allowed)
40
41 if (allowed !== true) return
42
43 return next()
44 }
45 ]
46
47 const ensureCanAccessPrivateVideoHLSFiles = [
48 query('videoFileToken').optional().custom(exists),
49
50 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
51 if (areValidationErrors(req, res)) return
52
53 const videoUUID = basename(dirname(req.originalUrl))
54
55 if (!isUUIDValid(videoUUID)) {
56 logger.debug('Path does not contain valid video UUID to serve static file %s', req.originalUrl)
57
58 return res.sendStatus(HttpStatusCode.FORBIDDEN_403)
59 }
60
61 const token = extractTokenOrDie(req, res)
62 if (!token) return
63
64 const cacheKey = token + '-' + videoUUID
65
66 if (staticFileTokenBypass.has(cacheKey)) {
67 const allowedFromCache = staticFileTokenBypass.get(cacheKey)
68
69 if (allowedFromCache === true) return next()
70
71 return res.sendStatus(HttpStatusCode.FORBIDDEN_403)
72 }
73
74 const allowed = await isHLSAllowed(req, res, videoUUID)
75
76 staticFileTokenBypass.set(cacheKey, allowed)
77
78 if (allowed !== true) return
79
80 return next()
81 }
82 ]
83
84 export {
85 ensureCanAccessVideoPrivateWebTorrentFiles,
86 ensureCanAccessPrivateVideoHLSFiles
87 }
88
89 // ---------------------------------------------------------------------------
90
91 async function isWebTorrentAllowed (req: express.Request, res: express.Response) {
92 const filename = basename(req.path)
93
94 const file = await VideoFileModel.loadWithVideoByFilename(filename)
95 if (!file) {
96 logger.debug('Unknown static file %s to serve', req.originalUrl, { filename })
97
98 res.sendStatus(HttpStatusCode.FORBIDDEN_403)
99 return false
100 }
101
102 const video = file.getVideo()
103
104 return checkCanAccessVideoStaticFiles({ req, res, video, paramId: video.uuid })
105 }
106
107 async function isHLSAllowed (req: express.Request, res: express.Response, videoUUID: string) {
108 const video = await VideoModel.load(videoUUID)
109
110 if (!video) {
111 logger.debug('Unknown static file %s to serve', req.originalUrl, { videoUUID })
112
113 res.sendStatus(HttpStatusCode.FORBIDDEN_403)
114 return false
115 }
116
117 return checkCanAccessVideoStaticFiles({ req, res, video, paramId: video.uuid })
118 }
119
120 function extractTokenOrDie (req: express.Request, res: express.Response) {
121 const token = res.locals.oauth?.token.accessToken || req.query.videoFileToken
122
123 if (!token) {
124 return res.fail({
125 message: 'Bearer token is missing in headers or video file token is missing in URL query parameters',
126 status: HttpStatusCode.FORBIDDEN_403
127 })
128 }
129
130 return token
131 }