diff options
author | Wicklow <123956049+wickloww@users.noreply.github.com> | 2023-06-29 07:48:55 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-06-29 09:48:55 +0200 |
commit | 40346ead2b0b7afa475aef057d3673b6c7574b7a (patch) | |
tree | 24ffdc23c3a9d987334842e0d400b5bd44500cf7 /server/tests/api/videos | |
parent | ae22c59f14d0d553f60b281948b6c232c2aca178 (diff) | |
download | PeerTube-40346ead2b0b7afa475aef057d3673b6c7574b7a.tar.gz PeerTube-40346ead2b0b7afa475aef057d3673b6c7574b7a.tar.zst PeerTube-40346ead2b0b7afa475aef057d3673b6c7574b7a.zip |
Feature/password protected videos (#5836)
* Add server endpoints
* Refactoring test suites
* Update server and add openapi documentation
* fix compliation and tests
* upload/import password protected video on client
* add server error code
* Add video password to update resolver
* add custom message when sharing pw protected video
* improve confirm component
* Add new alert in component
* Add ability to watch protected video on client
* Cannot have password protected replay privacy
* Add migration
* Add tests
* update after review
* Update check params tests
* Add live videos test
* Add more filter test
* Update static file privacy test
* Update object storage tests
* Add test on feeds
* Add missing word
* Fix tests
* Fix tests on live videos
* add embed support on password protected videos
* fix style
* Correcting data leaks
* Unable to add password protected privacy on replay
* Updated code based on review comments
* fix validator and command
* Updated code based on review comments
Diffstat (limited to 'server/tests/api/videos')
-rw-r--r-- | server/tests/api/videos/video-passwords.ts | 97 | ||||
-rw-r--r-- | server/tests/api/videos/video-playlists.ts | 17 | ||||
-rw-r--r-- | server/tests/api/videos/video-static-file-privacy.ts | 127 |
3 files changed, 232 insertions, 9 deletions
diff --git a/server/tests/api/videos/video-passwords.ts b/server/tests/api/videos/video-passwords.ts new file mode 100644 index 000000000..e01a93a4d --- /dev/null +++ b/server/tests/api/videos/video-passwords.ts | |||
@@ -0,0 +1,97 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { | ||
5 | cleanupTests, | ||
6 | createSingleServer, | ||
7 | VideoPasswordsCommand, | ||
8 | PeerTubeServer, | ||
9 | setAccessTokensToServers, | ||
10 | setDefaultAccountAvatar, | ||
11 | setDefaultChannelAvatar | ||
12 | } from '@shared/server-commands' | ||
13 | import { VideoPrivacy } from '@shared/models' | ||
14 | |||
15 | describe('Test video passwords', function () { | ||
16 | let server: PeerTubeServer | ||
17 | let videoUUID: string | ||
18 | |||
19 | let userAccessTokenServer1: string | ||
20 | |||
21 | let videoPasswords: string[] = [] | ||
22 | let command: VideoPasswordsCommand | ||
23 | |||
24 | before(async function () { | ||
25 | this.timeout(30000) | ||
26 | |||
27 | server = await createSingleServer(1) | ||
28 | |||
29 | await setAccessTokensToServers([ server ]) | ||
30 | |||
31 | for (let i = 0; i < 10; i++) { | ||
32 | videoPasswords.push(`password ${i + 1}`) | ||
33 | } | ||
34 | const { uuid } = await server.videos.upload({ attributes: { privacy: VideoPrivacy.PASSWORD_PROTECTED, videoPasswords } }) | ||
35 | videoUUID = uuid | ||
36 | |||
37 | await setDefaultChannelAvatar(server) | ||
38 | await setDefaultAccountAvatar(server) | ||
39 | |||
40 | userAccessTokenServer1 = await server.users.generateUserAndToken('user1') | ||
41 | await setDefaultChannelAvatar(server, 'user1_channel') | ||
42 | await setDefaultAccountAvatar(server, userAccessTokenServer1) | ||
43 | |||
44 | command = server.videoPasswords | ||
45 | }) | ||
46 | |||
47 | it('Should list video passwords', async function () { | ||
48 | const body = await command.list({ videoId: videoUUID }) | ||
49 | |||
50 | expect(body.total).to.equal(10) | ||
51 | expect(body.data).to.be.an('array') | ||
52 | expect(body.data).to.have.lengthOf(10) | ||
53 | }) | ||
54 | |||
55 | it('Should filter passwords on this video', async function () { | ||
56 | const body = await command.list({ videoId: videoUUID, count: 2, start: 3, sort: 'createdAt' }) | ||
57 | |||
58 | expect(body.total).to.equal(10) | ||
59 | expect(body.data).to.be.an('array') | ||
60 | expect(body.data).to.have.lengthOf(2) | ||
61 | expect(body.data[0].password).to.equal('password 4') | ||
62 | expect(body.data[1].password).to.equal('password 5') | ||
63 | }) | ||
64 | |||
65 | it('Should update password for this video', async function () { | ||
66 | videoPasswords = [ 'my super new password 1', 'my super new password 2' ] | ||
67 | |||
68 | await command.updateAll({ videoId: videoUUID, passwords: videoPasswords }) | ||
69 | const body = await command.list({ videoId: videoUUID }) | ||
70 | expect(body.total).to.equal(2) | ||
71 | expect(body.data).to.be.an('array') | ||
72 | expect(body.data).to.have.lengthOf(2) | ||
73 | expect(body.data[0].password).to.equal('my super new password 2') | ||
74 | expect(body.data[1].password).to.equal('my super new password 1') | ||
75 | }) | ||
76 | |||
77 | it('Should delete one password', async function () { | ||
78 | { | ||
79 | const body = await command.list({ videoId: videoUUID }) | ||
80 | expect(body.total).to.equal(2) | ||
81 | expect(body.data).to.be.an('array') | ||
82 | expect(body.data).to.have.lengthOf(2) | ||
83 | await command.remove({ id: body.data[0].id, videoId: videoUUID }) | ||
84 | } | ||
85 | { | ||
86 | const body = await command.list({ videoId: videoUUID }) | ||
87 | |||
88 | expect(body.total).to.equal(1) | ||
89 | expect(body.data).to.be.an('array') | ||
90 | expect(body.data).to.have.lengthOf(1) | ||
91 | } | ||
92 | }) | ||
93 | |||
94 | after(async function () { | ||
95 | await cleanupTests([ server ]) | ||
96 | }) | ||
97 | }) | ||
diff --git a/server/tests/api/videos/video-playlists.ts b/server/tests/api/videos/video-playlists.ts index d9c5bdf16..9277b49f4 100644 --- a/server/tests/api/videos/video-playlists.ts +++ b/server/tests/api/videos/video-playlists.ts | |||
@@ -474,7 +474,7 @@ describe('Test video playlists', function () { | |||
474 | await servers[1].playlists.get({ playlistId: unlistedPlaylist.id, expectedStatus: 404 }) | 474 | await servers[1].playlists.get({ playlistId: unlistedPlaylist.id, expectedStatus: 404 }) |
475 | }) | 475 | }) |
476 | 476 | ||
477 | it('Should get unlisted plyaylist using uuid or shortUUID', async function () { | 477 | it('Should get unlisted playlist using uuid or shortUUID', async function () { |
478 | await servers[1].playlists.get({ playlistId: unlistedPlaylist.uuid }) | 478 | await servers[1].playlists.get({ playlistId: unlistedPlaylist.uuid }) |
479 | await servers[1].playlists.get({ playlistId: unlistedPlaylist.shortUUID }) | 479 | await servers[1].playlists.get({ playlistId: unlistedPlaylist.shortUUID }) |
480 | }) | 480 | }) |
@@ -686,7 +686,7 @@ describe('Test video playlists', function () { | |||
686 | await waitJobs(servers) | 686 | await waitJobs(servers) |
687 | }) | 687 | }) |
688 | 688 | ||
689 | it('Should update the element type if the video is private', async function () { | 689 | it('Should update the element type if the video is private/password protected', async function () { |
690 | this.timeout(20000) | 690 | this.timeout(20000) |
691 | 691 | ||
692 | const name = 'video 89' | 692 | const name = 'video 89' |
@@ -703,6 +703,19 @@ describe('Test video playlists', function () { | |||
703 | } | 703 | } |
704 | 704 | ||
705 | { | 705 | { |
706 | await servers[0].videos.update({ | ||
707 | id: video1, | ||
708 | attributes: { privacy: VideoPrivacy.PASSWORD_PROTECTED, videoPasswords: [ 'password' ] } | ||
709 | }) | ||
710 | await waitJobs(servers) | ||
711 | |||
712 | await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) | ||
713 | await checkPlaylistElementType(groupWithoutToken1, playlistServer1UUID2, VideoPlaylistElementType.PRIVATE, position, name, 3) | ||
714 | await checkPlaylistElementType(group1, playlistServer1UUID2, VideoPlaylistElementType.PRIVATE, position, name, 3) | ||
715 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.DELETED, position, name, 3) | ||
716 | } | ||
717 | |||
718 | { | ||
706 | await servers[0].videos.update({ id: video1, attributes: { privacy: VideoPrivacy.PUBLIC } }) | 719 | await servers[0].videos.update({ id: video1, attributes: { privacy: VideoPrivacy.PUBLIC } }) |
707 | await waitJobs(servers) | 720 | await waitJobs(servers) |
708 | 721 | ||
diff --git a/server/tests/api/videos/video-static-file-privacy.ts b/server/tests/api/videos/video-static-file-privacy.ts index 542848533..ec4c697db 100644 --- a/server/tests/api/videos/video-static-file-privacy.ts +++ b/server/tests/api/videos/video-static-file-privacy.ts | |||
@@ -90,7 +90,7 @@ describe('Test video static file privacy', function () { | |||
90 | } | 90 | } |
91 | } | 91 | } |
92 | 92 | ||
93 | it('Should upload a private/internal video and have a private static path', async function () { | 93 | it('Should upload a private/internal/password protected video and have a private static path', async function () { |
94 | this.timeout(120000) | 94 | this.timeout(120000) |
95 | 95 | ||
96 | for (const privacy of [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]) { | 96 | for (const privacy of [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]) { |
@@ -99,6 +99,15 @@ describe('Test video static file privacy', function () { | |||
99 | 99 | ||
100 | await checkPrivateFiles(uuid) | 100 | await checkPrivateFiles(uuid) |
101 | } | 101 | } |
102 | |||
103 | const { uuid } = await server.videos.quickUpload({ | ||
104 | name: 'video', | ||
105 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
106 | videoPasswords: [ 'my super password' ] | ||
107 | }) | ||
108 | await waitJobs([ server ]) | ||
109 | |||
110 | await checkPrivateFiles(uuid) | ||
102 | }) | 111 | }) |
103 | 112 | ||
104 | it('Should upload a public video and update it as private/internal to have a private static path', async function () { | 113 | it('Should upload a public video and update it as private/internal to have a private static path', async function () { |
@@ -185,8 +194,9 @@ describe('Test video static file privacy', function () { | |||
185 | expectedStatus: HttpStatusCode | 194 | expectedStatus: HttpStatusCode |
186 | token: string | 195 | token: string |
187 | videoFileToken: string | 196 | videoFileToken: string |
197 | videoPassword?: string | ||
188 | }) { | 198 | }) { |
189 | const { id, expectedStatus, token, videoFileToken } = options | 199 | const { id, expectedStatus, token, videoFileToken, videoPassword } = options |
190 | 200 | ||
191 | const video = await server.videos.getWithToken({ id }) | 201 | const video = await server.videos.getWithToken({ id }) |
192 | 202 | ||
@@ -196,6 +206,12 @@ describe('Test video static file privacy', function () { | |||
196 | 206 | ||
197 | await makeRawRequest({ url: file.fileUrl, query: { videoFileToken }, expectedStatus }) | 207 | await makeRawRequest({ url: file.fileUrl, query: { videoFileToken }, expectedStatus }) |
198 | await makeRawRequest({ url: file.fileDownloadUrl, query: { videoFileToken }, expectedStatus }) | 208 | await makeRawRequest({ url: file.fileDownloadUrl, query: { videoFileToken }, expectedStatus }) |
209 | |||
210 | if (videoPassword) { | ||
211 | const headers = { 'x-peertube-video-password': videoPassword } | ||
212 | await makeRawRequest({ url: file.fileUrl, headers, expectedStatus }) | ||
213 | await makeRawRequest({ url: file.fileDownloadUrl, headers, expectedStatus }) | ||
214 | } | ||
199 | } | 215 | } |
200 | 216 | ||
201 | const hls = video.streamingPlaylists[0] | 217 | const hls = video.streamingPlaylists[0] |
@@ -204,6 +220,12 @@ describe('Test video static file privacy', function () { | |||
204 | 220 | ||
205 | await makeRawRequest({ url: hls.playlistUrl, query: { videoFileToken }, expectedStatus }) | 221 | await makeRawRequest({ url: hls.playlistUrl, query: { videoFileToken }, expectedStatus }) |
206 | await makeRawRequest({ url: hls.segmentsSha256Url, query: { videoFileToken }, expectedStatus }) | 222 | await makeRawRequest({ url: hls.segmentsSha256Url, query: { videoFileToken }, expectedStatus }) |
223 | |||
224 | if (videoPassword) { | ||
225 | const headers = { 'x-peertube-video-password': videoPassword } | ||
226 | await makeRawRequest({ url: hls.playlistUrl, token: null, headers, expectedStatus }) | ||
227 | await makeRawRequest({ url: hls.segmentsSha256Url, token: null, headers, expectedStatus }) | ||
228 | } | ||
207 | } | 229 | } |
208 | 230 | ||
209 | before(async function () { | 231 | before(async function () { |
@@ -216,13 +238,53 @@ describe('Test video static file privacy', function () { | |||
216 | it('Should not be able to access a private video files without OAuth token and file token', async function () { | 238 | it('Should not be able to access a private video files without OAuth token and file token', async function () { |
217 | this.timeout(120000) | 239 | this.timeout(120000) |
218 | 240 | ||
219 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.INTERNAL }) | 241 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) |
220 | await waitJobs([ server ]) | 242 | await waitJobs([ server ]) |
221 | 243 | ||
222 | await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403, token: null, videoFileToken: null }) | 244 | await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.FORBIDDEN_403, token: null, videoFileToken: null }) |
223 | }) | 245 | }) |
224 | 246 | ||
225 | it('Should not be able to access an internal video files without appropriate OAuth token and file token', async function () { | 247 | it('Should not be able to access password protected video files without OAuth token, file token and password', async function () { |
248 | this.timeout(120000) | ||
249 | const videoPassword = 'my super password' | ||
250 | |||
251 | const { uuid } = await server.videos.quickUpload({ | ||
252 | name: 'password protected video', | ||
253 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
254 | videoPasswords: [ videoPassword ] | ||
255 | }) | ||
256 | await waitJobs([ server ]) | ||
257 | |||
258 | await checkVideoFiles({ | ||
259 | id: uuid, | ||
260 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
261 | token: null, | ||
262 | videoFileToken: null, | ||
263 | videoPassword: null | ||
264 | }) | ||
265 | }) | ||
266 | |||
267 | it('Should not be able to access an password video files with incorrect OAuth token, file token and password', async function () { | ||
268 | this.timeout(120000) | ||
269 | const videoPassword = 'my super password' | ||
270 | |||
271 | const { uuid } = await server.videos.quickUpload({ | ||
272 | name: 'password protected video', | ||
273 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
274 | videoPasswords: [ videoPassword ] | ||
275 | }) | ||
276 | await waitJobs([ server ]) | ||
277 | |||
278 | await checkVideoFiles({ | ||
279 | id: uuid, | ||
280 | expectedStatus: HttpStatusCode.FORBIDDEN_403, | ||
281 | token: userToken, | ||
282 | videoFileToken: unrelatedFileToken, | ||
283 | videoPassword: 'incorrectPassword' | ||
284 | }) | ||
285 | }) | ||
286 | |||
287 | it('Should not be able to access an private video files without appropriate OAuth token and file token', async function () { | ||
226 | this.timeout(120000) | 288 | this.timeout(120000) |
227 | 289 | ||
228 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) | 290 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) |
@@ -247,6 +309,23 @@ describe('Test video static file privacy', function () { | |||
247 | await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken, videoFileToken }) | 309 | await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken, videoFileToken }) |
248 | }) | 310 | }) |
249 | 311 | ||
312 | it('Should be able to access a password protected video files with appropriate OAuth token or file token', async function () { | ||
313 | this.timeout(120000) | ||
314 | const videoPassword = 'my super password' | ||
315 | |||
316 | const { uuid } = await server.videos.quickUpload({ | ||
317 | name: 'video', | ||
318 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
319 | videoPasswords: [ videoPassword ] | ||
320 | }) | ||
321 | |||
322 | const videoFileToken = await server.videoToken.getVideoFileToken({ token: null, videoId: uuid, videoPassword }) | ||
323 | |||
324 | await waitJobs([ server ]) | ||
325 | |||
326 | await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken, videoFileToken, videoPassword }) | ||
327 | }) | ||
328 | |||
250 | it('Should reinject video file token', async function () { | 329 | it('Should reinject video file token', async function () { |
251 | this.timeout(120000) | 330 | this.timeout(120000) |
252 | 331 | ||
@@ -294,13 +373,20 @@ describe('Test video static file privacy', function () { | |||
294 | let permanentLiveId: string | 373 | let permanentLiveId: string |
295 | let permanentLive: LiveVideo | 374 | let permanentLive: LiveVideo |
296 | 375 | ||
376 | let passwordProtectedLiveId: string | ||
377 | let passwordProtectedLive: LiveVideo | ||
378 | |||
379 | const correctPassword = 'my super password' | ||
380 | |||
297 | let unrelatedFileToken: string | 381 | let unrelatedFileToken: string |
298 | 382 | ||
299 | async function checkLiveFiles (live: LiveVideo, liveId: string) { | 383 | async function checkLiveFiles (options: { live: LiveVideo, liveId: string, videoPassword?: string }) { |
384 | const { live, liveId, videoPassword } = options | ||
300 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) | 385 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: live.rtmpUrl, streamKey: live.streamKey }) |
301 | await server.live.waitUntilPublished({ videoId: liveId }) | 386 | await server.live.waitUntilPublished({ videoId: liveId }) |
302 | 387 | ||
303 | const video = await server.videos.getWithToken({ id: liveId }) | 388 | const video = await server.videos.getWithToken({ id: liveId }) |
389 | |||
304 | const fileToken = await server.videoToken.getVideoFileToken({ videoId: video.uuid }) | 390 | const fileToken = await server.videoToken.getVideoFileToken({ videoId: video.uuid }) |
305 | 391 | ||
306 | const hls = video.streamingPlaylists[0] | 392 | const hls = video.streamingPlaylists[0] |
@@ -314,6 +400,16 @@ describe('Test video static file privacy', function () { | |||
314 | await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 400 | await makeRawRequest({ url, token: userToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
315 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 401 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
316 | await makeRawRequest({ url, query: { videoFileToken: unrelatedFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 402 | await makeRawRequest({ url, query: { videoFileToken: unrelatedFileToken }, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) |
403 | |||
404 | if (videoPassword) { | ||
405 | await makeRawRequest({ url, headers: { 'x-peertube-video-password': videoPassword }, expectedStatus: HttpStatusCode.OK_200 }) | ||
406 | await makeRawRequest({ | ||
407 | url, | ||
408 | headers: { 'x-peertube-video-password': 'incorrectPassword' }, | ||
409 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
410 | }) | ||
411 | } | ||
412 | |||
317 | } | 413 | } |
318 | 414 | ||
319 | await stopFfmpeg(ffmpegCommand) | 415 | await stopFfmpeg(ffmpegCommand) |
@@ -381,18 +477,35 @@ describe('Test video static file privacy', function () { | |||
381 | permanentLiveId = video.uuid | 477 | permanentLiveId = video.uuid |
382 | permanentLive = live | 478 | permanentLive = live |
383 | } | 479 | } |
480 | |||
481 | { | ||
482 | const { video, live } = await server.live.quickCreate({ | ||
483 | saveReplay: false, | ||
484 | permanentLive: false, | ||
485 | privacy: VideoPrivacy.PASSWORD_PROTECTED, | ||
486 | videoPasswords: [ correctPassword ] | ||
487 | }) | ||
488 | passwordProtectedLiveId = video.uuid | ||
489 | passwordProtectedLive = live | ||
490 | } | ||
384 | }) | 491 | }) |
385 | 492 | ||
386 | it('Should create a private normal live and have a private static path', async function () { | 493 | it('Should create a private normal live and have a private static path', async function () { |
387 | this.timeout(240000) | 494 | this.timeout(240000) |
388 | 495 | ||
389 | await checkLiveFiles(normalLive, normalLiveId) | 496 | await checkLiveFiles({ live: normalLive, liveId: normalLiveId }) |
390 | }) | 497 | }) |
391 | 498 | ||
392 | it('Should create a private permanent live and have a private static path', async function () { | 499 | it('Should create a private permanent live and have a private static path', async function () { |
393 | this.timeout(240000) | 500 | this.timeout(240000) |
394 | 501 | ||
395 | await checkLiveFiles(permanentLive, permanentLiveId) | 502 | await checkLiveFiles({ live: permanentLive, liveId: permanentLiveId }) |
503 | }) | ||
504 | |||
505 | it('Should create a password protected live and have a private static path', async function () { | ||
506 | this.timeout(240000) | ||
507 | |||
508 | await checkLiveFiles({ live: passwordProtectedLive, liveId: passwordProtectedLiveId, videoPassword: correctPassword }) | ||
396 | }) | 509 | }) |
397 | 510 | ||
398 | it('Should reinject video file token on permanent live', async function () { | 511 | it('Should reinject video file token on permanent live', async function () { |