From 474542d7ac60f7860daf9ea34d1c31968f43ab29 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 9 Feb 2022 08:58:40 +0100 Subject: Add additional checks when importing a video --- server/controllers/api/videos/import.ts | 29 ++++++++++++- server/helpers/custom-validators/video-captions.ts | 13 ++++++ .../helpers/youtube-dl/youtube-dl-info-builder.ts | 49 ++++++++++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) (limited to 'server') diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts index 8cbfd3286..b54fa822c 100644 --- a/server/controllers/api/videos/import.ts +++ b/server/controllers/api/videos/import.ts @@ -1,9 +1,11 @@ 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' @@ -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) // 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) for (const subtitle of subtitles) { + if (!await isVTTFileValid(subtitle.path)) { + await remove(subtitle.path) + continue + } + const videoCaption = new VideoCaptionModel({ videoId, language: subtitle.language, @@ -449,3 +463,16 @@ async function processYoutubeSubtitles (youtubeDL: YoutubeDLWrapper, targetUrl: 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 +} diff --git a/server/helpers/custom-validators/video-captions.ts b/server/helpers/custom-validators/video-captions.ts index 528edf60c..4cc7dcaf4 100644 --- a/server/helpers/custom-validators/video-captions.ts +++ b/server/helpers/custom-validators/video-captions.ts @@ -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 { 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) } +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 } diff --git a/server/helpers/youtube-dl/youtube-dl-info-builder.ts b/server/helpers/youtube-dl/youtube-dl-info-builder.ts index 9746a7067..71572f292 100644 --- a/server/helpers/youtube-dl/youtube-dl-info-builder.ts +++ b/server/helpers/youtube-dl/youtube-dl-info-builder.ts @@ -1,5 +1,6 @@ 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 @@ -12,6 +13,8 @@ type YoutubeDLInfo = { thumbnailUrl?: string ext?: string originallyPublishedAt?: Date + + urls?: string[] } class YoutubeDLInfoBuilder { @@ -76,11 +79,57 @@ 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, -- cgit v1.2.3