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'
videoFileMetadataVideoStream: FileMetadata | undefined
videoFileMetadataAudioStream: FileMetadata | undefined
videoCaptions: VideoCaption[]
- activeModal: NgbActiveModal
+ activeModal: NgbModalRef
type: DownloadType = 'video'
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)
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 () {
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)
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()
}
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'
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)
)
// ---------------------------------------------------------------------------
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[]) {
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
+}
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> {
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)
}
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 {
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 () {
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'
})
})
+ 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)
})
// 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
'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