]>
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' | |
d7ce9dca | 5 | import { exists, isSafePeerTubeFilenameWithoutExtension, 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 | 71 | |
d7ce9dca C |
72 | query('playlistName') |
73 | .optional() | |
74 | .customSanitizer(isSafePeerTubeFilenameWithoutExtension), | |
75 | ||
3545e72c C |
76 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
77 | if (areValidationErrors(req, res)) return | |
78 | ||
79 | const videoUUID = basename(dirname(req.originalUrl)) | |
80 | ||
81 | if (!isUUIDValid(videoUUID)) { | |
82 | logger.debug('Path does not contain valid video UUID to serve static file %s', req.originalUrl) | |
83 | ||
84 | return res.sendStatus(HttpStatusCode.FORBIDDEN_403) | |
85 | } | |
86 | ||
87 | const token = extractTokenOrDie(req, res) | |
88 | if (!token) return | |
89 | ||
90 | const cacheKey = token + '-' + videoUUID | |
91 | ||
92 | if (staticFileTokenBypass.has(cacheKey)) { | |
9ab330b9 | 93 | const { allowed, file, playlist, video } = staticFileTokenBypass.get(cacheKey) |
3545e72c | 94 | |
9ab330b9 C |
95 | if (allowed === true) { |
96 | res.locals.onlyVideo = video | |
97 | res.locals.videoFile = file | |
98 | res.locals.videoStreamingPlaylist = playlist | |
99 | ||
100 | return next() | |
101 | } | |
3545e72c C |
102 | |
103 | return res.sendStatus(HttpStatusCode.FORBIDDEN_403) | |
104 | } | |
105 | ||
9ab330b9 C |
106 | const result = await isHLSAllowed(req, res, videoUUID) |
107 | ||
108 | staticFileTokenBypass.set(cacheKey, result) | |
3545e72c | 109 | |
9ab330b9 | 110 | if (result.allowed !== true) return |
3545e72c | 111 | |
9ab330b9 C |
112 | res.locals.onlyVideo = result.video |
113 | res.locals.videoFile = result.file | |
114 | res.locals.videoStreamingPlaylist = result.playlist | |
3545e72c C |
115 | |
116 | return next() | |
117 | } | |
118 | ] | |
119 | ||
120 | export { | |
121 | ensureCanAccessVideoPrivateWebTorrentFiles, | |
122 | ensureCanAccessPrivateVideoHLSFiles | |
123 | } | |
124 | ||
125 | // --------------------------------------------------------------------------- | |
126 | ||
127 | async function isWebTorrentAllowed (req: express.Request, res: express.Response) { | |
128 | const filename = basename(req.path) | |
129 | ||
130 | const file = await VideoFileModel.loadWithVideoByFilename(filename) | |
131 | if (!file) { | |
132 | logger.debug('Unknown static file %s to serve', req.originalUrl, { filename }) | |
133 | ||
134 | res.sendStatus(HttpStatusCode.FORBIDDEN_403) | |
9ab330b9 | 135 | return { allowed: false } |
3545e72c C |
136 | } |
137 | ||
9ab330b9 | 138 | const video = await VideoModel.load(file.getVideo().id) |
3545e72c | 139 | |
9ab330b9 C |
140 | return { |
141 | file, | |
142 | video, | |
143 | allowed: await checkCanAccessVideoStaticFiles({ req, res, video, paramId: video.uuid }) | |
144 | } | |
3545e72c C |
145 | } |
146 | ||
147 | async function isHLSAllowed (req: express.Request, res: express.Response, videoUUID: string) { | |
9ab330b9 C |
148 | const filename = basename(req.path) |
149 | ||
150 | const video = await VideoModel.loadWithFiles(videoUUID) | |
3545e72c C |
151 | |
152 | if (!video) { | |
153 | logger.debug('Unknown static file %s to serve', req.originalUrl, { videoUUID }) | |
154 | ||
155 | res.sendStatus(HttpStatusCode.FORBIDDEN_403) | |
9ab330b9 | 156 | return { allowed: false } |
3545e72c C |
157 | } |
158 | ||
9ab330b9 C |
159 | const file = await VideoFileModel.loadByFilename(filename) |
160 | ||
161 | return { | |
162 | file, | |
163 | video, | |
164 | playlist: video.getHLSPlaylist(), | |
165 | allowed: await checkCanAccessVideoStaticFiles({ req, res, video, paramId: video.uuid }) | |
166 | } | |
3545e72c C |
167 | } |
168 | ||
169 | function extractTokenOrDie (req: express.Request, res: express.Response) { | |
170 | const token = res.locals.oauth?.token.accessToken || req.query.videoFileToken | |
171 | ||
172 | if (!token) { | |
173 | return res.fail({ | |
174 | message: 'Bearer token is missing in headers or video file token is missing in URL query parameters', | |
175 | status: HttpStatusCode.FORBIDDEN_403 | |
176 | }) | |
177 | } | |
178 | ||
179 | return token | |
180 | } |