]>
Commit | Line | Data |
---|---|---|
3545e72c C |
1 | import express from 'express' |
2 | import { query } from 'express-validator' | |
3 | import LRUCache from 'lru-cache' | |
4 | import { basename, dirname } from 'path' | |
71e3e879 | 5 | import { exists, isUUIDValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc' |
3545e72c C |
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' | |
9ab330b9 | 10 | import { MStreamingPlaylist, MVideoFile, MVideoThumbnail } from '@server/types/models' |
3545e72c C |
11 | import { HttpStatusCode } from '@shared/models' |
12 | import { areValidationErrors, checkCanAccessVideoStaticFiles } from './shared' | |
13 | ||
9ab330b9 C |
14 | type LRUValue = { |
15 | allowed: boolean | |
16 | video?: MVideoThumbnail | |
17 | file?: MVideoFile | |
18 | playlist?: MStreamingPlaylist } | |
19 | ||
20 | const staticFileTokenBypass = new LRUCache<string, LRUValue>({ | |
3545e72c C |
21 | max: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.MAX_SIZE, |
22 | ttl: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.TTL | |
23 | }) | |
24 | ||
25 | const ensureCanAccessVideoPrivateWebTorrentFiles = [ | |
26 | query('videoFileToken').optional().custom(exists), | |
27 | ||
28 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | |
29 | if (areValidationErrors(req, res)) return | |
30 | ||
31 | const token = extractTokenOrDie(req, res) | |
32 | if (!token) return | |
33 | ||
34 | const cacheKey = token + '-' + req.originalUrl | |
35 | ||
36 | if (staticFileTokenBypass.has(cacheKey)) { | |
9ab330b9 C |
37 | const { allowed, file, video } = staticFileTokenBypass.get(cacheKey) |
38 | ||
39 | if (allowed === true) { | |
40 | res.locals.onlyVideo = video | |
41 | res.locals.videoFile = file | |
3545e72c | 42 | |
9ab330b9 C |
43 | return next() |
44 | } | |
3545e72c C |
45 | |
46 | return res.sendStatus(HttpStatusCode.FORBIDDEN_403) | |
47 | } | |
48 | ||
9ab330b9 C |
49 | const result = await isWebTorrentAllowed(req, res) |
50 | ||
51 | staticFileTokenBypass.set(cacheKey, result) | |
3545e72c | 52 | |
9ab330b9 | 53 | if (result.allowed !== true) return |
3545e72c | 54 | |
9ab330b9 C |
55 | res.locals.onlyVideo = result.video |
56 | res.locals.videoFile = result.file | |
3545e72c C |
57 | |
58 | return next() | |
59 | } | |
60 | ] | |
61 | ||
62 | const ensureCanAccessPrivateVideoHLSFiles = [ | |
71e3e879 C |
63 | query('videoFileToken') |
64 | .optional() | |
65 | .custom(exists), | |
66 | ||
67 | query('reinjectVideoFileToken') | |
68 | .optional() | |
69 | .customSanitizer(toBooleanOrNull) | |
70 | .isBoolean().withMessage('Should be a valid reinjectVideoFileToken boolean'), | |
3545e72c C |
71 | |
72 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | |
73 | if (areValidationErrors(req, res)) return | |
74 | ||
75 | const videoUUID = basename(dirname(req.originalUrl)) | |
76 | ||
77 | if (!isUUIDValid(videoUUID)) { | |
78 | logger.debug('Path does not contain valid video UUID to serve static file %s', req.originalUrl) | |
79 | ||
80 | return res.sendStatus(HttpStatusCode.FORBIDDEN_403) | |
81 | } | |
82 | ||
83 | const token = extractTokenOrDie(req, res) | |
84 | if (!token) return | |
85 | ||
86 | const cacheKey = token + '-' + videoUUID | |
87 | ||
88 | if (staticFileTokenBypass.has(cacheKey)) { | |
9ab330b9 | 89 | const { allowed, file, playlist, video } = staticFileTokenBypass.get(cacheKey) |
3545e72c | 90 | |
9ab330b9 C |
91 | if (allowed === true) { |
92 | res.locals.onlyVideo = video | |
93 | res.locals.videoFile = file | |
94 | res.locals.videoStreamingPlaylist = playlist | |
95 | ||
96 | return next() | |
97 | } | |
3545e72c C |
98 | |
99 | return res.sendStatus(HttpStatusCode.FORBIDDEN_403) | |
100 | } | |
101 | ||
9ab330b9 C |
102 | const result = await isHLSAllowed(req, res, videoUUID) |
103 | ||
104 | staticFileTokenBypass.set(cacheKey, result) | |
3545e72c | 105 | |
9ab330b9 | 106 | if (result.allowed !== true) return |
3545e72c | 107 | |
9ab330b9 C |
108 | res.locals.onlyVideo = result.video |
109 | res.locals.videoFile = result.file | |
110 | res.locals.videoStreamingPlaylist = result.playlist | |
3545e72c C |
111 | |
112 | return next() | |
113 | } | |
114 | ] | |
115 | ||
116 | export { | |
117 | ensureCanAccessVideoPrivateWebTorrentFiles, | |
118 | ensureCanAccessPrivateVideoHLSFiles | |
119 | } | |
120 | ||
121 | // --------------------------------------------------------------------------- | |
122 | ||
123 | async function isWebTorrentAllowed (req: express.Request, res: express.Response) { | |
124 | const filename = basename(req.path) | |
125 | ||
126 | const file = await VideoFileModel.loadWithVideoByFilename(filename) | |
127 | if (!file) { | |
128 | logger.debug('Unknown static file %s to serve', req.originalUrl, { filename }) | |
129 | ||
130 | res.sendStatus(HttpStatusCode.FORBIDDEN_403) | |
9ab330b9 | 131 | return { allowed: false } |
3545e72c C |
132 | } |
133 | ||
9ab330b9 | 134 | const video = await VideoModel.load(file.getVideo().id) |
3545e72c | 135 | |
9ab330b9 C |
136 | return { |
137 | file, | |
138 | video, | |
139 | allowed: await checkCanAccessVideoStaticFiles({ req, res, video, paramId: video.uuid }) | |
140 | } | |
3545e72c C |
141 | } |
142 | ||
143 | async function isHLSAllowed (req: express.Request, res: express.Response, videoUUID: string) { | |
9ab330b9 C |
144 | const filename = basename(req.path) |
145 | ||
146 | const video = await VideoModel.loadWithFiles(videoUUID) | |
3545e72c C |
147 | |
148 | if (!video) { | |
149 | logger.debug('Unknown static file %s to serve', req.originalUrl, { videoUUID }) | |
150 | ||
151 | res.sendStatus(HttpStatusCode.FORBIDDEN_403) | |
9ab330b9 | 152 | return { allowed: false } |
3545e72c C |
153 | } |
154 | ||
9ab330b9 C |
155 | const file = await VideoFileModel.loadByFilename(filename) |
156 | ||
157 | return { | |
158 | file, | |
159 | video, | |
160 | playlist: video.getHLSPlaylist(), | |
161 | allowed: await checkCanAccessVideoStaticFiles({ req, res, video, paramId: video.uuid }) | |
162 | } | |
3545e72c C |
163 | } |
164 | ||
165 | function extractTokenOrDie (req: express.Request, res: express.Response) { | |
166 | const token = res.locals.oauth?.token.accessToken || req.query.videoFileToken | |
167 | ||
168 | if (!token) { | |
169 | return res.fail({ | |
170 | message: 'Bearer token is missing in headers or video file token is missing in URL query parameters', | |
171 | status: HttpStatusCode.FORBIDDEN_403 | |
172 | }) | |
173 | } | |
174 | ||
175 | return token | |
176 | } |