]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add additional checks when importing a video
authorChocobozzz <me@florianbigard.com>
Wed, 9 Feb 2022 07:58:40 +0000 (08:58 +0100)
committerChocobozzz <me@florianbigard.com>
Wed, 9 Feb 2022 10:54:18 +0000 (11:54 +0100)
CHANGELOG.md
client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html
client/src/app/+admin/follows/follows.routes.ts
config/default.yaml
config/production.yaml.example
server/controllers/api/videos/import.ts
server/helpers/custom-validators/video-captions.ts
server/helpers/youtube-dl/youtube-dl-info-builder.ts

index f8a44e9a78c014c3f7bdad77fe97a5855e6da8a4..16a74f31506866fe56cf42b668a82e227db43f7a 100644 (file)
@@ -1,6 +1,6 @@
 # Changelog
 
-## v4.1.0-rc.1
+## v4.1.0-rc.1 (unreleased)
 
 ### IMPORTANT NOTES
 
index c9533208a14fb73a559c2feac0e2eeed7b48feed..37989cb59cc645ad5d6c8463f42cd1770aeed7d8 100644 (file)
             <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>
index 1825ce2789c9ea767d55506ce7b6f9f7194bd3ac..718493dc74a52f5d75d80a81112b406bc0245cee 100644 (file)
@@ -42,7 +42,12 @@ export const FollowsRoutes: Routes = [
       },
       {
         path: 'video-redundancies-list',
-        component: VideoRedundanciesListComponent
+        component: VideoRedundanciesListComponent,
+        data: {
+          meta: {
+            title: $localize`Redundancy`
+          }
+        }
       }
     ]
   }
index 2b0419535874e7b145dd5b62cb55c0fd0aabcc73..23be08f85e6b187c2085dab84c6872ceb015af5a 100644 (file)
@@ -431,7 +431,10 @@ import:
     # 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:
@@ -452,7 +455,10 @@ import:
       # 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:
index 893ccc33ed28c6d0adac4ad6b12c162d3acfaf63..675801caa0f938b53801c42d3306f27e6610b0f8 100644 (file)
@@ -439,7 +439,10 @@ import:
     # 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:
@@ -460,7 +463,10 @@ import:
       # 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:
index 8cbfd3286458fa61e8fc6c054191dcad7a2e4841..b54fa822c29736dfef3fccf7a49c5b95e3246566 100644 (file)
@@ -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
+}
index 528edf60ce6810a01a19b1491eb42f7438e38b8d..4cc7dcaf4ab62845f2ac6de572d343622a57ca7f 100644 (file)
@@ -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
 }
index 9746a7067a17b043340f47bfcdf3150617302e1a..71572f2920d2d8f927a08e867da50e0795a7a712 100644 (file)
@@ -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,