diff options
Diffstat (limited to 'server/middlewares')
18 files changed, 374 insertions, 56 deletions
diff --git a/server/middlewares/auth.ts b/server/middlewares/auth.ts index 0eefa2a8e..39a7b2998 100644 --- a/server/middlewares/auth.ts +++ b/server/middlewares/auth.ts | |||
@@ -5,6 +5,7 @@ import { RunnerModel } from '@server/models/runner/runner' | |||
5 | import { HttpStatusCode } from '../../shared/models/http/http-error-codes' | 5 | import { HttpStatusCode } from '../../shared/models/http/http-error-codes' |
6 | import { logger } from '../helpers/logger' | 6 | import { logger } from '../helpers/logger' |
7 | import { handleOAuthAuthenticate } from '../lib/auth/oauth' | 7 | import { handleOAuthAuthenticate } from '../lib/auth/oauth' |
8 | import { ServerErrorCode } from '@shared/models' | ||
8 | 9 | ||
9 | function authenticate (req: express.Request, res: express.Response, next: express.NextFunction) { | 10 | function authenticate (req: express.Request, res: express.Response, next: express.NextFunction) { |
10 | handleOAuthAuthenticate(req, res) | 11 | handleOAuthAuthenticate(req, res) |
@@ -48,15 +49,23 @@ function authenticateSocket (socket: Socket, next: (err?: any) => void) { | |||
48 | .catch(err => logger.error('Cannot get access token.', { err })) | 49 | .catch(err => logger.error('Cannot get access token.', { err })) |
49 | } | 50 | } |
50 | 51 | ||
51 | function authenticatePromise (req: express.Request, res: express.Response) { | 52 | function authenticatePromise (options: { |
53 | req: express.Request | ||
54 | res: express.Response | ||
55 | errorMessage?: string | ||
56 | errorStatus?: HttpStatusCode | ||
57 | errorType?: ServerErrorCode | ||
58 | }) { | ||
59 | const { req, res, errorMessage = 'Not authenticated', errorStatus = HttpStatusCode.UNAUTHORIZED_401, errorType } = options | ||
52 | return new Promise<void>(resolve => { | 60 | return new Promise<void>(resolve => { |
53 | // Already authenticated? (or tried to) | 61 | // Already authenticated? (or tried to) |
54 | if (res.locals.oauth?.token.User) return resolve() | 62 | if (res.locals.oauth?.token.User) return resolve() |
55 | 63 | ||
56 | if (res.locals.authenticated === false) { | 64 | if (res.locals.authenticated === false) { |
57 | return res.fail({ | 65 | return res.fail({ |
58 | status: HttpStatusCode.UNAUTHORIZED_401, | 66 | status: errorStatus, |
59 | message: 'Not authenticated' | 67 | type: errorType, |
68 | message: errorMessage | ||
60 | }) | 69 | }) |
61 | } | 70 | } |
62 | 71 | ||
diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts index a0074cb24..a6dbba524 100644 --- a/server/middlewares/validators/config.ts +++ b/server/middlewares/validators/config.ts | |||
@@ -25,6 +25,7 @@ const customConfigUpdateValidator = [ | |||
25 | body('cache.previews.size').isInt(), | 25 | body('cache.previews.size').isInt(), |
26 | body('cache.captions.size').isInt(), | 26 | body('cache.captions.size').isInt(), |
27 | body('cache.torrents.size').isInt(), | 27 | body('cache.torrents.size').isInt(), |
28 | body('cache.storyboards.size').isInt(), | ||
28 | 29 | ||
29 | body('signup.enabled').isBoolean(), | 30 | body('signup.enabled').isBoolean(), |
30 | body('signup.limit').isInt(), | 31 | body('signup.limit').isInt(), |
@@ -58,7 +59,7 @@ const customConfigUpdateValidator = [ | |||
58 | 59 | ||
59 | body('transcoding.alwaysTranscodeOriginalResolution').isBoolean(), | 60 | body('transcoding.alwaysTranscodeOriginalResolution').isBoolean(), |
60 | 61 | ||
61 | body('transcoding.webtorrent.enabled').isBoolean(), | 62 | body('transcoding.webVideos.enabled').isBoolean(), |
62 | body('transcoding.hls.enabled').isBoolean(), | 63 | body('transcoding.hls.enabled').isBoolean(), |
63 | 64 | ||
64 | body('videoStudio.enabled').isBoolean(), | 65 | body('videoStudio.enabled').isBoolean(), |
@@ -152,8 +153,8 @@ function checkInvalidConfigIfEmailDisabled (customConfig: CustomConfig, res: exp | |||
152 | function checkInvalidTranscodingConfig (customConfig: CustomConfig, res: express.Response) { | 153 | function checkInvalidTranscodingConfig (customConfig: CustomConfig, res: express.Response) { |
153 | if (customConfig.transcoding.enabled === false) return true | 154 | if (customConfig.transcoding.enabled === false) return true |
154 | 155 | ||
155 | if (customConfig.transcoding.webtorrent.enabled === false && customConfig.transcoding.hls.enabled === false) { | 156 | if (customConfig.transcoding.webVideos.enabled === false && customConfig.transcoding.hls.enabled === false) { |
156 | res.fail({ message: 'You need to enable at least webtorrent transcoding or hls transcoding' }) | 157 | res.fail({ message: 'You need to enable at least web_videos transcoding or hls transcoding' }) |
157 | return false | 158 | return false |
158 | } | 159 | } |
159 | 160 | ||
diff --git a/server/middlewares/validators/shared/index.ts b/server/middlewares/validators/shared/index.ts index de98cd442..e5cff2dda 100644 --- a/server/middlewares/validators/shared/index.ts +++ b/server/middlewares/validators/shared/index.ts | |||
@@ -10,4 +10,5 @@ export * from './video-comments' | |||
10 | export * from './video-imports' | 10 | export * from './video-imports' |
11 | export * from './video-ownerships' | 11 | export * from './video-ownerships' |
12 | export * from './video-playlists' | 12 | export * from './video-playlists' |
13 | export * from './video-passwords' | ||
13 | export * from './videos' | 14 | export * from './videos' |
diff --git a/server/middlewares/validators/shared/video-passwords.ts b/server/middlewares/validators/shared/video-passwords.ts new file mode 100644 index 000000000..efcc95dc4 --- /dev/null +++ b/server/middlewares/validators/shared/video-passwords.ts | |||
@@ -0,0 +1,80 @@ | |||
1 | import express from 'express' | ||
2 | import { HttpStatusCode, UserRight, VideoPrivacy } from '@shared/models' | ||
3 | import { forceNumber } from '@shared/core-utils' | ||
4 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
5 | import { header } from 'express-validator' | ||
6 | import { getVideoWithAttributes } from '@server/helpers/video' | ||
7 | |||
8 | function isValidVideoPasswordHeader () { | ||
9 | return header('x-peertube-video-password') | ||
10 | .optional() | ||
11 | .isString() | ||
12 | } | ||
13 | |||
14 | function checkVideoIsPasswordProtected (res: express.Response) { | ||
15 | const video = getVideoWithAttributes(res) | ||
16 | if (video.privacy !== VideoPrivacy.PASSWORD_PROTECTED) { | ||
17 | res.fail({ | ||
18 | status: HttpStatusCode.BAD_REQUEST_400, | ||
19 | message: 'Video is not password protected' | ||
20 | }) | ||
21 | return false | ||
22 | } | ||
23 | |||
24 | return true | ||
25 | } | ||
26 | |||
27 | async function doesVideoPasswordExist (idArg: number | string, res: express.Response) { | ||
28 | const video = getVideoWithAttributes(res) | ||
29 | const id = forceNumber(idArg) | ||
30 | const videoPassword = await VideoPasswordModel.loadByIdAndVideo({ id, videoId: video.id }) | ||
31 | |||
32 | if (!videoPassword) { | ||
33 | res.fail({ | ||
34 | status: HttpStatusCode.NOT_FOUND_404, | ||
35 | message: 'Video password not found' | ||
36 | }) | ||
37 | return false | ||
38 | } | ||
39 | |||
40 | res.locals.videoPassword = videoPassword | ||
41 | |||
42 | return true | ||
43 | } | ||
44 | |||
45 | async function isVideoPasswordDeletable (res: express.Response) { | ||
46 | const user = res.locals.oauth.token.User | ||
47 | const userAccount = user.Account | ||
48 | const video = res.locals.videoAll | ||
49 | |||
50 | // Check if the user who did the request is able to delete the video passwords | ||
51 | if ( | ||
52 | user.hasRight(UserRight.UPDATE_ANY_VIDEO) === false && // Not a moderator | ||
53 | video.VideoChannel.accountId !== userAccount.id // Not the video owner | ||
54 | ) { | ||
55 | res.fail({ | ||
56 | status: HttpStatusCode.FORBIDDEN_403, | ||
57 | message: 'Cannot remove passwords of another user\'s video' | ||
58 | }) | ||
59 | return false | ||
60 | } | ||
61 | |||
62 | const passwordCount = await VideoPasswordModel.countByVideoId(video.id) | ||
63 | |||
64 | if (passwordCount <= 1) { | ||
65 | res.fail({ | ||
66 | status: HttpStatusCode.BAD_REQUEST_400, | ||
67 | message: 'Cannot delete the last password of the protected video' | ||
68 | }) | ||
69 | return false | ||
70 | } | ||
71 | |||
72 | return true | ||
73 | } | ||
74 | |||
75 | export { | ||
76 | isValidVideoPasswordHeader, | ||
77 | checkVideoIsPasswordProtected as isVideoPasswordProtected, | ||
78 | doesVideoPasswordExist, | ||
79 | isVideoPasswordDeletable | ||
80 | } | ||
diff --git a/server/middlewares/validators/shared/videos.ts b/server/middlewares/validators/shared/videos.ts index 0033a32ff..9a7497007 100644 --- a/server/middlewares/validators/shared/videos.ts +++ b/server/middlewares/validators/shared/videos.ts | |||
@@ -20,6 +20,8 @@ import { | |||
20 | MVideoWithRights | 20 | MVideoWithRights |
21 | } from '@server/types/models' | 21 | } from '@server/types/models' |
22 | import { HttpStatusCode, ServerErrorCode, UserRight, VideoPrivacy } from '@shared/models' | 22 | import { HttpStatusCode, ServerErrorCode, UserRight, VideoPrivacy } from '@shared/models' |
23 | import { VideoPasswordModel } from '@server/models/video/video-password' | ||
24 | import { exists } from '@server/helpers/custom-validators/misc' | ||
23 | 25 | ||
24 | async function doesVideoExist (id: number | string, res: Response, fetchType: VideoLoadType = 'all') { | 26 | async function doesVideoExist (id: number | string, res: Response, fetchType: VideoLoadType = 'all') { |
25 | const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined | 27 | const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined |
@@ -111,8 +113,12 @@ async function checkCanSeeVideo (options: { | |||
111 | }) { | 113 | }) { |
112 | const { req, res, video, paramId } = options | 114 | const { req, res, video, paramId } = options |
113 | 115 | ||
114 | if (video.requiresAuth({ urlParamId: paramId, checkBlacklist: true })) { | 116 | if (video.requiresUserAuth({ urlParamId: paramId, checkBlacklist: true })) { |
115 | return checkCanSeeAuthVideo(req, res, video) | 117 | return checkCanSeeUserAuthVideo({ req, res, video }) |
118 | } | ||
119 | |||
120 | if (video.privacy === VideoPrivacy.PASSWORD_PROTECTED) { | ||
121 | return checkCanSeePasswordProtectedVideo({ req, res, video }) | ||
116 | } | 122 | } |
117 | 123 | ||
118 | if (video.privacy === VideoPrivacy.UNLISTED || video.privacy === VideoPrivacy.PUBLIC) { | 124 | if (video.privacy === VideoPrivacy.UNLISTED || video.privacy === VideoPrivacy.PUBLIC) { |
@@ -122,7 +128,13 @@ async function checkCanSeeVideo (options: { | |||
122 | throw new Error('Unknown video privacy when checking video right ' + video.url) | 128 | throw new Error('Unknown video privacy when checking video right ' + video.url) |
123 | } | 129 | } |
124 | 130 | ||
125 | async function checkCanSeeAuthVideo (req: Request, res: Response, video: MVideoId | MVideoWithRights) { | 131 | async function checkCanSeeUserAuthVideo (options: { |
132 | req: Request | ||
133 | res: Response | ||
134 | video: MVideoId | MVideoWithRights | ||
135 | }) { | ||
136 | const { req, res, video } = options | ||
137 | |||
126 | const fail = () => { | 138 | const fail = () => { |
127 | res.fail({ | 139 | res.fail({ |
128 | status: HttpStatusCode.FORBIDDEN_403, | 140 | status: HttpStatusCode.FORBIDDEN_403, |
@@ -132,14 +144,12 @@ async function checkCanSeeAuthVideo (req: Request, res: Response, video: MVideoI | |||
132 | return false | 144 | return false |
133 | } | 145 | } |
134 | 146 | ||
135 | await authenticatePromise(req, res) | 147 | await authenticatePromise({ req, res }) |
136 | 148 | ||
137 | const user = res.locals.oauth?.token.User | 149 | const user = res.locals.oauth?.token.User |
138 | if (!user) return fail() | 150 | if (!user) return fail() |
139 | 151 | ||
140 | const videoWithRights = (video as MVideoWithRights).VideoChannel?.Account?.userId | 152 | const videoWithRights = await getVideoWithRights(video as MVideoWithRights) |
141 | ? video as MVideoWithRights | ||
142 | : await VideoModel.loadFull(video.id) | ||
143 | 153 | ||
144 | const privacy = videoWithRights.privacy | 154 | const privacy = videoWithRights.privacy |
145 | 155 | ||
@@ -148,16 +158,14 @@ async function checkCanSeeAuthVideo (req: Request, res: Response, video: MVideoI | |||
148 | return true | 158 | return true |
149 | } | 159 | } |
150 | 160 | ||
151 | const isOwnedByUser = videoWithRights.VideoChannel.Account.userId === user.id | ||
152 | |||
153 | if (videoWithRights.isBlacklisted()) { | 161 | if (videoWithRights.isBlacklisted()) { |
154 | if (isOwnedByUser || user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) return true | 162 | if (canUserAccessVideo(user, videoWithRights, UserRight.MANAGE_VIDEO_BLACKLIST)) return true |
155 | 163 | ||
156 | return fail() | 164 | return fail() |
157 | } | 165 | } |
158 | 166 | ||
159 | if (privacy === VideoPrivacy.PRIVATE || privacy === VideoPrivacy.UNLISTED) { | 167 | if (privacy === VideoPrivacy.PRIVATE || privacy === VideoPrivacy.UNLISTED) { |
160 | if (isOwnedByUser || user.hasRight(UserRight.SEE_ALL_VIDEOS)) return true | 168 | if (canUserAccessVideo(user, videoWithRights, UserRight.SEE_ALL_VIDEOS)) return true |
161 | 169 | ||
162 | return fail() | 170 | return fail() |
163 | } | 171 | } |
@@ -166,6 +174,59 @@ async function checkCanSeeAuthVideo (req: Request, res: Response, video: MVideoI | |||
166 | return fail() | 174 | return fail() |
167 | } | 175 | } |
168 | 176 | ||
177 | async function checkCanSeePasswordProtectedVideo (options: { | ||
178 | req: Request | ||
179 | res: Response | ||
180 | video: MVideo | ||
181 | }) { | ||
182 | const { req, res, video } = options | ||
183 | |||
184 | const videoWithRights = await getVideoWithRights(video as MVideoWithRights) | ||
185 | |||
186 | const videoPassword = req.header('x-peertube-video-password') | ||
187 | |||
188 | if (!exists(videoPassword)) { | ||
189 | const errorMessage = 'Please provide a password to access this password protected video' | ||
190 | const errorType = ServerErrorCode.VIDEO_REQUIRES_PASSWORD | ||
191 | |||
192 | if (req.header('authorization')) { | ||
193 | await authenticatePromise({ req, res, errorMessage, errorStatus: HttpStatusCode.FORBIDDEN_403, errorType }) | ||
194 | const user = res.locals.oauth?.token.User | ||
195 | |||
196 | if (canUserAccessVideo(user, videoWithRights, UserRight.SEE_ALL_VIDEOS)) return true | ||
197 | } | ||
198 | |||
199 | res.fail({ | ||
200 | status: HttpStatusCode.FORBIDDEN_403, | ||
201 | type: errorType, | ||
202 | message: errorMessage | ||
203 | }) | ||
204 | return false | ||
205 | } | ||
206 | |||
207 | if (await VideoPasswordModel.isACorrectPassword({ videoId: video.id, password: videoPassword })) return true | ||
208 | |||
209 | res.fail({ | ||
210 | status: HttpStatusCode.FORBIDDEN_403, | ||
211 | type: ServerErrorCode.INCORRECT_VIDEO_PASSWORD, | ||
212 | message: 'Incorrect video password. Access to the video is denied.' | ||
213 | }) | ||
214 | |||
215 | return false | ||
216 | } | ||
217 | |||
218 | function canUserAccessVideo (user: MUser, video: MVideoWithRights | MVideoAccountLight, right: UserRight) { | ||
219 | const isOwnedByUser = video.VideoChannel.Account.userId === user.id | ||
220 | |||
221 | return isOwnedByUser || user.hasRight(right) | ||
222 | } | ||
223 | |||
224 | async function getVideoWithRights (video: MVideoWithRights): Promise<MVideoWithRights> { | ||
225 | return video.VideoChannel?.Account?.userId | ||
226 | ? video | ||
227 | : VideoModel.loadFull(video.id) | ||
228 | } | ||
229 | |||
169 | // --------------------------------------------------------------------------- | 230 | // --------------------------------------------------------------------------- |
170 | 231 | ||
171 | async function checkCanAccessVideoStaticFiles (options: { | 232 | async function checkCanAccessVideoStaticFiles (options: { |
@@ -176,7 +237,7 @@ async function checkCanAccessVideoStaticFiles (options: { | |||
176 | }) { | 237 | }) { |
177 | const { video, req, res } = options | 238 | const { video, req, res } = options |
178 | 239 | ||
179 | if (res.locals.oauth?.token.User) { | 240 | if (res.locals.oauth?.token.User || exists(req.header('x-peertube-video-password'))) { |
180 | return checkCanSeeVideo(options) | 241 | return checkCanSeeVideo(options) |
181 | } | 242 | } |
182 | 243 | ||
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts index 959f663ac..07d6cba82 100644 --- a/server/middlewares/validators/sort.ts +++ b/server/middlewares/validators/sort.ts | |||
@@ -28,6 +28,7 @@ export const pluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.PLUGINS) | |||
28 | export const availablePluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.AVAILABLE_PLUGINS) | 28 | export const availablePluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.AVAILABLE_PLUGINS) |
29 | export const videoRedundanciesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_REDUNDANCIES) | 29 | export const videoRedundanciesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_REDUNDANCIES) |
30 | export const videoChannelSyncsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNEL_SYNCS) | 30 | export const videoChannelSyncsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNEL_SYNCS) |
31 | export const videoPasswordsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_PASSWORDS) | ||
31 | 32 | ||
32 | export const accountsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNT_FOLLOWERS) | 33 | export const accountsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNT_FOLLOWERS) |
33 | export const videoChannelsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.CHANNEL_FOLLOWERS) | 34 | export const videoChannelsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.CHANNEL_FOLLOWERS) |
diff --git a/server/middlewares/validators/static.ts b/server/middlewares/validators/static.ts index 9c2d890ba..86cc0a8d7 100644 --- a/server/middlewares/validators/static.ts +++ b/server/middlewares/validators/static.ts | |||
@@ -9,7 +9,7 @@ import { VideoModel } from '@server/models/video/video' | |||
9 | import { VideoFileModel } from '@server/models/video/video-file' | 9 | import { VideoFileModel } from '@server/models/video/video-file' |
10 | import { MStreamingPlaylist, MVideoFile, MVideoThumbnail } from '@server/types/models' | 10 | import { MStreamingPlaylist, MVideoFile, MVideoThumbnail } from '@server/types/models' |
11 | import { HttpStatusCode } from '@shared/models' | 11 | import { HttpStatusCode } from '@shared/models' |
12 | import { areValidationErrors, checkCanAccessVideoStaticFiles } from './shared' | 12 | import { areValidationErrors, checkCanAccessVideoStaticFiles, isValidVideoPasswordHeader } from './shared' |
13 | 13 | ||
14 | type LRUValue = { | 14 | type LRUValue = { |
15 | allowed: boolean | 15 | allowed: boolean |
@@ -22,9 +22,11 @@ const staticFileTokenBypass = new LRUCache<string, LRUValue>({ | |||
22 | ttl: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.TTL | 22 | ttl: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.TTL |
23 | }) | 23 | }) |
24 | 24 | ||
25 | const ensureCanAccessVideoPrivateWebTorrentFiles = [ | 25 | const ensureCanAccessVideoPrivateWebVideoFiles = [ |
26 | query('videoFileToken').optional().custom(exists), | 26 | query('videoFileToken').optional().custom(exists), |
27 | 27 | ||
28 | isValidVideoPasswordHeader(), | ||
29 | |||
28 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 30 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
29 | if (areValidationErrors(req, res)) return | 31 | if (areValidationErrors(req, res)) return |
30 | 32 | ||
@@ -46,7 +48,7 @@ const ensureCanAccessVideoPrivateWebTorrentFiles = [ | |||
46 | return res.sendStatus(HttpStatusCode.FORBIDDEN_403) | 48 | return res.sendStatus(HttpStatusCode.FORBIDDEN_403) |
47 | } | 49 | } |
48 | 50 | ||
49 | const result = await isWebTorrentAllowed(req, res) | 51 | const result = await isWebVideoAllowed(req, res) |
50 | 52 | ||
51 | staticFileTokenBypass.set(cacheKey, result) | 53 | staticFileTokenBypass.set(cacheKey, result) |
52 | 54 | ||
@@ -73,6 +75,8 @@ const ensureCanAccessPrivateVideoHLSFiles = [ | |||
73 | .optional() | 75 | .optional() |
74 | .customSanitizer(isSafePeerTubeFilenameWithoutExtension), | 76 | .customSanitizer(isSafePeerTubeFilenameWithoutExtension), |
75 | 77 | ||
78 | isValidVideoPasswordHeader(), | ||
79 | |||
76 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 80 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
77 | if (areValidationErrors(req, res)) return | 81 | if (areValidationErrors(req, res)) return |
78 | 82 | ||
@@ -118,13 +122,13 @@ const ensureCanAccessPrivateVideoHLSFiles = [ | |||
118 | ] | 122 | ] |
119 | 123 | ||
120 | export { | 124 | export { |
121 | ensureCanAccessVideoPrivateWebTorrentFiles, | 125 | ensureCanAccessVideoPrivateWebVideoFiles, |
122 | ensureCanAccessPrivateVideoHLSFiles | 126 | ensureCanAccessPrivateVideoHLSFiles |
123 | } | 127 | } |
124 | 128 | ||
125 | // --------------------------------------------------------------------------- | 129 | // --------------------------------------------------------------------------- |
126 | 130 | ||
127 | async function isWebTorrentAllowed (req: express.Request, res: express.Response) { | 131 | async function isWebVideoAllowed (req: express.Request, res: express.Response) { |
128 | const filename = basename(req.path) | 132 | const filename = basename(req.path) |
129 | 133 | ||
130 | const file = await VideoFileModel.loadWithVideoByFilename(filename) | 134 | const file = await VideoFileModel.loadWithVideoByFilename(filename) |
@@ -167,11 +171,11 @@ async function isHLSAllowed (req: express.Request, res: express.Response, videoU | |||
167 | } | 171 | } |
168 | 172 | ||
169 | function extractTokenOrDie (req: express.Request, res: express.Response) { | 173 | function extractTokenOrDie (req: express.Request, res: express.Response) { |
170 | const token = res.locals.oauth?.token.accessToken || req.query.videoFileToken | 174 | const token = req.header('x-peertube-video-password') || req.query.videoFileToken || res.locals.oauth?.token.accessToken |
171 | 175 | ||
172 | if (!token) { | 176 | if (!token) { |
173 | return res.fail({ | 177 | return res.fail({ |
174 | message: 'Bearer token is missing in headers or video file token is missing in URL query parameters', | 178 | message: 'Video password header, video file token query parameter and bearer token are all missing', // |
175 | status: HttpStatusCode.FORBIDDEN_403 | 179 | status: HttpStatusCode.FORBIDDEN_403 |
176 | }) | 180 | }) |
177 | } | 181 | } |
diff --git a/server/middlewares/validators/videos/index.ts b/server/middlewares/validators/videos/index.ts index d225dfe45..0c824c314 100644 --- a/server/middlewares/validators/videos/index.ts +++ b/server/middlewares/validators/videos/index.ts | |||
@@ -12,6 +12,8 @@ export * from './video-shares' | |||
12 | export * from './video-source' | 12 | export * from './video-source' |
13 | export * from './video-stats' | 13 | export * from './video-stats' |
14 | export * from './video-studio' | 14 | export * from './video-studio' |
15 | export * from './video-token' | ||
15 | export * from './video-transcoding' | 16 | export * from './video-transcoding' |
16 | export * from './videos' | 17 | export * from './videos' |
17 | export * from './video-channel-sync' | 18 | export * from './video-channel-sync' |
19 | export * from './video-passwords' | ||
diff --git a/server/middlewares/validators/videos/video-captions.ts b/server/middlewares/validators/videos/video-captions.ts index 72b2febc3..077a58d2e 100644 --- a/server/middlewares/validators/videos/video-captions.ts +++ b/server/middlewares/validators/videos/video-captions.ts | |||
@@ -10,7 +10,8 @@ import { | |||
10 | checkUserCanManageVideo, | 10 | checkUserCanManageVideo, |
11 | doesVideoCaptionExist, | 11 | doesVideoCaptionExist, |
12 | doesVideoExist, | 12 | doesVideoExist, |
13 | isValidVideoIdParam | 13 | isValidVideoIdParam, |
14 | isValidVideoPasswordHeader | ||
14 | } from '../shared' | 15 | } from '../shared' |
15 | 16 | ||
16 | const addVideoCaptionValidator = [ | 17 | const addVideoCaptionValidator = [ |
@@ -62,6 +63,8 @@ const deleteVideoCaptionValidator = [ | |||
62 | const listVideoCaptionsValidator = [ | 63 | const listVideoCaptionsValidator = [ |
63 | isValidVideoIdParam('videoId'), | 64 | isValidVideoIdParam('videoId'), |
64 | 65 | ||
66 | isValidVideoPasswordHeader(), | ||
67 | |||
65 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 68 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
66 | if (areValidationErrors(req, res)) return | 69 | if (areValidationErrors(req, res)) return |
67 | if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return | 70 | if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return |
diff --git a/server/middlewares/validators/videos/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts index 133feb7bd..70689b02e 100644 --- a/server/middlewares/validators/videos/video-comments.ts +++ b/server/middlewares/validators/videos/video-comments.ts | |||
@@ -14,7 +14,8 @@ import { | |||
14 | doesVideoCommentExist, | 14 | doesVideoCommentExist, |
15 | doesVideoCommentThreadExist, | 15 | doesVideoCommentThreadExist, |
16 | doesVideoExist, | 16 | doesVideoExist, |
17 | isValidVideoIdParam | 17 | isValidVideoIdParam, |
18 | isValidVideoPasswordHeader | ||
18 | } from '../shared' | 19 | } from '../shared' |
19 | 20 | ||
20 | const listVideoCommentsValidator = [ | 21 | const listVideoCommentsValidator = [ |
@@ -51,6 +52,7 @@ const listVideoCommentsValidator = [ | |||
51 | 52 | ||
52 | const listVideoCommentThreadsValidator = [ | 53 | const listVideoCommentThreadsValidator = [ |
53 | isValidVideoIdParam('videoId'), | 54 | isValidVideoIdParam('videoId'), |
55 | isValidVideoPasswordHeader(), | ||
54 | 56 | ||
55 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 57 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
56 | if (areValidationErrors(req, res)) return | 58 | if (areValidationErrors(req, res)) return |
@@ -67,6 +69,7 @@ const listVideoThreadCommentsValidator = [ | |||
67 | 69 | ||
68 | param('threadId') | 70 | param('threadId') |
69 | .custom(isIdValid), | 71 | .custom(isIdValid), |
72 | isValidVideoPasswordHeader(), | ||
70 | 73 | ||
71 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 74 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
72 | if (areValidationErrors(req, res)) return | 75 | if (areValidationErrors(req, res)) return |
@@ -84,6 +87,7 @@ const addVideoCommentThreadValidator = [ | |||
84 | 87 | ||
85 | body('text') | 88 | body('text') |
86 | .custom(isValidVideoCommentText), | 89 | .custom(isValidVideoCommentText), |
90 | isValidVideoPasswordHeader(), | ||
87 | 91 | ||
88 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 92 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
89 | if (areValidationErrors(req, res)) return | 93 | if (areValidationErrors(req, res)) return |
@@ -102,6 +106,7 @@ const addVideoCommentReplyValidator = [ | |||
102 | isValidVideoIdParam('videoId'), | 106 | isValidVideoIdParam('videoId'), |
103 | 107 | ||
104 | param('commentId').custom(isIdValid), | 108 | param('commentId').custom(isIdValid), |
109 | isValidVideoPasswordHeader(), | ||
105 | 110 | ||
106 | body('text').custom(isValidVideoCommentText), | 111 | body('text').custom(isValidVideoCommentText), |
107 | 112 | ||
diff --git a/server/middlewares/validators/videos/video-files.ts b/server/middlewares/validators/videos/video-files.ts index 92c5b9483..6c0ecda42 100644 --- a/server/middlewares/validators/videos/video-files.ts +++ b/server/middlewares/validators/videos/video-files.ts | |||
@@ -5,7 +5,7 @@ import { MVideo } from '@server/types/models' | |||
5 | import { HttpStatusCode } from '@shared/models' | 5 | import { HttpStatusCode } from '@shared/models' |
6 | import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' | 6 | import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' |
7 | 7 | ||
8 | const videoFilesDeleteWebTorrentValidator = [ | 8 | const videoFilesDeleteWebVideoValidator = [ |
9 | isValidVideoIdParam('id'), | 9 | isValidVideoIdParam('id'), |
10 | 10 | ||
11 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 11 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
@@ -16,17 +16,17 @@ const videoFilesDeleteWebTorrentValidator = [ | |||
16 | 16 | ||
17 | if (!checkLocalVideo(video, res)) return | 17 | if (!checkLocalVideo(video, res)) return |
18 | 18 | ||
19 | if (!video.hasWebTorrentFiles()) { | 19 | if (!video.hasWebVideoFiles()) { |
20 | return res.fail({ | 20 | return res.fail({ |
21 | status: HttpStatusCode.BAD_REQUEST_400, | 21 | status: HttpStatusCode.BAD_REQUEST_400, |
22 | message: 'This video does not have WebTorrent files' | 22 | message: 'This video does not have Web Video files' |
23 | }) | 23 | }) |
24 | } | 24 | } |
25 | 25 | ||
26 | if (!video.getHLSPlaylist()) { | 26 | if (!video.getHLSPlaylist()) { |
27 | return res.fail({ | 27 | return res.fail({ |
28 | status: HttpStatusCode.BAD_REQUEST_400, | 28 | status: HttpStatusCode.BAD_REQUEST_400, |
29 | message: 'Cannot delete WebTorrent files since this video does not have HLS playlist' | 29 | message: 'Cannot delete Web Video files since this video does not have HLS playlist' |
30 | }) | 30 | }) |
31 | } | 31 | } |
32 | 32 | ||
@@ -34,7 +34,7 @@ const videoFilesDeleteWebTorrentValidator = [ | |||
34 | } | 34 | } |
35 | ] | 35 | ] |
36 | 36 | ||
37 | const videoFilesDeleteWebTorrentFileValidator = [ | 37 | const videoFilesDeleteWebVideoFileValidator = [ |
38 | isValidVideoIdParam('id'), | 38 | isValidVideoIdParam('id'), |
39 | 39 | ||
40 | param('videoFileId') | 40 | param('videoFileId') |
@@ -52,14 +52,14 @@ const videoFilesDeleteWebTorrentFileValidator = [ | |||
52 | if (!files.find(f => f.id === +req.params.videoFileId)) { | 52 | if (!files.find(f => f.id === +req.params.videoFileId)) { |
53 | return res.fail({ | 53 | return res.fail({ |
54 | status: HttpStatusCode.NOT_FOUND_404, | 54 | status: HttpStatusCode.NOT_FOUND_404, |
55 | message: 'This video does not have this WebTorrent file id' | 55 | message: 'This video does not have this Web Video file id' |
56 | }) | 56 | }) |
57 | } | 57 | } |
58 | 58 | ||
59 | if (files.length === 1 && !video.getHLSPlaylist()) { | 59 | if (files.length === 1 && !video.getHLSPlaylist()) { |
60 | return res.fail({ | 60 | return res.fail({ |
61 | status: HttpStatusCode.BAD_REQUEST_400, | 61 | status: HttpStatusCode.BAD_REQUEST_400, |
62 | message: 'Cannot delete WebTorrent files since this video does not have HLS playlist' | 62 | message: 'Cannot delete Web Video files since this video does not have HLS playlist' |
63 | }) | 63 | }) |
64 | } | 64 | } |
65 | 65 | ||
@@ -87,10 +87,10 @@ const videoFilesDeleteHLSValidator = [ | |||
87 | }) | 87 | }) |
88 | } | 88 | } |
89 | 89 | ||
90 | if (!video.hasWebTorrentFiles()) { | 90 | if (!video.hasWebVideoFiles()) { |
91 | return res.fail({ | 91 | return res.fail({ |
92 | status: HttpStatusCode.BAD_REQUEST_400, | 92 | status: HttpStatusCode.BAD_REQUEST_400, |
93 | message: 'Cannot delete HLS playlist since this video does not have WebTorrent files' | 93 | message: 'Cannot delete HLS playlist since this video does not have Web Video files' |
94 | }) | 94 | }) |
95 | } | 95 | } |
96 | 96 | ||
@@ -128,10 +128,10 @@ const videoFilesDeleteHLSFileValidator = [ | |||
128 | } | 128 | } |
129 | 129 | ||
130 | // Last file to delete | 130 | // Last file to delete |
131 | if (hlsFiles.length === 1 && !video.hasWebTorrentFiles()) { | 131 | if (hlsFiles.length === 1 && !video.hasWebVideoFiles()) { |
132 | return res.fail({ | 132 | return res.fail({ |
133 | status: HttpStatusCode.BAD_REQUEST_400, | 133 | status: HttpStatusCode.BAD_REQUEST_400, |
134 | message: 'Cannot delete last HLS playlist file since this video does not have WebTorrent files' | 134 | message: 'Cannot delete last HLS playlist file since this video does not have Web Video files' |
135 | }) | 135 | }) |
136 | } | 136 | } |
137 | 137 | ||
@@ -140,8 +140,8 @@ const videoFilesDeleteHLSFileValidator = [ | |||
140 | ] | 140 | ] |
141 | 141 | ||
142 | export { | 142 | export { |
143 | videoFilesDeleteWebTorrentValidator, | 143 | videoFilesDeleteWebVideoValidator, |
144 | videoFilesDeleteWebTorrentFileValidator, | 144 | videoFilesDeleteWebVideoFileValidator, |
145 | 145 | ||
146 | videoFilesDeleteHLSValidator, | 146 | videoFilesDeleteHLSValidator, |
147 | videoFilesDeleteHLSFileValidator | 147 | videoFilesDeleteHLSFileValidator |
diff --git a/server/middlewares/validators/videos/video-imports.ts b/server/middlewares/validators/videos/video-imports.ts index 72442aeb6..a1cb65b70 100644 --- a/server/middlewares/validators/videos/video-imports.ts +++ b/server/middlewares/validators/videos/video-imports.ts | |||
@@ -9,7 +9,11 @@ import { HttpStatusCode, UserRight, VideoImportState } from '@shared/models' | |||
9 | import { VideoImportCreate } from '@shared/models/videos/import/video-import-create.model' | 9 | import { VideoImportCreate } from '@shared/models/videos/import/video-import-create.model' |
10 | import { isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc' | 10 | import { isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc' |
11 | import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports' | 11 | import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports' |
12 | import { isVideoMagnetUriValid, isVideoNameValid } from '../../../helpers/custom-validators/videos' | 12 | import { |
13 | isValidPasswordProtectedPrivacy, | ||
14 | isVideoMagnetUriValid, | ||
15 | isVideoNameValid | ||
16 | } from '../../../helpers/custom-validators/videos' | ||
13 | import { cleanUpReqFiles } from '../../../helpers/express-utils' | 17 | import { cleanUpReqFiles } from '../../../helpers/express-utils' |
14 | import { logger } from '../../../helpers/logger' | 18 | import { logger } from '../../../helpers/logger' |
15 | import { CONFIG } from '../../../initializers/config' | 19 | import { CONFIG } from '../../../initializers/config' |
@@ -38,6 +42,10 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([ | |||
38 | .custom(isVideoNameValid).withMessage( | 42 | .custom(isVideoNameValid).withMessage( |
39 | `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long` | 43 | `Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long` |
40 | ), | 44 | ), |
45 | body('videoPasswords') | ||
46 | .optional() | ||
47 | .isArray() | ||
48 | .withMessage('Video passwords should be an array.'), | ||
41 | 49 | ||
42 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 50 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
43 | const user = res.locals.oauth.token.User | 51 | const user = res.locals.oauth.token.User |
@@ -45,6 +53,8 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([ | |||
45 | 53 | ||
46 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) | 54 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) |
47 | 55 | ||
56 | if (!isValidPasswordProtectedPrivacy(req, res)) return cleanUpReqFiles(req) | ||
57 | |||
48 | if (CONFIG.IMPORT.VIDEOS.HTTP.ENABLED !== true && req.body.targetUrl) { | 58 | if (CONFIG.IMPORT.VIDEOS.HTTP.ENABLED !== true && req.body.targetUrl) { |
49 | cleanUpReqFiles(req) | 59 | cleanUpReqFiles(req) |
50 | 60 | ||
diff --git a/server/middlewares/validators/videos/video-live.ts b/server/middlewares/validators/videos/video-live.ts index 2aff831a8..ec69a3011 100644 --- a/server/middlewares/validators/videos/video-live.ts +++ b/server/middlewares/validators/videos/video-live.ts | |||
@@ -17,7 +17,7 @@ import { | |||
17 | VideoState | 17 | VideoState |
18 | } from '@shared/models' | 18 | } from '@shared/models' |
19 | import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' | 19 | import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' |
20 | import { isVideoNameValid, isVideoPrivacyValid } from '../../../helpers/custom-validators/videos' | 20 | import { isValidPasswordProtectedPrivacy, isVideoNameValid, isVideoReplayPrivacyValid } from '../../../helpers/custom-validators/videos' |
21 | import { cleanUpReqFiles } from '../../../helpers/express-utils' | 21 | import { cleanUpReqFiles } from '../../../helpers/express-utils' |
22 | import { logger } from '../../../helpers/logger' | 22 | import { logger } from '../../../helpers/logger' |
23 | import { CONFIG } from '../../../initializers/config' | 23 | import { CONFIG } from '../../../initializers/config' |
@@ -69,7 +69,7 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([ | |||
69 | body('replaySettings.privacy') | 69 | body('replaySettings.privacy') |
70 | .optional() | 70 | .optional() |
71 | .customSanitizer(toIntOrNull) | 71 | .customSanitizer(toIntOrNull) |
72 | .custom(isVideoPrivacyValid), | 72 | .custom(isVideoReplayPrivacyValid), |
73 | 73 | ||
74 | body('permanentLive') | 74 | body('permanentLive') |
75 | .optional() | 75 | .optional() |
@@ -81,9 +81,16 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([ | |||
81 | .customSanitizer(toIntOrNull) | 81 | .customSanitizer(toIntOrNull) |
82 | .custom(isLiveLatencyModeValid), | 82 | .custom(isLiveLatencyModeValid), |
83 | 83 | ||
84 | body('videoPasswords') | ||
85 | .optional() | ||
86 | .isArray() | ||
87 | .withMessage('Video passwords should be an array.'), | ||
88 | |||
84 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 89 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
85 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) | 90 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) |
86 | 91 | ||
92 | if (!isValidPasswordProtectedPrivacy(req, res)) return cleanUpReqFiles(req) | ||
93 | |||
87 | if (CONFIG.LIVE.ENABLED !== true) { | 94 | if (CONFIG.LIVE.ENABLED !== true) { |
88 | cleanUpReqFiles(req) | 95 | cleanUpReqFiles(req) |
89 | 96 | ||
@@ -170,7 +177,7 @@ const videoLiveUpdateValidator = [ | |||
170 | body('replaySettings.privacy') | 177 | body('replaySettings.privacy') |
171 | .optional() | 178 | .optional() |
172 | .customSanitizer(toIntOrNull) | 179 | .customSanitizer(toIntOrNull) |
173 | .custom(isVideoPrivacyValid), | 180 | .custom(isVideoReplayPrivacyValid), |
174 | 181 | ||
175 | body('latencyMode') | 182 | body('latencyMode') |
176 | .optional() | 183 | .optional() |
diff --git a/server/middlewares/validators/videos/video-passwords.ts b/server/middlewares/validators/videos/video-passwords.ts new file mode 100644 index 000000000..200e496f6 --- /dev/null +++ b/server/middlewares/validators/videos/video-passwords.ts | |||
@@ -0,0 +1,77 @@ | |||
1 | import express from 'express' | ||
2 | import { | ||
3 | areValidationErrors, | ||
4 | doesVideoExist, | ||
5 | isVideoPasswordProtected, | ||
6 | isValidVideoIdParam, | ||
7 | doesVideoPasswordExist, | ||
8 | isVideoPasswordDeletable, | ||
9 | checkUserCanManageVideo | ||
10 | } from '../shared' | ||
11 | import { body, param } from 'express-validator' | ||
12 | import { isIdValid } from '@server/helpers/custom-validators/misc' | ||
13 | import { isValidPasswordProtectedPrivacy } from '@server/helpers/custom-validators/videos' | ||
14 | import { UserRight } from '@shared/models' | ||
15 | |||
16 | const listVideoPasswordValidator = [ | ||
17 | isValidVideoIdParam('videoId'), | ||
18 | |||
19 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
20 | if (areValidationErrors(req, res)) return | ||
21 | |||
22 | if (!await doesVideoExist(req.params.videoId, res)) return | ||
23 | if (!isVideoPasswordProtected(res)) return | ||
24 | |||
25 | // Check if the user who did the request is able to access video password list | ||
26 | const user = res.locals.oauth.token.User | ||
27 | if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.SEE_ALL_VIDEOS, res)) return | ||
28 | |||
29 | return next() | ||
30 | } | ||
31 | ] | ||
32 | |||
33 | const updateVideoPasswordListValidator = [ | ||
34 | body('passwords') | ||
35 | .optional() | ||
36 | .isArray() | ||
37 | .withMessage('Video passwords should be an array.'), | ||
38 | |||
39 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
40 | if (areValidationErrors(req, res)) return | ||
41 | |||
42 | if (!await doesVideoExist(req.params.videoId, res)) return | ||
43 | if (!isValidPasswordProtectedPrivacy(req, res)) return | ||
44 | |||
45 | // Check if the user who did the request is able to update video passwords | ||
46 | const user = res.locals.oauth.token.User | ||
47 | if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return | ||
48 | |||
49 | return next() | ||
50 | } | ||
51 | ] | ||
52 | |||
53 | const removeVideoPasswordValidator = [ | ||
54 | isValidVideoIdParam('videoId'), | ||
55 | |||
56 | param('passwordId') | ||
57 | .custom(isIdValid), | ||
58 | |||
59 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
60 | if (areValidationErrors(req, res)) return | ||
61 | |||
62 | if (!await doesVideoExist(req.params.videoId, res)) return | ||
63 | if (!isVideoPasswordProtected(res)) return | ||
64 | if (!await doesVideoPasswordExist(req.params.passwordId, res)) return | ||
65 | if (!await isVideoPasswordDeletable(res)) return | ||
66 | |||
67 | return next() | ||
68 | } | ||
69 | ] | ||
70 | |||
71 | // --------------------------------------------------------------------------- | ||
72 | |||
73 | export { | ||
74 | listVideoPasswordValidator, | ||
75 | updateVideoPasswordListValidator, | ||
76 | removeVideoPasswordValidator | ||
77 | } | ||
diff --git a/server/middlewares/validators/videos/video-playlists.ts b/server/middlewares/validators/videos/video-playlists.ts index c631a16f8..95a5ba63a 100644 --- a/server/middlewares/validators/videos/video-playlists.ts +++ b/server/middlewares/validators/videos/video-playlists.ts | |||
@@ -153,7 +153,7 @@ const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => { | |||
153 | } | 153 | } |
154 | 154 | ||
155 | if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) { | 155 | if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) { |
156 | await authenticatePromise(req, res) | 156 | await authenticatePromise({ req, res }) |
157 | 157 | ||
158 | const user = res.locals.oauth ? res.locals.oauth.token.User : null | 158 | const user = res.locals.oauth ? res.locals.oauth.token.User : null |
159 | 159 | ||
diff --git a/server/middlewares/validators/videos/video-rates.ts b/server/middlewares/validators/videos/video-rates.ts index 275634d5b..c837b047b 100644 --- a/server/middlewares/validators/videos/video-rates.ts +++ b/server/middlewares/validators/videos/video-rates.ts | |||
@@ -7,13 +7,14 @@ import { isIdValid } from '../../../helpers/custom-validators/misc' | |||
7 | import { isRatingValid } from '../../../helpers/custom-validators/video-rates' | 7 | import { isRatingValid } from '../../../helpers/custom-validators/video-rates' |
8 | import { isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos' | 8 | import { isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos' |
9 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | 9 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' |
10 | import { areValidationErrors, checkCanSeeVideo, doesVideoExist, isValidVideoIdParam } from '../shared' | 10 | import { areValidationErrors, checkCanSeeVideo, doesVideoExist, isValidVideoIdParam, isValidVideoPasswordHeader } from '../shared' |
11 | 11 | ||
12 | const videoUpdateRateValidator = [ | 12 | const videoUpdateRateValidator = [ |
13 | isValidVideoIdParam('id'), | 13 | isValidVideoIdParam('id'), |
14 | 14 | ||
15 | body('rating') | 15 | body('rating') |
16 | .custom(isVideoRatingTypeValid), | 16 | .custom(isVideoRatingTypeValid), |
17 | isValidVideoPasswordHeader(), | ||
17 | 18 | ||
18 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 19 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
19 | if (areValidationErrors(req, res)) return | 20 | if (areValidationErrors(req, res)) return |
diff --git a/server/middlewares/validators/videos/video-token.ts b/server/middlewares/validators/videos/video-token.ts new file mode 100644 index 000000000..d4253e21d --- /dev/null +++ b/server/middlewares/validators/videos/video-token.ts | |||
@@ -0,0 +1,24 @@ | |||
1 | import express from 'express' | ||
2 | import { VideoPrivacy } from '../../../../shared/models/videos' | ||
3 | import { HttpStatusCode } from '@shared/models' | ||
4 | import { exists } from '@server/helpers/custom-validators/misc' | ||
5 | |||
6 | const videoFileTokenValidator = [ | ||
7 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
8 | const video = res.locals.onlyVideo | ||
9 | if (video.privacy !== VideoPrivacy.PASSWORD_PROTECTED && !exists(res.locals.oauth.token.User)) { | ||
10 | return res.fail({ | ||
11 | status: HttpStatusCode.UNAUTHORIZED_401, | ||
12 | message: 'Not authenticated' | ||
13 | }) | ||
14 | } | ||
15 | |||
16 | return next() | ||
17 | } | ||
18 | ] | ||
19 | |||
20 | // --------------------------------------------------------------------------- | ||
21 | |||
22 | export { | ||
23 | videoFileTokenValidator | ||
24 | } | ||
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index 794e1d4f1..b39d13a23 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts | |||
@@ -2,6 +2,7 @@ import express from 'express' | |||
2 | import { body, header, param, query, ValidationChain } from 'express-validator' | 2 | import { body, header, param, query, ValidationChain } from 'express-validator' |
3 | import { isTestInstance } from '@server/helpers/core-utils' | 3 | import { isTestInstance } from '@server/helpers/core-utils' |
4 | import { getResumableUploadPath } from '@server/helpers/upload' | 4 | import { getResumableUploadPath } from '@server/helpers/upload' |
5 | import { uploadx } from '@server/lib/uploadx' | ||
5 | import { Redis } from '@server/lib/redis' | 6 | import { Redis } from '@server/lib/redis' |
6 | import { getServerActor } from '@server/models/application/application' | 7 | import { getServerActor } from '@server/models/application/application' |
7 | import { ExpressPromiseHandler } from '@server/types/express-handler' | 8 | import { ExpressPromiseHandler } from '@server/types/express-handler' |
@@ -23,6 +24,7 @@ import { isBooleanBothQueryValid, isNumberArray, isStringArray } from '../../../ | |||
23 | import { | 24 | import { |
24 | areVideoTagsValid, | 25 | areVideoTagsValid, |
25 | isScheduleVideoUpdatePrivacyValid, | 26 | isScheduleVideoUpdatePrivacyValid, |
27 | isValidPasswordProtectedPrivacy, | ||
26 | isVideoCategoryValid, | 28 | isVideoCategoryValid, |
27 | isVideoDescriptionValid, | 29 | isVideoDescriptionValid, |
28 | isVideoFileMimeTypeValid, | 30 | isVideoFileMimeTypeValid, |
@@ -39,7 +41,6 @@ import { | |||
39 | } from '../../../helpers/custom-validators/videos' | 41 | } from '../../../helpers/custom-validators/videos' |
40 | import { cleanUpReqFiles } from '../../../helpers/express-utils' | 42 | import { cleanUpReqFiles } from '../../../helpers/express-utils' |
41 | import { logger } from '../../../helpers/logger' | 43 | import { logger } from '../../../helpers/logger' |
42 | import { deleteFileAndCatch } from '../../../helpers/utils' | ||
43 | import { getVideoWithAttributes } from '../../../helpers/video' | 44 | import { getVideoWithAttributes } from '../../../helpers/video' |
44 | import { CONFIG } from '../../../initializers/config' | 45 | import { CONFIG } from '../../../initializers/config' |
45 | import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants' | 46 | import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants' |
@@ -55,7 +56,8 @@ import { | |||
55 | doesVideoChannelOfAccountExist, | 56 | doesVideoChannelOfAccountExist, |
56 | doesVideoExist, | 57 | doesVideoExist, |
57 | doesVideoFileOfVideoExist, | 58 | doesVideoFileOfVideoExist, |
58 | isValidVideoIdParam | 59 | isValidVideoIdParam, |
60 | isValidVideoPasswordHeader | ||
59 | } from '../shared' | 61 | } from '../shared' |
60 | 62 | ||
61 | const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ | 63 | const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ |
@@ -70,6 +72,10 @@ const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ | |||
70 | body('channelId') | 72 | body('channelId') |
71 | .customSanitizer(toIntOrNull) | 73 | .customSanitizer(toIntOrNull) |
72 | .custom(isIdValid), | 74 | .custom(isIdValid), |
75 | body('videoPasswords') | ||
76 | .optional() | ||
77 | .isArray() | ||
78 | .withMessage('Video passwords should be an array.'), | ||
73 | 79 | ||
74 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 80 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
75 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) | 81 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) |
@@ -81,6 +87,8 @@ const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ | |||
81 | return cleanUpReqFiles(req) | 87 | return cleanUpReqFiles(req) |
82 | } | 88 | } |
83 | 89 | ||
90 | if (!isValidPasswordProtectedPrivacy(req, res)) return cleanUpReqFiles(req) | ||
91 | |||
84 | try { | 92 | try { |
85 | if (!videoFile.duration) await addDurationToVideo(videoFile) | 93 | if (!videoFile.duration) await addDurationToVideo(videoFile) |
86 | } catch (err) { | 94 | } catch (err) { |
@@ -107,7 +115,7 @@ const videosAddResumableValidator = [ | |||
107 | const user = res.locals.oauth.token.User | 115 | const user = res.locals.oauth.token.User |
108 | const body: express.CustomUploadXFile<express.UploadXFileMetadata> = req.body | 116 | const body: express.CustomUploadXFile<express.UploadXFileMetadata> = req.body |
109 | const file = { ...body, duration: undefined, path: getResumableUploadPath(body.name), filename: body.metadata.filename } | 117 | const file = { ...body, duration: undefined, path: getResumableUploadPath(body.name), filename: body.metadata.filename } |
110 | const cleanup = () => deleteFileAndCatch(file.path) | 118 | const cleanup = () => uploadx.storage.delete(file).catch(err => logger.error('Cannot delete the file %s', file.name, { err })) |
111 | 119 | ||
112 | const uploadId = req.query.upload_id | 120 | const uploadId = req.query.upload_id |
113 | const sessionExists = await Redis.Instance.doesUploadSessionExist(uploadId) | 121 | const sessionExists = await Redis.Instance.doesUploadSessionExist(uploadId) |
@@ -124,11 +132,15 @@ const videosAddResumableValidator = [ | |||
124 | }) | 132 | }) |
125 | } | 133 | } |
126 | 134 | ||
127 | if (isTestInstance()) { | 135 | const videoStillExists = await VideoModel.load(sessionResponse.video.id) |
128 | res.setHeader('x-resumable-upload-cached', 'true') | 136 | |
129 | } | 137 | if (videoStillExists) { |
138 | if (isTestInstance()) { | ||
139 | res.setHeader('x-resumable-upload-cached', 'true') | ||
140 | } | ||
130 | 141 | ||
131 | return res.json(sessionResponse) | 142 | return res.json(sessionResponse) |
143 | } | ||
132 | } | 144 | } |
133 | 145 | ||
134 | await Redis.Instance.setUploadSession(uploadId) | 146 | await Redis.Instance.setUploadSession(uploadId) |
@@ -174,6 +186,10 @@ const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([ | |||
174 | body('channelId') | 186 | body('channelId') |
175 | .customSanitizer(toIntOrNull) | 187 | .customSanitizer(toIntOrNull) |
176 | .custom(isIdValid), | 188 | .custom(isIdValid), |
189 | body('videoPasswords') | ||
190 | .optional() | ||
191 | .isArray() | ||
192 | .withMessage('Video passwords should be an array.'), | ||
177 | 193 | ||
178 | header('x-upload-content-length') | 194 | header('x-upload-content-length') |
179 | .isNumeric() | 195 | .isNumeric() |
@@ -205,10 +221,14 @@ const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([ | |||
205 | const files = { videofile: [ videoFileMetadata ] } | 221 | const files = { videofile: [ videoFileMetadata ] } |
206 | if (!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFileMetadata.size, files })) return cleanup() | 222 | if (!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFileMetadata.size, files })) return cleanup() |
207 | 223 | ||
208 | // multer required unsetting the Content-Type, now we can set it for node-uploadx | 224 | if (!isValidPasswordProtectedPrivacy(req, res)) return cleanup() |
225 | |||
226 | // Multer required unsetting the Content-Type, now we can set it for node-uploadx | ||
209 | req.headers['content-type'] = 'application/json; charset=utf-8' | 227 | req.headers['content-type'] = 'application/json; charset=utf-8' |
210 | // place previewfile in metadata so that uploadx saves it in .META | 228 | |
229 | // Place thumbnail/previewfile in metadata so that uploadx saves it in .META | ||
211 | if (req.files?.['previewfile']) req.body.previewfile = req.files['previewfile'] | 230 | if (req.files?.['previewfile']) req.body.previewfile = req.files['previewfile'] |
231 | if (req.files?.['thumbnailfile']) req.body.thumbnailfile = req.files['thumbnailfile'] | ||
212 | 232 | ||
213 | return next() | 233 | return next() |
214 | } | 234 | } |
@@ -227,12 +247,18 @@ const videosUpdateValidator = getCommonVideoEditAttributes().concat([ | |||
227 | .optional() | 247 | .optional() |
228 | .customSanitizer(toIntOrNull) | 248 | .customSanitizer(toIntOrNull) |
229 | .custom(isIdValid), | 249 | .custom(isIdValid), |
250 | body('videoPasswords') | ||
251 | .optional() | ||
252 | .isArray() | ||
253 | .withMessage('Video passwords should be an array.'), | ||
230 | 254 | ||
231 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 255 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
232 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) | 256 | if (areValidationErrors(req, res)) return cleanUpReqFiles(req) |
233 | if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req) | 257 | if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req) |
234 | if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req) | 258 | if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req) |
235 | 259 | ||
260 | if (!isValidPasswordProtectedPrivacy(req, res)) return cleanUpReqFiles(req) | ||
261 | |||
236 | const video = getVideoWithAttributes(res) | 262 | const video = getVideoWithAttributes(res) |
237 | if (video.isLive && video.privacy !== req.body.privacy && video.state !== VideoState.WAITING_FOR_LIVE) { | 263 | if (video.isLive && video.privacy !== req.body.privacy && video.state !== VideoState.WAITING_FOR_LIVE) { |
238 | return res.fail({ message: 'Cannot update privacy of a live that has already started' }) | 264 | return res.fail({ message: 'Cannot update privacy of a live that has already started' }) |
@@ -281,6 +307,8 @@ const videosCustomGetValidator = (fetchType: 'for-api' | 'all' | 'only-video' | | |||
281 | return [ | 307 | return [ |
282 | isValidVideoIdParam('id'), | 308 | isValidVideoIdParam('id'), |
283 | 309 | ||
310 | isValidVideoPasswordHeader(), | ||
311 | |||
284 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 312 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
285 | if (areValidationErrors(req, res)) return | 313 | if (areValidationErrors(req, res)) return |
286 | if (!await doesVideoExist(req.params.id, res, fetchType)) return | 314 | if (!await doesVideoExist(req.params.id, res, fetchType)) return |
@@ -478,10 +506,14 @@ const commonVideosFiltersValidator = [ | |||
478 | .optional() | 506 | .optional() |
479 | .customSanitizer(toBooleanOrNull) | 507 | .customSanitizer(toBooleanOrNull) |
480 | .custom(isBooleanValid).withMessage('Should have a valid hasHLSFiles boolean'), | 508 | .custom(isBooleanValid).withMessage('Should have a valid hasHLSFiles boolean'), |
481 | query('hasWebtorrentFiles') | 509 | query('hasWebtorrentFiles') // TODO: remove in v7 |
482 | .optional() | 510 | .optional() |
483 | .customSanitizer(toBooleanOrNull) | 511 | .customSanitizer(toBooleanOrNull) |
484 | .custom(isBooleanValid).withMessage('Should have a valid hasWebtorrentFiles boolean'), | 512 | .custom(isBooleanValid).withMessage('Should have a valid hasWebtorrentFiles boolean'), |
513 | query('hasWebVideoFiles') | ||
514 | .optional() | ||
515 | .customSanitizer(toBooleanOrNull) | ||
516 | .custom(isBooleanValid).withMessage('Should have a valid hasWebVideoFiles boolean'), | ||
485 | query('skipCount') | 517 | query('skipCount') |
486 | .optional() | 518 | .optional() |
487 | .customSanitizer(toBooleanOrNull) | 519 | .customSanitizer(toBooleanOrNull) |