Add additional checks when importing a video
This commit is contained in:
parent
ba8a8367e7
commit
474542d7ac
|
@ -1,6 +1,6 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## v4.1.0-rc.1
|
## v4.1.0-rc.1 (unreleased)
|
||||||
|
|
||||||
### IMPORTANT NOTES
|
### IMPORTANT NOTES
|
||||||
|
|
||||||
|
|
|
@ -267,14 +267,22 @@
|
||||||
<my-peertube-checkbox
|
<my-peertube-checkbox
|
||||||
inputName="importVideosHttpEnabled" formControlName="enabled"
|
inputName="importVideosHttpEnabled" formControlName="enabled"
|
||||||
i18n-labelText labelText="Allow import with HTTP URL (e.g. YouTube)"
|
i18n-labelText labelText="Allow import with HTTP URL (e.g. YouTube)"
|
||||||
></my-peertube-checkbox>
|
>
|
||||||
|
<ng-container ngProjectAs="description">
|
||||||
|
<span i18n>⚠️ If enabled, we recommend to use <a href="https://docs.joinpeertube.org/maintain-configuration?id=security">a HTTP proxy</a> to prevent private URL access from your PeerTube server</span>
|
||||||
|
</ng-container>
|
||||||
|
</my-peertube-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" formGroupName="torrent">
|
<div class="form-group" formGroupName="torrent">
|
||||||
<my-peertube-checkbox
|
<my-peertube-checkbox
|
||||||
inputName="importVideosTorrentEnabled" formControlName="enabled"
|
inputName="importVideosTorrentEnabled" formControlName="enabled"
|
||||||
i18n-labelText labelText="Allow import with a torrent file or a magnet URI"
|
i18n-labelText labelText="Allow import with a torrent file or a magnet URI"
|
||||||
></my-peertube-checkbox>
|
>
|
||||||
|
<ng-container ngProjectAs="description">
|
||||||
|
<span i18n>⚠️ We don't recommend to enable this feature if you don't trust your users</span>
|
||||||
|
</ng-container>
|
||||||
|
</my-peertube-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
|
@ -42,7 +42,12 @@ export const FollowsRoutes: Routes = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'video-redundancies-list',
|
path: 'video-redundancies-list',
|
||||||
component: VideoRedundanciesListComponent
|
component: VideoRedundanciesListComponent,
|
||||||
|
data: {
|
||||||
|
meta: {
|
||||||
|
title: $localize`Redundancy`
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -431,7 +431,10 @@ import:
|
||||||
# Amount of import jobs to execute in parallel
|
# Amount of import jobs to execute in parallel
|
||||||
concurrency: 1
|
concurrency: 1
|
||||||
|
|
||||||
http: # Classic HTTP or all sites supported by youtube-dl https://rg3.github.io/youtube-dl/supportedsites.html
|
# Classic HTTP or all sites supported by youtube-dl https://rg3.github.io/youtube-dl/supportedsites.html
|
||||||
|
http:
|
||||||
|
# We recommend to use a HTTP proxy if you enable HTTP import to prevent private URL access from this server
|
||||||
|
# See https://docs.joinpeertube.org/maintain-configuration?id=security for more information
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
youtube_dl_release:
|
youtube_dl_release:
|
||||||
|
@ -452,7 +455,10 @@ import:
|
||||||
# IPv6 is very strongly rate-limited on most sites supported by youtube-dl
|
# IPv6 is very strongly rate-limited on most sites supported by youtube-dl
|
||||||
force_ipv4: false
|
force_ipv4: false
|
||||||
|
|
||||||
torrent: # Magnet URI or torrent file (use classic TCP/UDP/WebSeed to download the file)
|
# Magnet URI or torrent file (use classic TCP/UDP/WebSeed to download the file)
|
||||||
|
torrent:
|
||||||
|
# We recommend to only enable magnet URI/torrent import if you trust your users
|
||||||
|
# See https://docs.joinpeertube.org/maintain-configuration?id=security for more information
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
auto_blacklist:
|
auto_blacklist:
|
||||||
|
|
|
@ -439,7 +439,10 @@ import:
|
||||||
# Amount of import jobs to execute in parallel
|
# Amount of import jobs to execute in parallel
|
||||||
concurrency: 1
|
concurrency: 1
|
||||||
|
|
||||||
http: # Classic HTTP or all sites supported by youtube-dl https://rg3.github.io/youtube-dl/supportedsites.html
|
# Classic HTTP or all sites supported by youtube-dl https://rg3.github.io/youtube-dl/supportedsites.html
|
||||||
|
http:
|
||||||
|
# We recommend to use a HTTP proxy if you enable HTTP import to prevent private URL access from this server
|
||||||
|
# See https://docs.joinpeertube.org/maintain-configuration?id=security for more information
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
youtube_dl_release:
|
youtube_dl_release:
|
||||||
|
@ -460,7 +463,10 @@ import:
|
||||||
# IPv6 is very strongly rate-limited on most sites supported by youtube-dl
|
# IPv6 is very strongly rate-limited on most sites supported by youtube-dl
|
||||||
force_ipv4: false
|
force_ipv4: false
|
||||||
|
|
||||||
torrent: # Magnet URI or torrent file (use classic TCP/UDP/WebSeed to download the file)
|
# Magnet URI or torrent file (use classic TCP/UDP/WebSeed to download the file)
|
||||||
|
torrent:
|
||||||
|
# We recommend to only enable magnet URI/torrent import if you trust your users
|
||||||
|
# See https://docs.joinpeertube.org/maintain-configuration?id=security for more information
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
auto_blacklist:
|
auto_blacklist:
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { move, readFile } from 'fs-extra'
|
import { move, readFile, remove } from 'fs-extra'
|
||||||
import { decode } from 'magnet-uri'
|
import { decode } from 'magnet-uri'
|
||||||
import parseTorrent, { Instance } from 'parse-torrent'
|
import parseTorrent, { Instance } from 'parse-torrent'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
import { isVTTFileValid } from '@server/helpers/custom-validators/video-captions'
|
||||||
import { isVideoFileExtnameValid } from '@server/helpers/custom-validators/videos'
|
import { isVideoFileExtnameValid } from '@server/helpers/custom-validators/videos'
|
||||||
|
import { isResolvingToUnicastOnly } from '@server/helpers/dns'
|
||||||
import { Hooks } from '@server/lib/plugins/hooks'
|
import { Hooks } from '@server/lib/plugins/hooks'
|
||||||
import { ServerConfigManager } from '@server/lib/server-config-manager'
|
import { ServerConfigManager } from '@server/lib/server-config-manager'
|
||||||
import { setVideoTags } from '@server/lib/video'
|
import { setVideoTags } from '@server/lib/video'
|
||||||
|
@ -195,6 +197,13 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!await hasUnicastURLsOnly(youtubeDLInfo)) {
|
||||||
|
return res.fail({
|
||||||
|
status: HttpStatusCode.FORBIDDEN_403,
|
||||||
|
message: 'Cannot use non unicast IP as targetUrl.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const video = await buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo)
|
const video = await buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo)
|
||||||
|
|
||||||
// Process video thumbnail from request.files
|
// Process video thumbnail from request.files
|
||||||
|
@ -432,6 +441,11 @@ async function processYoutubeSubtitles (youtubeDL: YoutubeDLWrapper, targetUrl:
|
||||||
logger.info('Will create %s subtitles from youtube import %s.', subtitles.length, targetUrl)
|
logger.info('Will create %s subtitles from youtube import %s.', subtitles.length, targetUrl)
|
||||||
|
|
||||||
for (const subtitle of subtitles) {
|
for (const subtitle of subtitles) {
|
||||||
|
if (!await isVTTFileValid(subtitle.path)) {
|
||||||
|
await remove(subtitle.path)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
const videoCaption = new VideoCaptionModel({
|
const videoCaption = new VideoCaptionModel({
|
||||||
videoId,
|
videoId,
|
||||||
language: subtitle.language,
|
language: subtitle.language,
|
||||||
|
@ -449,3 +463,16 @@ async function processYoutubeSubtitles (youtubeDL: YoutubeDLWrapper, targetUrl:
|
||||||
logger.warn('Cannot get video subtitles.', { err })
|
logger.warn('Cannot get video subtitles.', { err })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function hasUnicastURLsOnly (youtubeDLInfo: YoutubeDLInfo) {
|
||||||
|
const hosts = youtubeDLInfo.urls.map(u => new URL(u).hostname)
|
||||||
|
const uniqHosts = new Set(hosts)
|
||||||
|
|
||||||
|
for (const h of uniqHosts) {
|
||||||
|
if (await isResolvingToUnicastOnly(h) !== true) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { getFileSize } from '@shared/extra-utils'
|
||||||
|
import { readFile } from 'fs-extra'
|
||||||
import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_LANGUAGES } from '../../initializers/constants'
|
import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_LANGUAGES } from '../../initializers/constants'
|
||||||
import { exists, isFileValid } from './misc'
|
import { exists, isFileValid } from './misc'
|
||||||
|
|
||||||
|
@ -13,9 +15,20 @@ function isVideoCaptionFile (files: { [ fieldname: string ]: Express.Multer.File
|
||||||
return isFileValid(files, videoCaptionTypesRegex, field, CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max)
|
return isFileValid(files, videoCaptionTypesRegex, field, CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function isVTTFileValid (filePath: string) {
|
||||||
|
const size = await getFileSize(filePath)
|
||||||
|
|
||||||
|
if (size > CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max) return false
|
||||||
|
|
||||||
|
const content = await readFile(filePath, 'utf8')
|
||||||
|
|
||||||
|
return content?.startsWith('WEBVTT\n')
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
isVideoCaptionFile,
|
isVideoCaptionFile,
|
||||||
|
isVTTFileValid,
|
||||||
isVideoCaptionLanguageValid
|
isVideoCaptionLanguageValid
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../../initializers/constants'
|
import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../../initializers/constants'
|
||||||
import { peertubeTruncate } from '../core-utils'
|
import { peertubeTruncate } from '../core-utils'
|
||||||
|
import { isUrlValid } from '../custom-validators/activitypub/misc'
|
||||||
|
|
||||||
type YoutubeDLInfo = {
|
type YoutubeDLInfo = {
|
||||||
name?: string
|
name?: string
|
||||||
|
@ -12,6 +13,8 @@ type YoutubeDLInfo = {
|
||||||
thumbnailUrl?: string
|
thumbnailUrl?: string
|
||||||
ext?: string
|
ext?: string
|
||||||
originallyPublishedAt?: Date
|
originallyPublishedAt?: Date
|
||||||
|
|
||||||
|
urls?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
class YoutubeDLInfoBuilder {
|
class YoutubeDLInfoBuilder {
|
||||||
|
@ -76,11 +79,57 @@ class YoutubeDLInfoBuilder {
|
||||||
nsfw: this.isNSFW(obj),
|
nsfw: this.isNSFW(obj),
|
||||||
tags: this.getTags(obj.tags),
|
tags: this.getTags(obj.tags),
|
||||||
thumbnailUrl: obj.thumbnail || undefined,
|
thumbnailUrl: obj.thumbnail || undefined,
|
||||||
|
urls: this.buildAvailableUrl(obj),
|
||||||
originallyPublishedAt: this.buildOriginallyPublishedAt(obj),
|
originallyPublishedAt: this.buildOriginallyPublishedAt(obj),
|
||||||
ext: obj.ext
|
ext: obj.ext
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildAvailableUrl (obj: any) {
|
||||||
|
const urls: string[] = []
|
||||||
|
|
||||||
|
if (obj.url) urls.push(obj.url)
|
||||||
|
if (obj.urls) {
|
||||||
|
if (Array.isArray(obj.urls)) urls.push(...obj.urls)
|
||||||
|
else urls.push(obj.urls)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formats = Array.isArray(obj.formats)
|
||||||
|
? obj.formats
|
||||||
|
: []
|
||||||
|
|
||||||
|
for (const format of formats) {
|
||||||
|
if (!format.url) continue
|
||||||
|
|
||||||
|
urls.push(format.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
const thumbnails = Array.isArray(obj.thumbnails)
|
||||||
|
? obj.thumbnails
|
||||||
|
: []
|
||||||
|
|
||||||
|
for (const thumbnail of thumbnails) {
|
||||||
|
if (!thumbnail.url) continue
|
||||||
|
|
||||||
|
urls.push(thumbnail.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.thumbnail) urls.push(obj.thumbnail)
|
||||||
|
|
||||||
|
for (const subtitleKey of Object.keys(obj.subtitles || {})) {
|
||||||
|
const subtitles = obj.subtitles[subtitleKey]
|
||||||
|
if (!Array.isArray(subtitles)) continue
|
||||||
|
|
||||||
|
for (const subtitle of subtitles) {
|
||||||
|
if (!subtitle.url) continue
|
||||||
|
|
||||||
|
urls.push(subtitle.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return urls.filter(u => u && isUrlValid(u))
|
||||||
|
}
|
||||||
|
|
||||||
private titleTruncation (title: string) {
|
private titleTruncation (title: string) {
|
||||||
return peertubeTruncate(title, {
|
return peertubeTruncate(title, {
|
||||||
length: CONSTRAINTS_FIELDS.VIDEOS.NAME.max,
|
length: CONSTRAINTS_FIELDS.VIDEOS.NAME.max,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user