diff options
author | Chocobozzz <me@florianbigard.com> | 2021-03-23 11:54:08 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2021-03-24 18:18:41 +0100 |
commit | 4bc45da342597fb49593fc14c40f8dc5a97bb64e (patch) | |
tree | 9fa876e21995b27827fbc4467bd71b8d427312e2 /server | |
parent | c0ab041c2c749db05ce564d3078c2ad10d18f35f (diff) | |
download | PeerTube-4bc45da342597fb49593fc14c40f8dc5a97bb64e.tar.gz PeerTube-4bc45da342597fb49593fc14c40f8dc5a97bb64e.tar.zst PeerTube-4bc45da342597fb49593fc14c40f8dc5a97bb64e.zip |
Add hooks support for video download
Diffstat (limited to 'server')
-rw-r--r-- | server/controllers/download.ts | 85 | ||||
-rw-r--r-- | server/lib/files-cache/videos-torrent-cache.ts | 13 | ||||
-rw-r--r-- | server/tests/fixtures/peertube-plugin-test/main.js | 26 | ||||
-rw-r--r-- | server/tests/plugins/filter-hooks.ts | 63 |
4 files changed, 174 insertions, 13 deletions
diff --git a/server/controllers/download.ts b/server/controllers/download.ts index 27caa1518..fd44f10e9 100644 --- a/server/controllers/download.ts +++ b/server/controllers/download.ts | |||
@@ -1,8 +1,10 @@ | |||
1 | import * as cors from 'cors' | 1 | import * as cors from 'cors' |
2 | import * as express from 'express' | 2 | import * as express from 'express' |
3 | import { logger } from '@server/helpers/logger' | ||
3 | import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache' | 4 | import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache' |
5 | import { Hooks } from '@server/lib/plugins/hooks' | ||
4 | import { getVideoFilePath } from '@server/lib/video-paths' | 6 | import { getVideoFilePath } from '@server/lib/video-paths' |
5 | import { MVideoFile, MVideoFullLight } from '@server/types/models' | 7 | import { MStreamingPlaylist, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' |
6 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | 8 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' |
7 | import { VideoStreamingPlaylistType } from '@shared/models' | 9 | import { VideoStreamingPlaylistType } from '@shared/models' |
8 | import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants' | 10 | import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants' |
@@ -14,19 +16,19 @@ downloadRouter.use(cors()) | |||
14 | 16 | ||
15 | downloadRouter.use( | 17 | downloadRouter.use( |
16 | STATIC_DOWNLOAD_PATHS.TORRENTS + ':filename', | 18 | STATIC_DOWNLOAD_PATHS.TORRENTS + ':filename', |
17 | downloadTorrent | 19 | asyncMiddleware(downloadTorrent) |
18 | ) | 20 | ) |
19 | 21 | ||
20 | downloadRouter.use( | 22 | downloadRouter.use( |
21 | STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension', | 23 | STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension', |
22 | asyncMiddleware(videosDownloadValidator), | 24 | asyncMiddleware(videosDownloadValidator), |
23 | downloadVideoFile | 25 | asyncMiddleware(downloadVideoFile) |
24 | ) | 26 | ) |
25 | 27 | ||
26 | downloadRouter.use( | 28 | downloadRouter.use( |
27 | STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension', | 29 | STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension', |
28 | asyncMiddleware(videosDownloadValidator), | 30 | asyncMiddleware(videosDownloadValidator), |
29 | downloadHLSVideoFile | 31 | asyncMiddleware(downloadHLSVideoFile) |
30 | ) | 32 | ) |
31 | 33 | ||
32 | // --------------------------------------------------------------------------- | 34 | // --------------------------------------------------------------------------- |
@@ -41,28 +43,58 @@ async function downloadTorrent (req: express.Request, res: express.Response) { | |||
41 | const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename) | 43 | const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename) |
42 | if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) | 44 | if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) |
43 | 45 | ||
46 | const allowParameters = { torrentPath: result.path, downloadName: result.downloadName } | ||
47 | |||
48 | const allowedResult = await Hooks.wrapFun( | ||
49 | isTorrentDownloadAllowed, | ||
50 | allowParameters, | ||
51 | 'filter:api.download.torrent.allowed.result' | ||
52 | ) | ||
53 | |||
54 | if (!checkAllowResult(res, allowParameters, allowedResult)) return | ||
55 | |||
44 | return res.download(result.path, result.downloadName) | 56 | return res.download(result.path, result.downloadName) |
45 | } | 57 | } |
46 | 58 | ||
47 | function downloadVideoFile (req: express.Request, res: express.Response) { | 59 | async function downloadVideoFile (req: express.Request, res: express.Response) { |
48 | const video = res.locals.videoAll | 60 | const video = res.locals.videoAll |
49 | 61 | ||
50 | const videoFile = getVideoFile(req, video.VideoFiles) | 62 | const videoFile = getVideoFile(req, video.VideoFiles) |
51 | if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() | 63 | if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() |
52 | 64 | ||
65 | const allowParameters = { video, videoFile } | ||
66 | |||
67 | const allowedResult = await Hooks.wrapFun( | ||
68 | isVideoDownloadAllowed, | ||
69 | allowParameters, | ||
70 | 'filter:api.download.video.allowed.result' | ||
71 | ) | ||
72 | |||
73 | if (!checkAllowResult(res, allowParameters, allowedResult)) return | ||
74 | |||
53 | return res.download(getVideoFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`) | 75 | return res.download(getVideoFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`) |
54 | } | 76 | } |
55 | 77 | ||
56 | function downloadHLSVideoFile (req: express.Request, res: express.Response) { | 78 | async function downloadHLSVideoFile (req: express.Request, res: express.Response) { |
57 | const video = res.locals.videoAll | 79 | const video = res.locals.videoAll |
58 | const playlist = getHLSPlaylist(video) | 80 | const streamingPlaylist = getHLSPlaylist(video) |
59 | if (!playlist) return res.status(HttpStatusCode.NOT_FOUND_404).end | 81 | if (!streamingPlaylist) return res.status(HttpStatusCode.NOT_FOUND_404).end |
60 | 82 | ||
61 | const videoFile = getVideoFile(req, playlist.VideoFiles) | 83 | const videoFile = getVideoFile(req, streamingPlaylist.VideoFiles) |
62 | if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() | 84 | if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() |
63 | 85 | ||
64 | const filename = `${video.name}-${videoFile.resolution}p-${playlist.getStringType()}${videoFile.extname}` | 86 | const allowParameters = { video, streamingPlaylist, videoFile } |
65 | return res.download(getVideoFilePath(playlist, videoFile), filename) | 87 | |
88 | const allowedResult = await Hooks.wrapFun( | ||
89 | isVideoDownloadAllowed, | ||
90 | allowParameters, | ||
91 | 'filter:api.download.video.allowed.result' | ||
92 | ) | ||
93 | |||
94 | if (!checkAllowResult(res, allowParameters, allowedResult)) return | ||
95 | |||
96 | const filename = `${video.name}-${videoFile.resolution}p-${streamingPlaylist.getStringType()}${videoFile.extname}` | ||
97 | return res.download(getVideoFilePath(streamingPlaylist, videoFile), filename) | ||
66 | } | 98 | } |
67 | 99 | ||
68 | function getVideoFile (req: express.Request, files: MVideoFile[]) { | 100 | function getVideoFile (req: express.Request, files: MVideoFile[]) { |
@@ -76,3 +108,34 @@ function getHLSPlaylist (video: MVideoFullLight) { | |||
76 | 108 | ||
77 | return Object.assign(playlist, { Video: video }) | 109 | return Object.assign(playlist, { Video: video }) |
78 | } | 110 | } |
111 | |||
112 | type AllowedResult = { | ||
113 | allowed: boolean | ||
114 | errorMessage?: string | ||
115 | } | ||
116 | |||
117 | function isTorrentDownloadAllowed (_object: { | ||
118 | torrentPath: string | ||
119 | }): AllowedResult { | ||
120 | return { allowed: true } | ||
121 | } | ||
122 | |||
123 | function isVideoDownloadAllowed (_object: { | ||
124 | video: MVideo | ||
125 | videoFile: MVideoFile | ||
126 | streamingPlaylist?: MStreamingPlaylist | ||
127 | }): AllowedResult { | ||
128 | return { allowed: true } | ||
129 | } | ||
130 | |||
131 | function checkAllowResult (res: express.Response, allowParameters: any, result?: AllowedResult) { | ||
132 | if (!result || result.allowed !== true) { | ||
133 | logger.info('Download is not allowed.', { result, allowParameters }) | ||
134 | res.status(HttpStatusCode.FORBIDDEN_403) | ||
135 | .json({ error: result.errorMessage || 'Refused download' }) | ||
136 | |||
137 | return false | ||
138 | } | ||
139 | |||
140 | return true | ||
141 | } | ||
diff --git a/server/lib/files-cache/videos-torrent-cache.ts b/server/lib/files-cache/videos-torrent-cache.ts index 881fa9ced..23217f140 100644 --- a/server/lib/files-cache/videos-torrent-cache.ts +++ b/server/lib/files-cache/videos-torrent-cache.ts | |||
@@ -5,6 +5,7 @@ import { CONFIG } from '../../initializers/config' | |||
5 | import { FILES_CACHE } from '../../initializers/constants' | 5 | import { FILES_CACHE } from '../../initializers/constants' |
6 | import { VideoModel } from '../../models/video/video' | 6 | import { VideoModel } from '../../models/video/video' |
7 | import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' | 7 | import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' |
8 | import { MVideo, MVideoFile } from '@server/types/models' | ||
8 | 9 | ||
9 | class VideosTorrentCache extends AbstractVideoStaticFileCache <string> { | 10 | class VideosTorrentCache extends AbstractVideoStaticFileCache <string> { |
10 | 11 | ||
@@ -22,7 +23,11 @@ class VideosTorrentCache extends AbstractVideoStaticFileCache <string> { | |||
22 | const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(filename) | 23 | const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(filename) |
23 | if (!file) return undefined | 24 | if (!file) return undefined |
24 | 25 | ||
25 | if (file.getVideo().isOwned()) return { isOwned: true, path: join(CONFIG.STORAGE.TORRENTS_DIR, file.torrentFilename) } | 26 | if (file.getVideo().isOwned()) { |
27 | const downloadName = this.buildDownloadName(file.getVideo(), file) | ||
28 | |||
29 | return { isOwned: true, path: join(CONFIG.STORAGE.TORRENTS_DIR, file.torrentFilename), downloadName } | ||
30 | } | ||
26 | 31 | ||
27 | return this.loadRemoteFile(filename) | 32 | return this.loadRemoteFile(filename) |
28 | } | 33 | } |
@@ -43,10 +48,14 @@ class VideosTorrentCache extends AbstractVideoStaticFileCache <string> { | |||
43 | 48 | ||
44 | await doRequestAndSaveToFile(remoteUrl, destPath) | 49 | await doRequestAndSaveToFile(remoteUrl, destPath) |
45 | 50 | ||
46 | const downloadName = `${video.name}-${file.resolution}p.torrent` | 51 | const downloadName = this.buildDownloadName(video, file) |
47 | 52 | ||
48 | return { isOwned: false, path: destPath, downloadName } | 53 | return { isOwned: false, path: destPath, downloadName } |
49 | } | 54 | } |
55 | |||
56 | private buildDownloadName (video: MVideo, file: MVideoFile) { | ||
57 | return `${video.name}-${file.resolution}p.torrent` | ||
58 | } | ||
50 | } | 59 | } |
51 | 60 | ||
52 | export { | 61 | export { |
diff --git a/server/tests/fixtures/peertube-plugin-test/main.js b/server/tests/fixtures/peertube-plugin-test/main.js index 305d92002..9913d0846 100644 --- a/server/tests/fixtures/peertube-plugin-test/main.js +++ b/server/tests/fixtures/peertube-plugin-test/main.js | |||
@@ -184,6 +184,32 @@ async function register ({ registerHook, registerSetting, settingsManager, stora | |||
184 | return result | 184 | return result |
185 | } | 185 | } |
186 | }) | 186 | }) |
187 | |||
188 | registerHook({ | ||
189 | target: 'filter:api.download.torrent.allowed.result', | ||
190 | handler: (result, params) => { | ||
191 | if (params && params.downloadName.includes('bad torrent')) { | ||
192 | return { allowed: false, errorMessage: 'Liu Bei' } | ||
193 | } | ||
194 | |||
195 | return result | ||
196 | } | ||
197 | }) | ||
198 | |||
199 | registerHook({ | ||
200 | target: 'filter:api.download.video.allowed.result', | ||
201 | handler: (result, params) => { | ||
202 | if (params && !params.streamingPlaylist && params.video.name.includes('bad file')) { | ||
203 | return { allowed: false, errorMessage: 'Cao Cao' } | ||
204 | } | ||
205 | |||
206 | if (params && params.streamingPlaylist && params.video.name.includes('bad playlist file')) { | ||
207 | return { allowed: false, errorMessage: 'Sun Jian' } | ||
208 | } | ||
209 | |||
210 | return result | ||
211 | } | ||
212 | }) | ||
187 | } | 213 | } |
188 | 214 | ||
189 | async function unregister () { | 215 | async function unregister () { |
diff --git a/server/tests/plugins/filter-hooks.ts b/server/tests/plugins/filter-hooks.ts index d88170201..6996ae788 100644 --- a/server/tests/plugins/filter-hooks.ts +++ b/server/tests/plugins/filter-hooks.ts | |||
@@ -20,12 +20,14 @@ import { | |||
20 | getVideoThreadComments, | 20 | getVideoThreadComments, |
21 | getVideoWithToken, | 21 | getVideoWithToken, |
22 | installPlugin, | 22 | installPlugin, |
23 | makeRawRequest, | ||
23 | registerUser, | 24 | registerUser, |
24 | setAccessTokensToServers, | 25 | setAccessTokensToServers, |
25 | setDefaultVideoChannel, | 26 | setDefaultVideoChannel, |
26 | updateCustomSubConfig, | 27 | updateCustomSubConfig, |
27 | updateVideo, | 28 | updateVideo, |
28 | uploadVideo, | 29 | uploadVideo, |
30 | uploadVideoAndGetId, | ||
29 | waitJobs | 31 | waitJobs |
30 | } from '../../../shared/extra-utils' | 32 | } from '../../../shared/extra-utils' |
31 | import { cleanupTests, flushAndRunMultipleServers, ServerInfo } from '../../../shared/extra-utils/server/servers' | 33 | import { cleanupTests, flushAndRunMultipleServers, ServerInfo } from '../../../shared/extra-utils/server/servers' |
@@ -355,6 +357,67 @@ describe('Test plugin filter hooks', function () { | |||
355 | }) | 357 | }) |
356 | }) | 358 | }) |
357 | 359 | ||
360 | describe('Download hooks', function () { | ||
361 | const downloadVideos: VideoDetails[] = [] | ||
362 | |||
363 | before(async function () { | ||
364 | this.timeout(60000) | ||
365 | |||
366 | await updateCustomSubConfig(servers[0].url, servers[0].accessToken, { | ||
367 | transcoding: { | ||
368 | webtorrent: { | ||
369 | enabled: true | ||
370 | }, | ||
371 | hls: { | ||
372 | enabled: true | ||
373 | } | ||
374 | } | ||
375 | }) | ||
376 | |||
377 | const uuids: string[] = [] | ||
378 | |||
379 | for (const name of [ 'bad torrent', 'bad file', 'bad playlist file' ]) { | ||
380 | const uuid = (await uploadVideoAndGetId({ server: servers[0], videoName: name })).uuid | ||
381 | uuids.push(uuid) | ||
382 | } | ||
383 | |||
384 | await waitJobs(servers) | ||
385 | |||
386 | for (const uuid of uuids) { | ||
387 | const res = await getVideo(servers[0].url, uuid) | ||
388 | downloadVideos.push(res.body) | ||
389 | } | ||
390 | }) | ||
391 | |||
392 | it('Should run filter:api.download.torrent.allowed.result', async function () { | ||
393 | const res = await makeRawRequest(downloadVideos[0].files[0].torrentDownloadUrl, 403) | ||
394 | expect(res.body.error).to.equal('Liu Bei') | ||
395 | |||
396 | await makeRawRequest(downloadVideos[1].files[0].torrentDownloadUrl, 200) | ||
397 | await makeRawRequest(downloadVideos[2].files[0].torrentDownloadUrl, 200) | ||
398 | }) | ||
399 | |||
400 | it('Should run filter:api.download.video.allowed.result', async function () { | ||
401 | { | ||
402 | const res = await makeRawRequest(downloadVideos[1].files[0].fileDownloadUrl, 403) | ||
403 | expect(res.body.error).to.equal('Cao Cao') | ||
404 | |||
405 | await makeRawRequest(downloadVideos[0].files[0].fileDownloadUrl, 200) | ||
406 | await makeRawRequest(downloadVideos[2].files[0].fileDownloadUrl, 200) | ||
407 | } | ||
408 | |||
409 | { | ||
410 | const res = await makeRawRequest(downloadVideos[2].streamingPlaylists[0].files[0].fileDownloadUrl, 403) | ||
411 | expect(res.body.error).to.equal('Sun Jian') | ||
412 | |||
413 | await makeRawRequest(downloadVideos[2].files[0].fileDownloadUrl, 200) | ||
414 | |||
415 | await makeRawRequest(downloadVideos[0].streamingPlaylists[0].files[0].fileDownloadUrl, 200) | ||
416 | await makeRawRequest(downloadVideos[1].streamingPlaylists[0].files[0].fileDownloadUrl, 200) | ||
417 | } | ||
418 | }) | ||
419 | }) | ||
420 | |||
358 | after(async function () { | 421 | after(async function () { |
359 | await cleanupTests(servers) | 422 | await cleanupTests(servers) |
360 | }) | 423 | }) |