# Changelog
-## v4.1.0-rc.1
+## v4.1.0-rc.1 (unreleased)
### IMPORTANT NOTES
<my-peertube-checkbox
inputName="importVideosHttpEnabled" formControlName="enabled"
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 class="form-group" formGroupName="torrent">
<my-peertube-checkbox
inputName="importVideosTorrentEnabled" formControlName="enabled"
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>
</ng-container>
},
{
path: 'video-redundancies-list',
- component: VideoRedundanciesListComponent
+ component: VideoRedundanciesListComponent,
+ data: {
+ meta: {
+ title: $localize`Redundancy`
+ }
+ }
}
]
}
# Amount of import jobs to execute in parallel
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
youtube_dl_release:
# IPv6 is very strongly rate-limited on most sites supported by youtube-dl
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
auto_blacklist:
# Amount of import jobs to execute in parallel
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
youtube_dl_release:
# IPv6 is very strongly rate-limited on most sites supported by youtube-dl
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
auto_blacklist:
import express from 'express'
-import { move, readFile } from 'fs-extra'
+import { move, readFile, remove } from 'fs-extra'
import { decode } from 'magnet-uri'
import parseTorrent, { Instance } from 'parse-torrent'
import { join } from 'path'
+import { isVTTFileValid } from '@server/helpers/custom-validators/video-captions'
import { isVideoFileExtnameValid } from '@server/helpers/custom-validators/videos'
+import { isResolvingToUnicastOnly } from '@server/helpers/dns'
import { Hooks } from '@server/lib/plugins/hooks'
import { ServerConfigManager } from '@server/lib/server-config-manager'
import { setVideoTags } from '@server/lib/video'
})
}
+ 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)
// Process video thumbnail from request.files
logger.info('Will create %s subtitles from youtube import %s.', subtitles.length, targetUrl)
for (const subtitle of subtitles) {
+ if (!await isVTTFileValid(subtitle.path)) {
+ await remove(subtitle.path)
+ continue
+ }
+
const videoCaption = new VideoCaptionModel({
videoId,
language: subtitle.language,
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
+}
+import { getFileSize } from '@shared/extra-utils'
+import { readFile } from 'fs-extra'
import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_LANGUAGES } from '../../initializers/constants'
import { exists, isFileValid } from './misc'
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 {
isVideoCaptionFile,
+ isVTTFileValid,
isVideoCaptionLanguageValid
}
import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../../initializers/constants'
import { peertubeTruncate } from '../core-utils'
+import { isUrlValid } from '../custom-validators/activitypub/misc'
type YoutubeDLInfo = {
name?: string
thumbnailUrl?: string
ext?: string
originallyPublishedAt?: Date
+
+ urls?: string[]
}
class YoutubeDLInfoBuilder {
nsfw: this.isNSFW(obj),
tags: this.getTags(obj.tags),
thumbnailUrl: obj.thumbnail || undefined,
+ urls: this.buildAvailableUrl(obj),
originallyPublishedAt: this.buildOriginallyPublishedAt(obj),
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) {
return peertubeTruncate(title, {
length: CONSTRAINTS_FIELDS.VIDEOS.NAME.max,