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