Add hooks support for video download

This commit is contained in:
Chocobozzz 2021-03-23 11:54:08 +01:00
parent c0ab041c2c
commit 4bc45da342
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
7 changed files with 198 additions and 20 deletions

View File

@ -1,7 +1,9 @@
import { mapValues, pick } from 'lodash-es'
import { pipe } from 'rxjs'
import { tap } from 'rxjs/operators'
import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core'
import { AuthService, Notifier } from '@app/core'
import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { AuthService, HooksService, Notifier } from '@app/core'
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models'
import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoService } from '../shared-main'
@ -26,7 +28,7 @@ export class VideoDownloadComponent {
videoFileMetadataVideoStream: FileMetadata | undefined
videoFileMetadataAudioStream: FileMetadata | undefined
videoCaptions: VideoCaption[]
activeModal: NgbActiveModal
activeModal: NgbModalRef
type: DownloadType = 'video'
@ -38,7 +40,8 @@ export class VideoDownloadComponent {
private notifier: Notifier,
private modalService: NgbModal,
private videoService: VideoService,
private auth: AuthService
private auth: AuthService,
private hooks: HooksService
) {
this.bytesPipe = new BytesPipe()
this.numbersPipe = new NumberFormatterPipe(this.localeId)
@ -64,7 +67,12 @@ export class VideoDownloadComponent {
this.resolutionId = this.getVideoFiles()[0].resolution.id
this.onResolutionIdChange()
if (this.videoCaptions) this.subtitleLanguageId = this.videoCaptions[0].language.id
this.activeModal.shown.subscribe(() => {
this.hooks.runAction('action:modal.video-download.shown', 'common')
})
}
onClose () {
@ -88,6 +96,7 @@ export class VideoDownloadComponent {
if (this.videoFile.metadata || !this.videoFile.metadataUrl) return
await this.hydrateMetadataFromMetadataUrl(this.videoFile)
if (!this.videoFile.metadata) return
this.videoFileMetadataFormat = this.videoFile
? this.getMetadataFormat(this.videoFile.metadata.format)
@ -201,7 +210,7 @@ export class VideoDownloadComponent {
private hydrateMetadataFromMetadataUrl (file: VideoFile) {
const observable = this.videoService.getVideoFileMetadata(file.metadataUrl)
observable.subscribe(res => file.metadata = res)
.pipe(tap(res => file.metadata = res))
return observable.toPromise()
}

View File

@ -1,8 +1,10 @@
import * as cors from 'cors'
import * as express from 'express'
import { logger } from '@server/helpers/logger'
import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache'
import { Hooks } from '@server/lib/plugins/hooks'
import { getVideoFilePath } from '@server/lib/video-paths'
import { MVideoFile, MVideoFullLight } from '@server/types/models'
import { MStreamingPlaylist, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
import { VideoStreamingPlaylistType } from '@shared/models'
import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants'
@ -14,19 +16,19 @@ downloadRouter.use(cors())
downloadRouter.use(
STATIC_DOWNLOAD_PATHS.TORRENTS + ':filename',
downloadTorrent
asyncMiddleware(downloadTorrent)
)
downloadRouter.use(
STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension',
asyncMiddleware(videosDownloadValidator),
downloadVideoFile
asyncMiddleware(downloadVideoFile)
)
downloadRouter.use(
STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension',
asyncMiddleware(videosDownloadValidator),
downloadHLSVideoFile
asyncMiddleware(downloadHLSVideoFile)
)
// ---------------------------------------------------------------------------
@ -41,28 +43,58 @@ async function downloadTorrent (req: express.Request, res: express.Response) {
const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename)
if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
const allowParameters = { torrentPath: result.path, downloadName: result.downloadName }
const allowedResult = await Hooks.wrapFun(
isTorrentDownloadAllowed,
allowParameters,
'filter:api.download.torrent.allowed.result'
)
if (!checkAllowResult(res, allowParameters, allowedResult)) return
return res.download(result.path, result.downloadName)
}
function downloadVideoFile (req: express.Request, res: express.Response) {
async function downloadVideoFile (req: express.Request, res: express.Response) {
const video = res.locals.videoAll
const videoFile = getVideoFile(req, video.VideoFiles)
if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end()
const allowParameters = { video, videoFile }
const allowedResult = await Hooks.wrapFun(
isVideoDownloadAllowed,
allowParameters,
'filter:api.download.video.allowed.result'
)
if (!checkAllowResult(res, allowParameters, allowedResult)) return
return res.download(getVideoFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`)
}
function downloadHLSVideoFile (req: express.Request, res: express.Response) {
async function downloadHLSVideoFile (req: express.Request, res: express.Response) {
const video = res.locals.videoAll
const playlist = getHLSPlaylist(video)
if (!playlist) return res.status(HttpStatusCode.NOT_FOUND_404).end
const streamingPlaylist = getHLSPlaylist(video)
if (!streamingPlaylist) return res.status(HttpStatusCode.NOT_FOUND_404).end
const videoFile = getVideoFile(req, playlist.VideoFiles)
const videoFile = getVideoFile(req, streamingPlaylist.VideoFiles)
if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end()
const filename = `${video.name}-${videoFile.resolution}p-${playlist.getStringType()}${videoFile.extname}`
return res.download(getVideoFilePath(playlist, videoFile), filename)
const allowParameters = { video, streamingPlaylist, videoFile }
const allowedResult = await Hooks.wrapFun(
isVideoDownloadAllowed,
allowParameters,
'filter:api.download.video.allowed.result'
)
if (!checkAllowResult(res, allowParameters, allowedResult)) return
const filename = `${video.name}-${videoFile.resolution}p-${streamingPlaylist.getStringType()}${videoFile.extname}`
return res.download(getVideoFilePath(streamingPlaylist, videoFile), filename)
}
function getVideoFile (req: express.Request, files: MVideoFile[]) {
@ -76,3 +108,34 @@ function getHLSPlaylist (video: MVideoFullLight) {
return Object.assign(playlist, { Video: video })
}
type AllowedResult = {
allowed: boolean
errorMessage?: string
}
function isTorrentDownloadAllowed (_object: {
torrentPath: string
}): AllowedResult {
return { allowed: true }
}
function isVideoDownloadAllowed (_object: {
video: MVideo
videoFile: MVideoFile
streamingPlaylist?: MStreamingPlaylist
}): AllowedResult {
return { allowed: true }
}
function checkAllowResult (res: express.Response, allowParameters: any, result?: AllowedResult) {
if (!result || result.allowed !== true) {
logger.info('Download is not allowed.', { result, allowParameters })
res.status(HttpStatusCode.FORBIDDEN_403)
.json({ error: result.errorMessage || 'Refused download' })
return false
}
return true
}

View File

@ -5,6 +5,7 @@ import { CONFIG } from '../../initializers/config'
import { FILES_CACHE } from '../../initializers/constants'
import { VideoModel } from '../../models/video/video'
import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache'
import { MVideo, MVideoFile } from '@server/types/models'
class VideosTorrentCache extends AbstractVideoStaticFileCache <string> {
@ -22,7 +23,11 @@ class VideosTorrentCache extends AbstractVideoStaticFileCache <string> {
const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(filename)
if (!file) return undefined
if (file.getVideo().isOwned()) return { isOwned: true, path: join(CONFIG.STORAGE.TORRENTS_DIR, file.torrentFilename) }
if (file.getVideo().isOwned()) {
const downloadName = this.buildDownloadName(file.getVideo(), file)
return { isOwned: true, path: join(CONFIG.STORAGE.TORRENTS_DIR, file.torrentFilename), downloadName }
}
return this.loadRemoteFile(filename)
}
@ -43,10 +48,14 @@ class VideosTorrentCache extends AbstractVideoStaticFileCache <string> {
await doRequestAndSaveToFile(remoteUrl, destPath)
const downloadName = `${video.name}-${file.resolution}p.torrent`
const downloadName = this.buildDownloadName(video, file)
return { isOwned: false, path: destPath, downloadName }
}
private buildDownloadName (video: MVideo, file: MVideoFile) {
return `${video.name}-${file.resolution}p.torrent`
}
}
export {

View File

@ -184,6 +184,32 @@ async function register ({ registerHook, registerSetting, settingsManager, stora
return result
}
})
registerHook({
target: 'filter:api.download.torrent.allowed.result',
handler: (result, params) => {
if (params && params.downloadName.includes('bad torrent')) {
return { allowed: false, errorMessage: 'Liu Bei' }
}
return result
}
})
registerHook({
target: 'filter:api.download.video.allowed.result',
handler: (result, params) => {
if (params && !params.streamingPlaylist && params.video.name.includes('bad file')) {
return { allowed: false, errorMessage: 'Cao Cao' }
}
if (params && params.streamingPlaylist && params.video.name.includes('bad playlist file')) {
return { allowed: false, errorMessage: 'Sun Jian' }
}
return result
}
})
}
async function unregister () {

View File

@ -20,12 +20,14 @@ import {
getVideoThreadComments,
getVideoWithToken,
installPlugin,
makeRawRequest,
registerUser,
setAccessTokensToServers,
setDefaultVideoChannel,
updateCustomSubConfig,
updateVideo,
uploadVideo,
uploadVideoAndGetId,
waitJobs
} from '../../../shared/extra-utils'
import { cleanupTests, flushAndRunMultipleServers, ServerInfo } from '../../../shared/extra-utils/server/servers'
@ -355,6 +357,67 @@ describe('Test plugin filter hooks', function () {
})
})
describe('Download hooks', function () {
const downloadVideos: VideoDetails[] = []
before(async function () {
this.timeout(60000)
await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
transcoding: {
webtorrent: {
enabled: true
},
hls: {
enabled: true
}
}
})
const uuids: string[] = []
for (const name of [ 'bad torrent', 'bad file', 'bad playlist file' ]) {
const uuid = (await uploadVideoAndGetId({ server: servers[0], videoName: name })).uuid
uuids.push(uuid)
}
await waitJobs(servers)
for (const uuid of uuids) {
const res = await getVideo(servers[0].url, uuid)
downloadVideos.push(res.body)
}
})
it('Should run filter:api.download.torrent.allowed.result', async function () {
const res = await makeRawRequest(downloadVideos[0].files[0].torrentDownloadUrl, 403)
expect(res.body.error).to.equal('Liu Bei')
await makeRawRequest(downloadVideos[1].files[0].torrentDownloadUrl, 200)
await makeRawRequest(downloadVideos[2].files[0].torrentDownloadUrl, 200)
})
it('Should run filter:api.download.video.allowed.result', async function () {
{
const res = await makeRawRequest(downloadVideos[1].files[0].fileDownloadUrl, 403)
expect(res.body.error).to.equal('Cao Cao')
await makeRawRequest(downloadVideos[0].files[0].fileDownloadUrl, 200)
await makeRawRequest(downloadVideos[2].files[0].fileDownloadUrl, 200)
}
{
const res = await makeRawRequest(downloadVideos[2].streamingPlaylists[0].files[0].fileDownloadUrl, 403)
expect(res.body.error).to.equal('Sun Jian')
await makeRawRequest(downloadVideos[2].files[0].fileDownloadUrl, 200)
await makeRawRequest(downloadVideos[0].streamingPlaylists[0].files[0].fileDownloadUrl, 200)
await makeRawRequest(downloadVideos[1].streamingPlaylists[0].files[0].fileDownloadUrl, 200)
}
})
})
after(async function () {
await cleanupTests(servers)
})

View File

@ -85,8 +85,12 @@ export const clientActionHookObject = {
// Fired when the registration page is being initialized
'action:signup.register.init': true,
// Fired when the modal to download a video/caption is shown
'action:modal.video-download.shown': true,
// ####### Embed hooks #######
// In embed scope, peertube helpers are not available
// /!\ In embed scope, peertube helpers are not available
// ###########################
// Fired when the embed loaded the player
'action:embed.player.loaded': true

View File

@ -50,7 +50,11 @@ export const serverFilterHookObject = {
'filter:video.auto-blacklist.result': true,
// Filter result used to check if a user can register on the instance
'filter:api.user.signup.allowed.result': true
'filter:api.user.signup.allowed.result': true,
// Filter result used to check if video/torrent download is allowed
'filter:api.download.video.allowed.result': true,
'filter:api.download.torrent.allowed.result': true
}
export type ServerFilterHookName = keyof typeof serverFilterHookObject