diff options
author | Chocobozzz <me@florianbigard.com> | 2022-10-12 16:09:02 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2022-10-24 14:48:24 +0200 |
commit | 3545e72c686ff1725bbdfd8d16d693e2f4aa75a3 (patch) | |
tree | e7f1d12ef5dae1e1142c3a8d0b681c1dbbb0de10 /server/middlewares/validators/static.ts | |
parent | 38a3ccc7f8ad0ea94362b58c732af7c387ab46be (diff) | |
download | PeerTube-3545e72c686ff1725bbdfd8d16d693e2f4aa75a3.tar.gz PeerTube-3545e72c686ff1725bbdfd8d16d693e2f4aa75a3.tar.zst PeerTube-3545e72c686ff1725bbdfd8d16d693e2f4aa75a3.zip |
Put private videos under a specific subdirectory
Diffstat (limited to 'server/middlewares/validators/static.ts')
-rw-r--r-- | server/middlewares/validators/static.ts | 131 |
1 files changed, 131 insertions, 0 deletions
diff --git a/server/middlewares/validators/static.ts b/server/middlewares/validators/static.ts new file mode 100644 index 000000000..ff9e6ae6e --- /dev/null +++ b/server/middlewares/validators/static.ts | |||
@@ -0,0 +1,131 @@ | |||
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 | } | ||