diff options
author | Chocobozzz <me@florianbigard.com> | 2022-12-02 17:54:23 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2022-12-02 17:54:23 +0100 |
commit | b8598d40f650a31fe09a4a5426dcdc2c5c0d566c (patch) | |
tree | ee41176817ef13525aadc4f0b37fc9391364d5c9 /server | |
parent | 190ac9df7c95cdae5294596764afae7ce78d108d (diff) | |
parent | bd09dfaf8dcb0ca4cd5dac9f13e3117486f3bcce (diff) | |
download | PeerTube-b8598d40f650a31fe09a4a5426dcdc2c5c0d566c.tar.gz PeerTube-b8598d40f650a31fe09a4a5426dcdc2c5c0d566c.tar.zst PeerTube-b8598d40f650a31fe09a4a5426dcdc2c5c0d566c.zip |
Merge branch 'release/5.0.0' into develop
Diffstat (limited to 'server')
-rw-r--r-- | server/controllers/object-storage-proxy.ts | 20 | ||||
-rw-r--r-- | server/controllers/shared/m3u8-playlist.ts | 14 | ||||
-rw-r--r-- | server/controllers/static.ts | 39 | ||||
-rw-r--r-- | server/helpers/stream-replacer.ts | 58 | ||||
-rw-r--r-- | server/lib/hls.ts | 9 | ||||
-rw-r--r-- | server/middlewares/validators/static.ts | 11 | ||||
-rw-r--r-- | server/middlewares/validators/users.ts | 2 | ||||
-rw-r--r-- | server/tests/api/object-storage/video-static-file-privacy.ts | 36 | ||||
-rw-r--r-- | server/tests/api/videos/video-static-file-privacy.ts | 61 | ||||
-rw-r--r-- | server/tests/shared/checks.ts | 7 | ||||
-rw-r--r-- | server/tests/shared/index.ts | 2 | ||||
-rw-r--r-- | server/tests/shared/streaming-playlists.ts | 50 | ||||
-rw-r--r-- | server/tests/shared/video-playlists.ts (renamed from server/tests/shared/playlists.ts) | 0 |
13 files changed, 299 insertions, 10 deletions
diff --git a/server/controllers/object-storage-proxy.ts b/server/controllers/object-storage-proxy.ts index 3ce279671..aa853a383 100644 --- a/server/controllers/object-storage-proxy.ts +++ b/server/controllers/object-storage-proxy.ts | |||
@@ -1,7 +1,10 @@ | |||
1 | import cors from 'cors' | 1 | import cors from 'cors' |
2 | import express from 'express' | 2 | import express from 'express' |
3 | import { PassThrough, pipeline } from 'stream' | ||
3 | import { logger } from '@server/helpers/logger' | 4 | import { logger } from '@server/helpers/logger' |
5 | import { StreamReplacer } from '@server/helpers/stream-replacer' | ||
4 | import { OBJECT_STORAGE_PROXY_PATHS } from '@server/initializers/constants' | 6 | import { OBJECT_STORAGE_PROXY_PATHS } from '@server/initializers/constants' |
7 | import { injectQueryToPlaylistUrls } from '@server/lib/hls' | ||
5 | import { getHLSFileReadStream, getWebTorrentFileReadStream } from '@server/lib/object-storage' | 8 | import { getHLSFileReadStream, getWebTorrentFileReadStream } from '@server/lib/object-storage' |
6 | import { | 9 | import { |
7 | asyncMiddleware, | 10 | asyncMiddleware, |
@@ -11,6 +14,7 @@ import { | |||
11 | optionalAuthenticate | 14 | optionalAuthenticate |
12 | } from '@server/middlewares' | 15 | } from '@server/middlewares' |
13 | import { HttpStatusCode } from '@shared/models' | 16 | import { HttpStatusCode } from '@shared/models' |
17 | import { buildReinjectVideoFileTokenQuery, doReinjectVideoFileToken } from './shared/m3u8-playlist' | ||
14 | 18 | ||
15 | const objectStorageProxyRouter = express.Router() | 19 | const objectStorageProxyRouter = express.Router() |
16 | 20 | ||
@@ -67,7 +71,20 @@ async function proxifyHLS (req: express.Request, res: express.Response) { | |||
67 | rangeHeader: req.header('range') | 71 | rangeHeader: req.header('range') |
68 | }) | 72 | }) |
69 | 73 | ||
70 | return stream.pipe(res) | 74 | const streamReplacer = filename.endsWith('.m3u8') && doReinjectVideoFileToken(req) |
75 | ? new StreamReplacer(line => injectQueryToPlaylistUrls(line, buildReinjectVideoFileTokenQuery(req))) | ||
76 | : new PassThrough() | ||
77 | |||
78 | return pipeline( | ||
79 | stream, | ||
80 | streamReplacer, | ||
81 | res, | ||
82 | err => { | ||
83 | if (!err) return | ||
84 | |||
85 | handleObjectStorageFailure(res, err) | ||
86 | } | ||
87 | ) | ||
71 | } catch (err) { | 88 | } catch (err) { |
72 | return handleObjectStorageFailure(res, err) | 89 | return handleObjectStorageFailure(res, err) |
73 | } | 90 | } |
@@ -75,6 +92,7 @@ async function proxifyHLS (req: express.Request, res: express.Response) { | |||
75 | 92 | ||
76 | function handleObjectStorageFailure (res: express.Response, err: Error) { | 93 | function handleObjectStorageFailure (res: express.Response, err: Error) { |
77 | if (err.name === 'NoSuchKey') { | 94 | if (err.name === 'NoSuchKey') { |
95 | logger.debug('Could not find key in object storage to proxify private HLS video file.', { err }) | ||
78 | return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 96 | return res.sendStatus(HttpStatusCode.NOT_FOUND_404) |
79 | } | 97 | } |
80 | 98 | ||
diff --git a/server/controllers/shared/m3u8-playlist.ts b/server/controllers/shared/m3u8-playlist.ts new file mode 100644 index 000000000..e2a66efc0 --- /dev/null +++ b/server/controllers/shared/m3u8-playlist.ts | |||
@@ -0,0 +1,14 @@ | |||
1 | import express from 'express' | ||
2 | |||
3 | function doReinjectVideoFileToken (req: express.Request) { | ||
4 | return req.query.videoFileToken && req.query.reinjectVideoFileToken | ||
5 | } | ||
6 | |||
7 | function buildReinjectVideoFileTokenQuery (req: express.Request) { | ||
8 | return 'videoFileToken=' + req.query.videoFileToken | ||
9 | } | ||
10 | |||
11 | export { | ||
12 | doReinjectVideoFileToken, | ||
13 | buildReinjectVideoFileTokenQuery | ||
14 | } | ||
diff --git a/server/controllers/static.ts b/server/controllers/static.ts index 6ef9154b9..52e48267f 100644 --- a/server/controllers/static.ts +++ b/server/controllers/static.ts | |||
@@ -1,5 +1,8 @@ | |||
1 | import cors from 'cors' | 1 | import cors from 'cors' |
2 | import express from 'express' | 2 | import express from 'express' |
3 | import { readFile } from 'fs-extra' | ||
4 | import { join } from 'path' | ||
5 | import { injectQueryToPlaylistUrls } from '@server/lib/hls' | ||
3 | import { | 6 | import { |
4 | asyncMiddleware, | 7 | asyncMiddleware, |
5 | ensureCanAccessPrivateVideoHLSFiles, | 8 | ensureCanAccessPrivateVideoHLSFiles, |
@@ -7,8 +10,10 @@ import { | |||
7 | handleStaticError, | 10 | handleStaticError, |
8 | optionalAuthenticate | 11 | optionalAuthenticate |
9 | } from '@server/middlewares' | 12 | } from '@server/middlewares' |
13 | import { HttpStatusCode } from '@shared/models' | ||
10 | import { CONFIG } from '../initializers/config' | 14 | import { CONFIG } from '../initializers/config' |
11 | import { DIRECTORIES, STATIC_MAX_AGE, STATIC_PATHS } from '../initializers/constants' | 15 | import { DIRECTORIES, STATIC_MAX_AGE, STATIC_PATHS } from '../initializers/constants' |
16 | import { buildReinjectVideoFileTokenQuery, doReinjectVideoFileToken } from './shared/m3u8-playlist' | ||
12 | 17 | ||
13 | const staticRouter = express.Router() | 18 | const staticRouter = express.Router() |
14 | 19 | ||
@@ -50,6 +55,12 @@ const privateHLSStaticMiddlewares = CONFIG.STATIC_FILES.PRIVATE_FILES_REQUIRE_AU | |||
50 | : [] | 55 | : [] |
51 | 56 | ||
52 | staticRouter.use( | 57 | staticRouter.use( |
58 | STATIC_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS + ':videoUUID/:playlistName.m3u8', | ||
59 | ...privateHLSStaticMiddlewares, | ||
60 | asyncMiddleware(servePrivateM3U8) | ||
61 | ) | ||
62 | |||
63 | staticRouter.use( | ||
53 | STATIC_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS, | 64 | STATIC_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS, |
54 | ...privateHLSStaticMiddlewares, | 65 | ...privateHLSStaticMiddlewares, |
55 | express.static(DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE, { fallthrough: false }), | 66 | express.static(DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE, { fallthrough: false }), |
@@ -74,3 +85,31 @@ staticRouter.use( | |||
74 | export { | 85 | export { |
75 | staticRouter | 86 | staticRouter |
76 | } | 87 | } |
88 | |||
89 | // --------------------------------------------------------------------------- | ||
90 | |||
91 | async function servePrivateM3U8 (req: express.Request, res: express.Response) { | ||
92 | const path = join(DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE, req.params.videoUUID, req.params.playlistName + '.m3u8') | ||
93 | |||
94 | let playlistContent: string | ||
95 | |||
96 | try { | ||
97 | playlistContent = await readFile(path, 'utf-8') | ||
98 | } catch (err) { | ||
99 | if (err.message.includes('ENOENT')) { | ||
100 | return res.fail({ | ||
101 | status: HttpStatusCode.NOT_FOUND_404, | ||
102 | message: 'File not found' | ||
103 | }) | ||
104 | } | ||
105 | |||
106 | throw err | ||
107 | } | ||
108 | |||
109 | // Inject token in playlist so players that cannot alter the HTTP request can still watch the video | ||
110 | const transformedContent = doReinjectVideoFileToken(req) | ||
111 | ? injectQueryToPlaylistUrls(playlistContent, buildReinjectVideoFileTokenQuery(req)) | ||
112 | : playlistContent | ||
113 | |||
114 | return res.set('content-type', 'application/vnd.apple.mpegurl').send(transformedContent).end() | ||
115 | } | ||
diff --git a/server/helpers/stream-replacer.ts b/server/helpers/stream-replacer.ts new file mode 100644 index 000000000..4babab418 --- /dev/null +++ b/server/helpers/stream-replacer.ts | |||
@@ -0,0 +1,58 @@ | |||
1 | import { Transform, TransformCallback } from 'stream' | ||
2 | |||
3 | // Thanks: https://stackoverflow.com/a/45126242 | ||
4 | class StreamReplacer extends Transform { | ||
5 | private pendingChunk: Buffer | ||
6 | |||
7 | constructor (private readonly replacer: (line: string) => string) { | ||
8 | super() | ||
9 | } | ||
10 | |||
11 | _transform (chunk: Buffer, _encoding: BufferEncoding, done: TransformCallback) { | ||
12 | try { | ||
13 | this.pendingChunk = this.pendingChunk?.length | ||
14 | ? Buffer.concat([ this.pendingChunk, chunk ]) | ||
15 | : chunk | ||
16 | |||
17 | let index: number | ||
18 | |||
19 | // As long as we keep finding newlines, keep making slices of the buffer and push them to the | ||
20 | // readable side of the transform stream | ||
21 | while ((index = this.pendingChunk.indexOf('\n')) !== -1) { | ||
22 | // The `end` parameter is non-inclusive, so increase it to include the newline we found | ||
23 | const line = this.pendingChunk.slice(0, ++index) | ||
24 | |||
25 | // `start` is inclusive, but we are already one char ahead of the newline -> all good | ||
26 | this.pendingChunk = this.pendingChunk.slice(index) | ||
27 | |||
28 | // We have a single line here! Prepend the string we want | ||
29 | this.push(this.doReplace(line)) | ||
30 | } | ||
31 | |||
32 | return done() | ||
33 | } catch (err) { | ||
34 | return done(err) | ||
35 | } | ||
36 | } | ||
37 | |||
38 | _flush (done: TransformCallback) { | ||
39 | // If we have any remaining data in the cache, send it out | ||
40 | if (!this.pendingChunk?.length) return done() | ||
41 | |||
42 | try { | ||
43 | return done(null, this.doReplace(this.pendingChunk)) | ||
44 | } catch (err) { | ||
45 | return done(err) | ||
46 | } | ||
47 | } | ||
48 | |||
49 | private doReplace (buffer: Buffer) { | ||
50 | const line = this.replacer(buffer.toString('utf8')) | ||
51 | |||
52 | return Buffer.from(line, 'utf8') | ||
53 | } | ||
54 | } | ||
55 | |||
56 | export { | ||
57 | StreamReplacer | ||
58 | } | ||
diff --git a/server/lib/hls.ts b/server/lib/hls.ts index a41f1ae48..053b5d326 100644 --- a/server/lib/hls.ts +++ b/server/lib/hls.ts | |||
@@ -234,13 +234,20 @@ function downloadPlaylistSegments (playlistUrl: string, destinationDir: string, | |||
234 | 234 | ||
235 | // --------------------------------------------------------------------------- | 235 | // --------------------------------------------------------------------------- |
236 | 236 | ||
237 | function injectQueryToPlaylistUrls (content: string, queryString: string) { | ||
238 | return content.replace(/\.(m3u8|ts|mp4)/gm, '.$1?' + queryString) | ||
239 | } | ||
240 | |||
241 | // --------------------------------------------------------------------------- | ||
242 | |||
237 | export { | 243 | export { |
238 | updateMasterHLSPlaylist, | 244 | updateMasterHLSPlaylist, |
239 | updateSha256VODSegments, | 245 | updateSha256VODSegments, |
240 | buildSha256Segment, | 246 | buildSha256Segment, |
241 | downloadPlaylistSegments, | 247 | downloadPlaylistSegments, |
242 | updateStreamingPlaylistsInfohashesIfNeeded, | 248 | updateStreamingPlaylistsInfohashesIfNeeded, |
243 | updatePlaylistAfterFileChange | 249 | updatePlaylistAfterFileChange, |
250 | injectQueryToPlaylistUrls | ||
244 | } | 251 | } |
245 | 252 | ||
246 | // --------------------------------------------------------------------------- | 253 | // --------------------------------------------------------------------------- |
diff --git a/server/middlewares/validators/static.ts b/server/middlewares/validators/static.ts index 13fde6dd1..d3d307787 100644 --- a/server/middlewares/validators/static.ts +++ b/server/middlewares/validators/static.ts | |||
@@ -2,7 +2,7 @@ import express from 'express' | |||
2 | import { query } from 'express-validator' | 2 | import { query } from 'express-validator' |
3 | import LRUCache from 'lru-cache' | 3 | import LRUCache from 'lru-cache' |
4 | import { basename, dirname } from 'path' | 4 | import { basename, dirname } from 'path' |
5 | import { exists, isUUIDValid } from '@server/helpers/custom-validators/misc' | 5 | import { exists, isUUIDValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc' |
6 | import { logger } from '@server/helpers/logger' | 6 | import { logger } from '@server/helpers/logger' |
7 | import { LRU_CACHE } from '@server/initializers/constants' | 7 | import { LRU_CACHE } from '@server/initializers/constants' |
8 | import { VideoModel } from '@server/models/video/video' | 8 | import { VideoModel } from '@server/models/video/video' |
@@ -60,7 +60,14 @@ const ensureCanAccessVideoPrivateWebTorrentFiles = [ | |||
60 | ] | 60 | ] |
61 | 61 | ||
62 | const ensureCanAccessPrivateVideoHLSFiles = [ | 62 | const ensureCanAccessPrivateVideoHLSFiles = [ |
63 | query('videoFileToken').optional().custom(exists), | 63 | query('videoFileToken') |
64 | .optional() | ||
65 | .custom(exists), | ||
66 | |||
67 | query('reinjectVideoFileToken') | ||
68 | .optional() | ||
69 | .customSanitizer(toBooleanOrNull) | ||
70 | .isBoolean().withMessage('Should be a valid reinjectVideoFileToken boolean'), | ||
64 | 71 | ||
65 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 72 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
66 | if (areValidationErrors(req, res)) return | 73 | if (areValidationErrors(req, res)) return |
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index 50327b6ae..64bd9ca70 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts | |||
@@ -44,7 +44,7 @@ const usersListValidator = [ | |||
44 | query('blocked') | 44 | query('blocked') |
45 | .optional() | 45 | .optional() |
46 | .customSanitizer(toBooleanOrNull) | 46 | .customSanitizer(toBooleanOrNull) |
47 | .isBoolean().withMessage('Should be a valid blocked boolena'), | 47 | .isBoolean().withMessage('Should be a valid blocked boolean'), |
48 | 48 | ||
49 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | 49 | (req: express.Request, res: express.Response, next: express.NextFunction) => { |
50 | if (areValidationErrors(req, res)) return | 50 | if (areValidationErrors(req, res)) return |
diff --git a/server/tests/api/object-storage/video-static-file-privacy.ts b/server/tests/api/object-storage/video-static-file-privacy.ts index 62edd10ba..71ad35a43 100644 --- a/server/tests/api/object-storage/video-static-file-privacy.ts +++ b/server/tests/api/object-storage/video-static-file-privacy.ts | |||
@@ -2,7 +2,7 @@ | |||
2 | 2 | ||
3 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
4 | import { basename } from 'path' | 4 | import { basename } from 'path' |
5 | import { expectStartWith } from '@server/tests/shared' | 5 | import { checkVideoFileTokenReinjection, expectStartWith } from '@server/tests/shared' |
6 | import { areScalewayObjectStorageTestsDisabled, getAllFiles, getHLS } from '@shared/core-utils' | 6 | import { areScalewayObjectStorageTestsDisabled, getAllFiles, getHLS } from '@shared/core-utils' |
7 | import { HttpStatusCode, LiveVideo, VideoDetails, VideoPrivacy } from '@shared/models' | 7 | import { HttpStatusCode, LiveVideo, VideoDetails, VideoPrivacy } from '@shared/models' |
8 | import { | 8 | import { |
@@ -191,6 +191,20 @@ describe('Object storage for video static file privacy', function () { | |||
191 | } | 191 | } |
192 | }) | 192 | }) |
193 | 193 | ||
194 | it('Should reinject video file token', async function () { | ||
195 | this.timeout(120000) | ||
196 | |||
197 | const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: privateVideoUUID }) | ||
198 | |||
199 | await checkVideoFileTokenReinjection({ | ||
200 | server, | ||
201 | videoUUID: privateVideoUUID, | ||
202 | videoFileToken, | ||
203 | resolutions: [ 240, 720 ], | ||
204 | isLive: false | ||
205 | }) | ||
206 | }) | ||
207 | |||
194 | it('Should update public video to private', async function () { | 208 | it('Should update public video to private', async function () { |
195 | this.timeout(60000) | 209 | this.timeout(60000) |
196 | 210 | ||
@@ -315,6 +329,26 @@ describe('Object storage for video static file privacy', function () { | |||
315 | await checkLiveFiles(permanentLive, permanentLiveId) | 329 | await checkLiveFiles(permanentLive, permanentLiveId) |
316 | }) | 330 | }) |
317 | 331 | ||
332 | it('Should reinject video file token in permanent live', async function () { | ||
333 | this.timeout(240000) | ||
334 | |||
335 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: permanentLive.rtmpUrl, streamKey: permanentLive.streamKey }) | ||
336 | await server.live.waitUntilPublished({ videoId: permanentLiveId }) | ||
337 | |||
338 | const video = await server.videos.getWithToken({ id: permanentLiveId }) | ||
339 | const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: video.uuid }) | ||
340 | |||
341 | await checkVideoFileTokenReinjection({ | ||
342 | server, | ||
343 | videoUUID: permanentLiveId, | ||
344 | videoFileToken, | ||
345 | resolutions: [ 720 ], | ||
346 | isLive: true | ||
347 | }) | ||
348 | |||
349 | await stopFfmpeg(ffmpegCommand) | ||
350 | }) | ||
351 | |||
318 | it('Should have created a replay of the normal live with a private static path', async function () { | 352 | it('Should have created a replay of the normal live with a private static path', async function () { |
319 | this.timeout(240000) | 353 | this.timeout(240000) |
320 | 354 | ||
diff --git a/server/tests/api/videos/video-static-file-privacy.ts b/server/tests/api/videos/video-static-file-privacy.ts index eaaed5aad..ef0774b41 100644 --- a/server/tests/api/videos/video-static-file-privacy.ts +++ b/server/tests/api/videos/video-static-file-privacy.ts | |||
@@ -2,7 +2,7 @@ | |||
2 | 2 | ||
3 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
4 | import { decode } from 'magnet-uri' | 4 | import { decode } from 'magnet-uri' |
5 | import { expectStartWith } from '@server/tests/shared' | 5 | import { checkVideoFileTokenReinjection, expectStartWith } from '@server/tests/shared' |
6 | import { getAllFiles, wait } from '@shared/core-utils' | 6 | import { getAllFiles, wait } from '@shared/core-utils' |
7 | import { HttpStatusCode, LiveVideo, VideoDetails, VideoPrivacy } from '@shared/models' | 7 | import { HttpStatusCode, LiveVideo, VideoDetails, VideoPrivacy } from '@shared/models' |
8 | import { | 8 | import { |
@@ -248,6 +248,35 @@ describe('Test video static file privacy', function () { | |||
248 | await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken, videoFileToken }) | 248 | await checkVideoFiles({ id: uuid, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken, videoFileToken }) |
249 | }) | 249 | }) |
250 | 250 | ||
251 | it('Should reinject video file token', async function () { | ||
252 | this.timeout(120000) | ||
253 | |||
254 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) | ||
255 | |||
256 | const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: uuid }) | ||
257 | await waitJobs([ server ]) | ||
258 | |||
259 | const video = await server.videos.getWithToken({ id: uuid }) | ||
260 | const hls = video.streamingPlaylists[0] | ||
261 | |||
262 | { | ||
263 | const query = { videoFileToken } | ||
264 | const { text } = await makeRawRequest({ url: hls.playlistUrl, query, expectedStatus: HttpStatusCode.OK_200 }) | ||
265 | |||
266 | expect(text).to.not.include(videoFileToken) | ||
267 | } | ||
268 | |||
269 | { | ||
270 | await checkVideoFileTokenReinjection({ | ||
271 | server, | ||
272 | videoUUID: uuid, | ||
273 | videoFileToken, | ||
274 | resolutions: [ 240, 720 ], | ||
275 | isLive: false | ||
276 | }) | ||
277 | } | ||
278 | }) | ||
279 | |||
251 | it('Should be able to access a private video of another user with an admin OAuth token or file token', async function () { | 280 | it('Should be able to access a private video of another user with an admin OAuth token or file token', async function () { |
252 | this.timeout(120000) | 281 | this.timeout(120000) |
253 | 282 | ||
@@ -360,6 +389,36 @@ describe('Test video static file privacy', function () { | |||
360 | await checkLiveFiles(permanentLive, permanentLiveId) | 389 | await checkLiveFiles(permanentLive, permanentLiveId) |
361 | }) | 390 | }) |
362 | 391 | ||
392 | it('Should reinject video file token on permanent live', async function () { | ||
393 | this.timeout(240000) | ||
394 | |||
395 | const ffmpegCommand = sendRTMPStream({ rtmpBaseUrl: permanentLive.rtmpUrl, streamKey: permanentLive.streamKey }) | ||
396 | await server.live.waitUntilPublished({ videoId: permanentLiveId }) | ||
397 | |||
398 | const video = await server.videos.getWithToken({ id: permanentLiveId }) | ||
399 | const videoFileToken = await server.videoToken.getVideoFileToken({ videoId: video.uuid }) | ||
400 | const hls = video.streamingPlaylists[0] | ||
401 | |||
402 | { | ||
403 | const query = { videoFileToken } | ||
404 | const { text } = await makeRawRequest({ url: hls.playlistUrl, query, expectedStatus: HttpStatusCode.OK_200 }) | ||
405 | |||
406 | expect(text).to.not.include(videoFileToken) | ||
407 | } | ||
408 | |||
409 | { | ||
410 | await checkVideoFileTokenReinjection({ | ||
411 | server, | ||
412 | videoUUID: permanentLiveId, | ||
413 | videoFileToken, | ||
414 | resolutions: [ 720 ], | ||
415 | isLive: true | ||
416 | }) | ||
417 | } | ||
418 | |||
419 | await stopFfmpeg(ffmpegCommand) | ||
420 | }) | ||
421 | |||
363 | it('Should have created a replay of the normal live with a private static path', async function () { | 422 | it('Should have created a replay of the normal live with a private static path', async function () { |
364 | this.timeout(240000) | 423 | this.timeout(240000) |
365 | 424 | ||
diff --git a/server/tests/shared/checks.ts b/server/tests/shared/checks.ts index 55ebc6c3e..523d37420 100644 --- a/server/tests/shared/checks.ts +++ b/server/tests/shared/checks.ts | |||
@@ -23,6 +23,12 @@ function expectNotStartWith (str: string, start: string) { | |||
23 | expect(str.startsWith(start), `${str} does not start with ${start}`).to.be.false | 23 | expect(str.startsWith(start), `${str} does not start with ${start}`).to.be.false |
24 | } | 24 | } |
25 | 25 | ||
26 | function expectEndWith (str: string, end: string) { | ||
27 | expect(str.endsWith(end), `${str} does not end with ${end}`).to.be.true | ||
28 | } | ||
29 | |||
30 | // --------------------------------------------------------------------------- | ||
31 | |||
26 | async function expectLogDoesNotContain (server: PeerTubeServer, str: string) { | 32 | async function expectLogDoesNotContain (server: PeerTubeServer, str: string) { |
27 | const content = await server.servers.getLogContent() | 33 | const content = await server.servers.getLogContent() |
28 | 34 | ||
@@ -103,6 +109,7 @@ export { | |||
103 | testFileExistsOrNot, | 109 | testFileExistsOrNot, |
104 | expectStartWith, | 110 | expectStartWith, |
105 | expectNotStartWith, | 111 | expectNotStartWith, |
112 | expectEndWith, | ||
106 | checkBadStartPagination, | 113 | checkBadStartPagination, |
107 | checkBadCountPagination, | 114 | checkBadCountPagination, |
108 | checkBadSortPagination, | 115 | checkBadSortPagination, |
diff --git a/server/tests/shared/index.ts b/server/tests/shared/index.ts index 9f7ade53d..963ef8fe6 100644 --- a/server/tests/shared/index.ts +++ b/server/tests/shared/index.ts | |||
@@ -6,7 +6,7 @@ export * from './directories' | |||
6 | export * from './generate' | 6 | export * from './generate' |
7 | export * from './live' | 7 | export * from './live' |
8 | export * from './notifications' | 8 | export * from './notifications' |
9 | export * from './playlists' | 9 | export * from './video-playlists' |
10 | export * from './plugins' | 10 | export * from './plugins' |
11 | export * from './requests' | 11 | export * from './requests' |
12 | export * from './streaming-playlists' | 12 | export * from './streaming-playlists' |
diff --git a/server/tests/shared/streaming-playlists.ts b/server/tests/shared/streaming-playlists.ts index 824c3dcef..5c62af812 100644 --- a/server/tests/shared/streaming-playlists.ts +++ b/server/tests/shared/streaming-playlists.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | 1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ |
2 | 2 | ||
3 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
4 | import { basename } from 'path' | 4 | import { basename, dirname, join } from 'path' |
5 | import { removeFragmentedMP4Ext, uuidRegex } from '@shared/core-utils' | 5 | import { removeFragmentedMP4Ext, uuidRegex } from '@shared/core-utils' |
6 | import { sha256 } from '@shared/extra-utils' | 6 | import { sha256 } from '@shared/extra-utils' |
7 | import { HttpStatusCode, VideoStreamingPlaylist, VideoStreamingPlaylistType } from '@shared/models' | 7 | import { HttpStatusCode, VideoStreamingPlaylist, VideoStreamingPlaylistType } from '@shared/models' |
@@ -188,9 +188,55 @@ async function completeCheckHlsPlaylist (options: { | |||
188 | } | 188 | } |
189 | } | 189 | } |
190 | 190 | ||
191 | async function checkVideoFileTokenReinjection (options: { | ||
192 | server: PeerTubeServer | ||
193 | videoUUID: string | ||
194 | videoFileToken: string | ||
195 | resolutions: number[] | ||
196 | isLive: boolean | ||
197 | }) { | ||
198 | const { server, resolutions, videoFileToken, videoUUID, isLive } = options | ||
199 | |||
200 | const video = await server.videos.getWithToken({ id: videoUUID }) | ||
201 | const hls = video.streamingPlaylists[0] | ||
202 | |||
203 | const query = { videoFileToken, reinjectVideoFileToken: 'true' } | ||
204 | const { text } = await makeRawRequest({ url: hls.playlistUrl, query, expectedStatus: HttpStatusCode.OK_200 }) | ||
205 | |||
206 | for (let i = 0; i < resolutions.length; i++) { | ||
207 | const resolution = resolutions[i] | ||
208 | |||
209 | const suffix = isLive | ||
210 | ? i | ||
211 | : `-${resolution}` | ||
212 | |||
213 | expect(text).to.contain(`${suffix}.m3u8?videoFileToken=${videoFileToken}`) | ||
214 | } | ||
215 | |||
216 | const resolutionPlaylists = extractResolutionPlaylistUrls(hls.playlistUrl, text) | ||
217 | expect(resolutionPlaylists).to.have.lengthOf(resolutions.length) | ||
218 | |||
219 | for (const url of resolutionPlaylists) { | ||
220 | const { text } = await makeRawRequest({ url, query, expectedStatus: HttpStatusCode.OK_200 }) | ||
221 | |||
222 | const extension = isLive | ||
223 | ? '.ts' | ||
224 | : '.mp4' | ||
225 | |||
226 | expect(text).to.contain(`${extension}?videoFileToken=${videoFileToken}`) | ||
227 | } | ||
228 | } | ||
229 | |||
230 | function extractResolutionPlaylistUrls (masterPath: string, masterContent: string) { | ||
231 | return masterContent.match(/^([^.]+\.m3u8.*)/mg) | ||
232 | .map(filename => join(dirname(masterPath), filename)) | ||
233 | } | ||
234 | |||
191 | export { | 235 | export { |
192 | checkSegmentHash, | 236 | checkSegmentHash, |
193 | checkLiveSegmentHash, | 237 | checkLiveSegmentHash, |
194 | checkResolutionsInMasterPlaylist, | 238 | checkResolutionsInMasterPlaylist, |
195 | completeCheckHlsPlaylist | 239 | completeCheckHlsPlaylist, |
240 | extractResolutionPlaylistUrls, | ||
241 | checkVideoFileTokenReinjection | ||
196 | } | 242 | } |
diff --git a/server/tests/shared/playlists.ts b/server/tests/shared/video-playlists.ts index 8db303fd8..8db303fd8 100644 --- a/server/tests/shared/playlists.ts +++ b/server/tests/shared/video-playlists.ts | |||