diff options
Diffstat (limited to 'server/helpers')
-rw-r--r-- | server/helpers/core-utils.ts | 14 | ||||
-rw-r--r-- | server/helpers/custom-validators/activitypub/videos.ts | 10 | ||||
-rw-r--r-- | server/helpers/custom-validators/misc.ts | 2 | ||||
-rw-r--r-- | server/helpers/custom-validators/plugins.ts | 22 | ||||
-rw-r--r-- | server/helpers/custom-validators/servers.ts | 4 | ||||
-rw-r--r-- | server/helpers/custom-validators/video-studio.ts | 3 | ||||
-rw-r--r-- | server/helpers/custom-validators/videos.ts | 5 | ||||
-rw-r--r-- | server/helpers/ffmpeg/ffmpeg-commons.ts | 2 | ||||
-rw-r--r-- | server/helpers/ffmpeg/ffmpeg-vod.ts | 13 | ||||
-rw-r--r-- | server/helpers/ffmpeg/ffprobe-utils.ts | 9 | ||||
-rw-r--r-- | server/helpers/otp.ts | 58 | ||||
-rw-r--r-- | server/helpers/peertube-crypto.ts | 49 | ||||
-rw-r--r-- | server/helpers/upload.ts | 6 | ||||
-rw-r--r-- | server/helpers/video.ts | 5 | ||||
-rw-r--r-- | server/helpers/webtorrent.ts | 16 | ||||
-rw-r--r-- | server/helpers/youtube-dl/youtube-dl-cli.ts | 15 | ||||
-rw-r--r-- | server/helpers/youtube-dl/youtube-dl-wrapper.ts | 2 |
17 files changed, 192 insertions, 43 deletions
diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts index c762f6a29..73bd994c1 100644 --- a/server/helpers/core-utils.ts +++ b/server/helpers/core-utils.ts | |||
@@ -6,7 +6,7 @@ | |||
6 | */ | 6 | */ |
7 | 7 | ||
8 | import { exec, ExecOptions } from 'child_process' | 8 | import { exec, ExecOptions } from 'child_process' |
9 | import { ED25519KeyPairOptions, generateKeyPair, randomBytes, RSAKeyPairOptions } from 'crypto' | 9 | import { ED25519KeyPairOptions, generateKeyPair, randomBytes, RSAKeyPairOptions, scrypt } from 'crypto' |
10 | import { truncate } from 'lodash' | 10 | import { truncate } from 'lodash' |
11 | import { pipeline } from 'stream' | 11 | import { pipeline } from 'stream' |
12 | import { URL } from 'url' | 12 | import { URL } from 'url' |
@@ -311,7 +311,17 @@ function promisify2<T, U, A> (func: (arg1: T, arg2: U, cb: (err: any, result: A) | |||
311 | } | 311 | } |
312 | } | 312 | } |
313 | 313 | ||
314 | // eslint-disable-next-line max-len | ||
315 | function promisify3<T, U, V, A> (func: (arg1: T, arg2: U, arg3: V, cb: (err: any, result: A) => void) => void): (arg1: T, arg2: U, arg3: V) => Promise<A> { | ||
316 | return function promisified (arg1: T, arg2: U, arg3: V): Promise<A> { | ||
317 | return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => { | ||
318 | func.apply(null, [ arg1, arg2, arg3, (err: any, res: A) => err ? reject(err) : resolve(res) ]) | ||
319 | }) | ||
320 | } | ||
321 | } | ||
322 | |||
314 | const randomBytesPromise = promisify1<number, Buffer>(randomBytes) | 323 | const randomBytesPromise = promisify1<number, Buffer>(randomBytes) |
324 | const scryptPromise = promisify3<string, string, number, Buffer>(scrypt) | ||
315 | const execPromise2 = promisify2<string, any, string>(exec) | 325 | const execPromise2 = promisify2<string, any, string>(exec) |
316 | const execPromise = promisify1<string, string>(exec) | 326 | const execPromise = promisify1<string, string>(exec) |
317 | const pipelinePromise = promisify(pipeline) | 327 | const pipelinePromise = promisify(pipeline) |
@@ -339,6 +349,8 @@ export { | |||
339 | promisify1, | 349 | promisify1, |
340 | promisify2, | 350 | promisify2, |
341 | 351 | ||
352 | scryptPromise, | ||
353 | |||
342 | randomBytesPromise, | 354 | randomBytesPromise, |
343 | 355 | ||
344 | generateRSAKeyPairPromise, | 356 | generateRSAKeyPairPromise, |
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts index 2a2f008b9..97b3577af 100644 --- a/server/helpers/custom-validators/activitypub/videos.ts +++ b/server/helpers/custom-validators/activitypub/videos.ts | |||
@@ -7,11 +7,11 @@ import { peertubeTruncate } from '../../core-utils' | |||
7 | import { isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc' | 7 | import { isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc' |
8 | import { isLiveLatencyModeValid } from '../video-lives' | 8 | import { isLiveLatencyModeValid } from '../video-lives' |
9 | import { | 9 | import { |
10 | isVideoDescriptionValid, | ||
10 | isVideoDurationValid, | 11 | isVideoDurationValid, |
11 | isVideoNameValid, | 12 | isVideoNameValid, |
12 | isVideoStateValid, | 13 | isVideoStateValid, |
13 | isVideoTagValid, | 14 | isVideoTagValid, |
14 | isVideoTruncatedDescriptionValid, | ||
15 | isVideoViewsValid | 15 | isVideoViewsValid |
16 | } from '../videos' | 16 | } from '../videos' |
17 | import { isActivityPubUrlValid, isActivityPubVideoDurationValid, isBaseActivityValid, setValidAttributedTo } from './misc' | 17 | import { isActivityPubUrlValid, isActivityPubVideoDurationValid, isBaseActivityValid, setValidAttributedTo } from './misc' |
@@ -32,7 +32,7 @@ function sanitizeAndCheckVideoTorrentObject (video: any) { | |||
32 | logger.debug('Video has invalid urls', { video }) | 32 | logger.debug('Video has invalid urls', { video }) |
33 | return false | 33 | return false |
34 | } | 34 | } |
35 | if (!setRemoteVideoTruncatedContent(video)) { | 35 | if (!setRemoteVideoContent(video)) { |
36 | logger.debug('Video has invalid content', { video }) | 36 | logger.debug('Video has invalid content', { video }) |
37 | return false | 37 | return false |
38 | } | 38 | } |
@@ -168,7 +168,7 @@ function isRemoteStringIdentifierValid (data: any) { | |||
168 | } | 168 | } |
169 | 169 | ||
170 | function isRemoteVideoContentValid (mediaType: string, content: string) { | 170 | function isRemoteVideoContentValid (mediaType: string, content: string) { |
171 | return mediaType === 'text/markdown' && isVideoTruncatedDescriptionValid(content) | 171 | return mediaType === 'text/markdown' && isVideoDescriptionValid(content) |
172 | } | 172 | } |
173 | 173 | ||
174 | function setValidRemoteIcon (video: any) { | 174 | function setValidRemoteIcon (video: any) { |
@@ -194,9 +194,9 @@ function setValidRemoteVideoUrls (video: any) { | |||
194 | return true | 194 | return true |
195 | } | 195 | } |
196 | 196 | ||
197 | function setRemoteVideoTruncatedContent (video: any) { | 197 | function setRemoteVideoContent (video: any) { |
198 | if (video.content) { | 198 | if (video.content) { |
199 | video.content = peertubeTruncate(video.content, { length: CONSTRAINTS_FIELDS.VIDEOS.TRUNCATED_DESCRIPTION.max }) | 199 | video.content = peertubeTruncate(video.content, { length: CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max }) |
200 | } | 200 | } |
201 | 201 | ||
202 | return true | 202 | return true |
diff --git a/server/helpers/custom-validators/misc.ts b/server/helpers/custom-validators/misc.ts index 17750379d..3dc5504e3 100644 --- a/server/helpers/custom-validators/misc.ts +++ b/server/helpers/custom-validators/misc.ts | |||
@@ -86,7 +86,7 @@ function isFileValid (options: { | |||
86 | 86 | ||
87 | // The file exists | 87 | // The file exists |
88 | const file = fileArray[0] | 88 | const file = fileArray[0] |
89 | if (!file || !file.originalname) return false | 89 | if (!file?.originalname) return false |
90 | 90 | ||
91 | // Check size | 91 | // Check size |
92 | if ((maxSize !== null) && file.size > maxSize) return false | 92 | if ((maxSize !== null) && file.size > maxSize) return false |
diff --git a/server/helpers/custom-validators/plugins.ts b/server/helpers/custom-validators/plugins.ts index 60b29dc89..a20de0c4a 100644 --- a/server/helpers/custom-validators/plugins.ts +++ b/server/helpers/custom-validators/plugins.ts | |||
@@ -1,9 +1,9 @@ | |||
1 | import { exists, isArray, isSafePath } from './misc' | ||
2 | import validator from 'validator' | 1 | import validator from 'validator' |
2 | import { PluginPackageJSON } from '../../../shared/models/plugins/plugin-package-json.model' | ||
3 | import { PluginType } from '../../../shared/models/plugins/plugin.type' | 3 | import { PluginType } from '../../../shared/models/plugins/plugin.type' |
4 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | 4 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' |
5 | import { PluginPackageJSON } from '../../../shared/models/plugins/plugin-package-json.model' | ||
6 | import { isUrlValid } from './activitypub/misc' | 5 | import { isUrlValid } from './activitypub/misc' |
6 | import { exists, isArray, isSafePath } from './misc' | ||
7 | 7 | ||
8 | const PLUGINS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.PLUGINS | 8 | const PLUGINS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.PLUGINS |
9 | 9 | ||
@@ -29,7 +29,7 @@ function isPluginDescriptionValid (value: string) { | |||
29 | return exists(value) && validator.isLength(value, PLUGINS_CONSTRAINTS_FIELDS.DESCRIPTION) | 29 | return exists(value) && validator.isLength(value, PLUGINS_CONSTRAINTS_FIELDS.DESCRIPTION) |
30 | } | 30 | } |
31 | 31 | ||
32 | function isPluginVersionValid (value: string) { | 32 | function isPluginStableVersionValid (value: string) { |
33 | if (!exists(value)) return false | 33 | if (!exists(value)) return false |
34 | 34 | ||
35 | const parts = (value + '').split('.') | 35 | const parts = (value + '').split('.') |
@@ -37,6 +37,19 @@ function isPluginVersionValid (value: string) { | |||
37 | return parts.length === 3 && parts.every(p => validator.isInt(p)) | 37 | return parts.length === 3 && parts.every(p => validator.isInt(p)) |
38 | } | 38 | } |
39 | 39 | ||
40 | function isPluginStableOrUnstableVersionValid (value: string) { | ||
41 | if (!exists(value)) return false | ||
42 | |||
43 | // suffix is beta.x or alpha.x | ||
44 | const [ stable, suffix ] = value.split('-') | ||
45 | if (!isPluginStableVersionValid(stable)) return false | ||
46 | |||
47 | const suffixRegex = /^(rc|alpha|beta)\.\d+$/ | ||
48 | if (suffix && !suffixRegex.test(suffix)) return false | ||
49 | |||
50 | return true | ||
51 | } | ||
52 | |||
40 | function isPluginEngineValid (engine: any) { | 53 | function isPluginEngineValid (engine: any) { |
41 | return exists(engine) && exists(engine.peertube) | 54 | return exists(engine) && exists(engine.peertube) |
42 | } | 55 | } |
@@ -156,7 +169,8 @@ export { | |||
156 | isPackageJSONValid, | 169 | isPackageJSONValid, |
157 | isThemeNameValid, | 170 | isThemeNameValid, |
158 | isPluginHomepage, | 171 | isPluginHomepage, |
159 | isPluginVersionValid, | 172 | isPluginStableVersionValid, |
173 | isPluginStableOrUnstableVersionValid, | ||
160 | isPluginNameValid, | 174 | isPluginNameValid, |
161 | isPluginDescriptionValid, | 175 | isPluginDescriptionValid, |
162 | isLibraryCodeValid, | 176 | isLibraryCodeValid, |
diff --git a/server/helpers/custom-validators/servers.ts b/server/helpers/custom-validators/servers.ts index b9f45c282..94fda05aa 100644 --- a/server/helpers/custom-validators/servers.ts +++ b/server/helpers/custom-validators/servers.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import validator from 'validator' | 1 | import validator from 'validator' |
2 | import { CONFIG } from '@server/initializers/config' | ||
2 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | 3 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' |
3 | import { isTestOrDevInstance } from '../core-utils' | ||
4 | import { exists, isArray } from './misc' | 4 | import { exists, isArray } from './misc' |
5 | 5 | ||
6 | function isHostValid (host: string) { | 6 | function isHostValid (host: string) { |
@@ -10,7 +10,7 @@ function isHostValid (host: string) { | |||
10 | } | 10 | } |
11 | 11 | ||
12 | // We validate 'localhost', so we don't have the top level domain | 12 | // We validate 'localhost', so we don't have the top level domain |
13 | if (isTestOrDevInstance()) { | 13 | if (CONFIG.WEBSERVER.HOSTNAME === 'localhost') { |
14 | isURLOptions.require_tld = false | 14 | isURLOptions.require_tld = false |
15 | } | 15 | } |
16 | 16 | ||
diff --git a/server/helpers/custom-validators/video-studio.ts b/server/helpers/custom-validators/video-studio.ts index 19e7906d5..68dfec8dd 100644 --- a/server/helpers/custom-validators/video-studio.ts +++ b/server/helpers/custom-validators/video-studio.ts | |||
@@ -4,6 +4,7 @@ import { buildTaskFileFieldname } from '@server/lib/video-studio' | |||
4 | import { VideoStudioTask } from '@shared/models' | 4 | import { VideoStudioTask } from '@shared/models' |
5 | import { isArray } from './misc' | 5 | import { isArray } from './misc' |
6 | import { isVideoFileMimeTypeValid, isVideoImageValid } from './videos' | 6 | import { isVideoFileMimeTypeValid, isVideoImageValid } from './videos' |
7 | import { forceNumber } from '@shared/core-utils' | ||
7 | 8 | ||
8 | function isValidStudioTasksArray (tasks: any) { | 9 | function isValidStudioTasksArray (tasks: any) { |
9 | if (!isArray(tasks)) return false | 10 | if (!isArray(tasks)) return false |
@@ -24,7 +25,7 @@ function isStudioCutTaskValid (task: VideoStudioTask) { | |||
24 | 25 | ||
25 | if (!start || !end) return true | 26 | if (!start || !end) return true |
26 | 27 | ||
27 | return parseInt(start + '') < parseInt(end + '') | 28 | return forceNumber(start) < forceNumber(end) |
28 | } | 29 | } |
29 | 30 | ||
30 | function isStudioTaskAddIntroOutroValid (task: VideoStudioTask, indice: number, files: Express.Multer.File[]) { | 31 | function isStudioTaskAddIntroOutroValid (task: VideoStudioTask, indice: number, files: Express.Multer.File[]) { |
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts index 3ebfe2937..9e8177f77 100644 --- a/server/helpers/custom-validators/videos.ts +++ b/server/helpers/custom-validators/videos.ts | |||
@@ -45,10 +45,6 @@ function isVideoDurationValid (value: string) { | |||
45 | return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.DURATION) | 45 | return exists(value) && validator.isInt(value + '', VIDEOS_CONSTRAINTS_FIELDS.DURATION) |
46 | } | 46 | } |
47 | 47 | ||
48 | function isVideoTruncatedDescriptionValid (value: string) { | ||
49 | return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.TRUNCATED_DESCRIPTION) | ||
50 | } | ||
51 | |||
52 | function isVideoDescriptionValid (value: string) { | 48 | function isVideoDescriptionValid (value: string) { |
53 | return value === null || (exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.DESCRIPTION)) | 49 | return value === null || (exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.DESCRIPTION)) |
54 | } | 50 | } |
@@ -151,7 +147,6 @@ export { | |||
151 | isVideoCategoryValid, | 147 | isVideoCategoryValid, |
152 | isVideoLicenceValid, | 148 | isVideoLicenceValid, |
153 | isVideoLanguageValid, | 149 | isVideoLanguageValid, |
154 | isVideoTruncatedDescriptionValid, | ||
155 | isVideoDescriptionValid, | 150 | isVideoDescriptionValid, |
156 | isVideoFileInfoHashValid, | 151 | isVideoFileInfoHashValid, |
157 | isVideoNameValid, | 152 | isVideoNameValid, |
diff --git a/server/helpers/ffmpeg/ffmpeg-commons.ts b/server/helpers/ffmpeg/ffmpeg-commons.ts index b01989899..3906a2089 100644 --- a/server/helpers/ffmpeg/ffmpeg-commons.ts +++ b/server/helpers/ffmpeg/ffmpeg-commons.ts | |||
@@ -38,7 +38,7 @@ function getFFmpegVersion () { | |||
38 | return execPromise(`${ffmpegPath} -version`) | 38 | return execPromise(`${ffmpegPath} -version`) |
39 | .then(stdout => { | 39 | .then(stdout => { |
40 | const parsed = stdout.match(/ffmpeg version .?(\d+\.\d+(\.\d+)?)/) | 40 | const parsed = stdout.match(/ffmpeg version .?(\d+\.\d+(\.\d+)?)/) |
41 | if (!parsed || !parsed[1]) return rej(new Error(`Could not find ffmpeg version in ${stdout}`)) | 41 | if (!parsed?.[1]) return rej(new Error(`Could not find ffmpeg version in ${stdout}`)) |
42 | 42 | ||
43 | // Fix ffmpeg version that does not include patch version (4.4 for example) | 43 | // Fix ffmpeg version that does not include patch version (4.4 for example) |
44 | let version = parsed[1] | 44 | let version = parsed[1] |
diff --git a/server/helpers/ffmpeg/ffmpeg-vod.ts b/server/helpers/ffmpeg/ffmpeg-vod.ts index 7a81a1313..d84703eb9 100644 --- a/server/helpers/ffmpeg/ffmpeg-vod.ts +++ b/server/helpers/ffmpeg/ffmpeg-vod.ts | |||
@@ -1,14 +1,15 @@ | |||
1 | import { MutexInterface } from 'async-mutex' | ||
1 | import { Job } from 'bullmq' | 2 | import { Job } from 'bullmq' |
2 | import { FfmpegCommand } from 'fluent-ffmpeg' | 3 | import { FfmpegCommand } from 'fluent-ffmpeg' |
3 | import { readFile, writeFile } from 'fs-extra' | 4 | import { readFile, writeFile } from 'fs-extra' |
4 | import { dirname } from 'path' | 5 | import { dirname } from 'path' |
6 | import { VIDEO_TRANSCODING_FPS } from '@server/initializers/constants' | ||
5 | import { pick } from '@shared/core-utils' | 7 | import { pick } from '@shared/core-utils' |
6 | import { AvailableEncoders, VideoResolution } from '@shared/models' | 8 | import { AvailableEncoders, VideoResolution } from '@shared/models' |
7 | import { logger, loggerTagsFactory } from '../logger' | 9 | import { logger, loggerTagsFactory } from '../logger' |
8 | import { getFFmpeg, runCommand } from './ffmpeg-commons' | 10 | import { getFFmpeg, runCommand } from './ffmpeg-commons' |
9 | import { presetCopy, presetOnlyAudio, presetVOD } from './ffmpeg-presets' | 11 | import { presetCopy, presetOnlyAudio, presetVOD } from './ffmpeg-presets' |
10 | import { computeFPS, ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS } from './ffprobe-utils' | 12 | import { computeFPS, ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS } from './ffprobe-utils' |
11 | import { VIDEO_TRANSCODING_FPS } from '@server/initializers/constants' | ||
12 | 13 | ||
13 | const lTags = loggerTagsFactory('ffmpeg') | 14 | const lTags = loggerTagsFactory('ffmpeg') |
14 | 15 | ||
@@ -22,6 +23,10 @@ interface BaseTranscodeVODOptions { | |||
22 | inputPath: string | 23 | inputPath: string |
23 | outputPath: string | 24 | outputPath: string |
24 | 25 | ||
26 | // Will be released after the ffmpeg started | ||
27 | // To prevent a bug where the input file does not exist anymore when running ffmpeg | ||
28 | inputFileMutexReleaser: MutexInterface.Releaser | ||
29 | |||
25 | availableEncoders: AvailableEncoders | 30 | availableEncoders: AvailableEncoders |
26 | profile: string | 31 | profile: string |
27 | 32 | ||
@@ -94,6 +99,12 @@ async function transcodeVOD (options: TranscodeVODOptions) { | |||
94 | 99 | ||
95 | command = await builders[options.type](command, options) | 100 | command = await builders[options.type](command, options) |
96 | 101 | ||
102 | command.on('start', () => { | ||
103 | setTimeout(() => { | ||
104 | options.inputFileMutexReleaser() | ||
105 | }, 1000) | ||
106 | }) | ||
107 | |||
97 | await runCommand({ command, job: options.job }) | 108 | await runCommand({ command, job: options.job }) |
98 | 109 | ||
99 | await fixHLSPlaylistIfNeeded(options) | 110 | await fixHLSPlaylistIfNeeded(options) |
diff --git a/server/helpers/ffmpeg/ffprobe-utils.ts b/server/helpers/ffmpeg/ffprobe-utils.ts index 2c6253d44..fb270b3cb 100644 --- a/server/helpers/ffmpeg/ffprobe-utils.ts +++ b/server/helpers/ffmpeg/ffprobe-utils.ts | |||
@@ -15,6 +15,7 @@ import { | |||
15 | import { VideoResolution, VideoTranscodingFPS } from '@shared/models' | 15 | import { VideoResolution, VideoTranscodingFPS } from '@shared/models' |
16 | import { CONFIG } from '../../initializers/config' | 16 | import { CONFIG } from '../../initializers/config' |
17 | import { VIDEO_TRANSCODING_FPS } from '../../initializers/constants' | 17 | import { VIDEO_TRANSCODING_FPS } from '../../initializers/constants' |
18 | import { toEven } from '../core-utils' | ||
18 | import { logger } from '../logger' | 19 | import { logger } from '../logger' |
19 | 20 | ||
20 | /** | 21 | /** |
@@ -96,8 +97,9 @@ function computeResolutionsToTranscode (options: { | |||
96 | type: 'vod' | 'live' | 97 | type: 'vod' | 'live' |
97 | includeInput: boolean | 98 | includeInput: boolean |
98 | strictLower: boolean | 99 | strictLower: boolean |
100 | hasAudio: boolean | ||
99 | }) { | 101 | }) { |
100 | const { input, type, includeInput, strictLower } = options | 102 | const { input, type, includeInput, strictLower, hasAudio } = options |
101 | 103 | ||
102 | const configResolutions = type === 'vod' | 104 | const configResolutions = type === 'vod' |
103 | ? CONFIG.TRANSCODING.RESOLUTIONS | 105 | ? CONFIG.TRANSCODING.RESOLUTIONS |
@@ -125,12 +127,15 @@ function computeResolutionsToTranscode (options: { | |||
125 | if (input < resolution) continue | 127 | if (input < resolution) continue |
126 | // We only want lower resolutions than input file | 128 | // We only want lower resolutions than input file |
127 | if (strictLower && input === resolution) continue | 129 | if (strictLower && input === resolution) continue |
130 | // Audio resolutio but no audio in the video | ||
131 | if (resolution === VideoResolution.H_NOVIDEO && !hasAudio) continue | ||
128 | 132 | ||
129 | resolutionsEnabled.add(resolution) | 133 | resolutionsEnabled.add(resolution) |
130 | } | 134 | } |
131 | 135 | ||
132 | if (includeInput) { | 136 | if (includeInput) { |
133 | resolutionsEnabled.add(input) | 137 | // Always use an even resolution to avoid issues with ffmpeg |
138 | resolutionsEnabled.add(toEven(input)) | ||
134 | } | 139 | } |
135 | 140 | ||
136 | return Array.from(resolutionsEnabled) | 141 | return Array.from(resolutionsEnabled) |
diff --git a/server/helpers/otp.ts b/server/helpers/otp.ts new file mode 100644 index 000000000..a32cc9621 --- /dev/null +++ b/server/helpers/otp.ts | |||
@@ -0,0 +1,58 @@ | |||
1 | import { Secret, TOTP } from 'otpauth' | ||
2 | import { CONFIG } from '@server/initializers/config' | ||
3 | import { WEBSERVER } from '@server/initializers/constants' | ||
4 | import { decrypt } from './peertube-crypto' | ||
5 | |||
6 | async function isOTPValid (options: { | ||
7 | encryptedSecret: string | ||
8 | token: string | ||
9 | }) { | ||
10 | const { token, encryptedSecret } = options | ||
11 | |||
12 | const secret = await decrypt(encryptedSecret, CONFIG.SECRETS.PEERTUBE) | ||
13 | |||
14 | const totp = new TOTP({ | ||
15 | ...baseOTPOptions(), | ||
16 | |||
17 | secret | ||
18 | }) | ||
19 | |||
20 | const delta = totp.validate({ | ||
21 | token, | ||
22 | window: 1 | ||
23 | }) | ||
24 | |||
25 | if (delta === null) return false | ||
26 | |||
27 | return true | ||
28 | } | ||
29 | |||
30 | function generateOTPSecret (email: string) { | ||
31 | const totp = new TOTP({ | ||
32 | ...baseOTPOptions(), | ||
33 | |||
34 | label: email, | ||
35 | secret: new Secret() | ||
36 | }) | ||
37 | |||
38 | return { | ||
39 | secret: totp.secret.base32, | ||
40 | uri: totp.toString() | ||
41 | } | ||
42 | } | ||
43 | |||
44 | export { | ||
45 | isOTPValid, | ||
46 | generateOTPSecret | ||
47 | } | ||
48 | |||
49 | // --------------------------------------------------------------------------- | ||
50 | |||
51 | function baseOTPOptions () { | ||
52 | return { | ||
53 | issuer: WEBSERVER.HOST, | ||
54 | algorithm: 'SHA1', | ||
55 | digits: 6, | ||
56 | period: 30 | ||
57 | } | ||
58 | } | ||
diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts index 8aca50900..ae7d11800 100644 --- a/server/helpers/peertube-crypto.ts +++ b/server/helpers/peertube-crypto.ts | |||
@@ -1,11 +1,11 @@ | |||
1 | import { compare, genSalt, hash } from 'bcrypt' | 1 | import { compare, genSalt, hash } from 'bcrypt' |
2 | import { createSign, createVerify } from 'crypto' | 2 | import { createCipheriv, createDecipheriv, createSign, createVerify } from 'crypto' |
3 | import { Request } from 'express' | 3 | import { Request } from 'express' |
4 | import { cloneDeep } from 'lodash' | 4 | import { cloneDeep } from 'lodash' |
5 | import { sha256 } from '@shared/extra-utils' | 5 | import { sha256 } from '@shared/extra-utils' |
6 | import { BCRYPT_SALT_SIZE, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers/constants' | 6 | import { BCRYPT_SALT_SIZE, ENCRYPTION, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers/constants' |
7 | import { MActor } from '../types/models' | 7 | import { MActor } from '../types/models' |
8 | import { generateRSAKeyPairPromise, promisify1, promisify2 } from './core-utils' | 8 | import { generateRSAKeyPairPromise, promisify1, promisify2, randomBytesPromise, scryptPromise } from './core-utils' |
9 | import { jsonld } from './custom-jsonld-signature' | 9 | import { jsonld } from './custom-jsonld-signature' |
10 | import { logger } from './logger' | 10 | import { logger } from './logger' |
11 | 11 | ||
@@ -21,9 +21,13 @@ function createPrivateAndPublicKeys () { | |||
21 | return generateRSAKeyPairPromise(PRIVATE_RSA_KEY_SIZE) | 21 | return generateRSAKeyPairPromise(PRIVATE_RSA_KEY_SIZE) |
22 | } | 22 | } |
23 | 23 | ||
24 | // --------------------------------------------------------------------------- | ||
24 | // User password checks | 25 | // User password checks |
26 | // --------------------------------------------------------------------------- | ||
25 | 27 | ||
26 | function comparePassword (plainPassword: string, hashPassword: string) { | 28 | function comparePassword (plainPassword: string, hashPassword: string) { |
29 | if (!plainPassword) return Promise.resolve(false) | ||
30 | |||
27 | return bcryptComparePromise(plainPassword, hashPassword) | 31 | return bcryptComparePromise(plainPassword, hashPassword) |
28 | } | 32 | } |
29 | 33 | ||
@@ -33,7 +37,9 @@ async function cryptPassword (password: string) { | |||
33 | return bcryptHashPromise(password, salt) | 37 | return bcryptHashPromise(password, salt) |
34 | } | 38 | } |
35 | 39 | ||
40 | // --------------------------------------------------------------------------- | ||
36 | // HTTP Signature | 41 | // HTTP Signature |
42 | // --------------------------------------------------------------------------- | ||
37 | 43 | ||
38 | function isHTTPSignatureDigestValid (rawBody: Buffer, req: Request): boolean { | 44 | function isHTTPSignatureDigestValid (rawBody: Buffer, req: Request): boolean { |
39 | if (req.headers[HTTP_SIGNATURE.HEADER_NAME] && req.headers['digest']) { | 45 | if (req.headers[HTTP_SIGNATURE.HEADER_NAME] && req.headers['digest']) { |
@@ -62,7 +68,9 @@ function parseHTTPSignature (req: Request, clockSkew?: number) { | |||
62 | return parsed | 68 | return parsed |
63 | } | 69 | } |
64 | 70 | ||
71 | // --------------------------------------------------------------------------- | ||
65 | // JSONLD | 72 | // JSONLD |
73 | // --------------------------------------------------------------------------- | ||
66 | 74 | ||
67 | function isJsonLDSignatureVerified (fromActor: MActor, signedDocument: any): Promise<boolean> { | 75 | function isJsonLDSignatureVerified (fromActor: MActor, signedDocument: any): Promise<boolean> { |
68 | if (signedDocument.signature.type === 'RsaSignature2017') { | 76 | if (signedDocument.signature.type === 'RsaSignature2017') { |
@@ -112,6 +120,8 @@ async function signJsonLDObject <T> (byActor: MActor, data: T) { | |||
112 | return Object.assign(data, { signature }) | 120 | return Object.assign(data, { signature }) |
113 | } | 121 | } |
114 | 122 | ||
123 | // --------------------------------------------------------------------------- | ||
124 | |||
115 | function buildDigest (body: any) { | 125 | function buildDigest (body: any) { |
116 | const rawBody = typeof body === 'string' ? body : JSON.stringify(body) | 126 | const rawBody = typeof body === 'string' ? body : JSON.stringify(body) |
117 | 127 | ||
@@ -119,6 +129,34 @@ function buildDigest (body: any) { | |||
119 | } | 129 | } |
120 | 130 | ||
121 | // --------------------------------------------------------------------------- | 131 | // --------------------------------------------------------------------------- |
132 | // Encryption | ||
133 | // --------------------------------------------------------------------------- | ||
134 | |||
135 | async function encrypt (str: string, secret: string) { | ||
136 | const iv = await randomBytesPromise(ENCRYPTION.IV) | ||
137 | |||
138 | const key = await scryptPromise(secret, ENCRYPTION.SALT, 32) | ||
139 | const cipher = createCipheriv(ENCRYPTION.ALGORITHM, key, iv) | ||
140 | |||
141 | let encrypted = iv.toString(ENCRYPTION.ENCODING) + ':' | ||
142 | encrypted += cipher.update(str, 'utf8', ENCRYPTION.ENCODING) | ||
143 | encrypted += cipher.final(ENCRYPTION.ENCODING) | ||
144 | |||
145 | return encrypted | ||
146 | } | ||
147 | |||
148 | async function decrypt (encryptedArg: string, secret: string) { | ||
149 | const [ ivStr, encryptedStr ] = encryptedArg.split(':') | ||
150 | |||
151 | const iv = Buffer.from(ivStr, 'hex') | ||
152 | const key = await scryptPromise(secret, ENCRYPTION.SALT, 32) | ||
153 | |||
154 | const decipher = createDecipheriv(ENCRYPTION.ALGORITHM, key, iv) | ||
155 | |||
156 | return decipher.update(encryptedStr, ENCRYPTION.ENCODING, 'utf8') + decipher.final('utf8') | ||
157 | } | ||
158 | |||
159 | // --------------------------------------------------------------------------- | ||
122 | 160 | ||
123 | export { | 161 | export { |
124 | isHTTPSignatureDigestValid, | 162 | isHTTPSignatureDigestValid, |
@@ -129,7 +167,10 @@ export { | |||
129 | comparePassword, | 167 | comparePassword, |
130 | createPrivateAndPublicKeys, | 168 | createPrivateAndPublicKeys, |
131 | cryptPassword, | 169 | cryptPassword, |
132 | signJsonLDObject | 170 | signJsonLDObject, |
171 | |||
172 | encrypt, | ||
173 | decrypt | ||
133 | } | 174 | } |
134 | 175 | ||
135 | // --------------------------------------------------------------------------- | 176 | // --------------------------------------------------------------------------- |
diff --git a/server/helpers/upload.ts b/server/helpers/upload.ts index 3cb17edd0..f5f476913 100644 --- a/server/helpers/upload.ts +++ b/server/helpers/upload.ts | |||
@@ -1,10 +1,10 @@ | |||
1 | import { join } from 'path' | 1 | import { join } from 'path' |
2 | import { RESUMABLE_UPLOAD_DIRECTORY } from '../initializers/constants' | 2 | import { DIRECTORIES } from '@server/initializers/constants' |
3 | 3 | ||
4 | function getResumableUploadPath (filename?: string) { | 4 | function getResumableUploadPath (filename?: string) { |
5 | if (filename) return join(RESUMABLE_UPLOAD_DIRECTORY, filename) | 5 | if (filename) return join(DIRECTORIES.RESUMABLE_UPLOAD, filename) |
6 | 6 | ||
7 | return RESUMABLE_UPLOAD_DIRECTORY | 7 | return DIRECTORIES.RESUMABLE_UPLOAD |
8 | } | 8 | } |
9 | 9 | ||
10 | // --------------------------------------------------------------------------- | 10 | // --------------------------------------------------------------------------- |
diff --git a/server/helpers/video.ts b/server/helpers/video.ts index f5f645d3e..c688ef1e3 100644 --- a/server/helpers/video.ts +++ b/server/helpers/video.ts | |||
@@ -2,6 +2,7 @@ import { Response } from 'express' | |||
2 | import { CONFIG } from '@server/initializers/config' | 2 | import { CONFIG } from '@server/initializers/config' |
3 | import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo } from '@server/types/models' | 3 | import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo } from '@server/types/models' |
4 | import { VideoPrivacy, VideoState } from '@shared/models' | 4 | import { VideoPrivacy, VideoState } from '@shared/models' |
5 | import { forceNumber } from '@shared/core-utils' | ||
5 | 6 | ||
6 | function getVideoWithAttributes (res: Response) { | 7 | function getVideoWithAttributes (res: Response) { |
7 | return res.locals.videoAPI || res.locals.videoAll || res.locals.onlyVideo | 8 | return res.locals.videoAPI || res.locals.videoAll || res.locals.onlyVideo |
@@ -14,14 +15,14 @@ function extractVideo (videoOrPlaylist: MVideo | MStreamingPlaylistVideo) { | |||
14 | } | 15 | } |
15 | 16 | ||
16 | function isPrivacyForFederation (privacy: VideoPrivacy) { | 17 | function isPrivacyForFederation (privacy: VideoPrivacy) { |
17 | const castedPrivacy = parseInt(privacy + '', 10) | 18 | const castedPrivacy = forceNumber(privacy) |
18 | 19 | ||
19 | return castedPrivacy === VideoPrivacy.PUBLIC || | 20 | return castedPrivacy === VideoPrivacy.PUBLIC || |
20 | (CONFIG.FEDERATION.VIDEOS.FEDERATE_UNLISTED === true && castedPrivacy === VideoPrivacy.UNLISTED) | 21 | (CONFIG.FEDERATION.VIDEOS.FEDERATE_UNLISTED === true && castedPrivacy === VideoPrivacy.UNLISTED) |
21 | } | 22 | } |
22 | 23 | ||
23 | function isStateForFederation (state: VideoState) { | 24 | function isStateForFederation (state: VideoState) { |
24 | const castedState = parseInt(state + '', 10) | 25 | const castedState = forceNumber(state) |
25 | 26 | ||
26 | return castedState === VideoState.PUBLISHED || castedState === VideoState.WAITING_FOR_LIVE || castedState === VideoState.LIVE_ENDED | 27 | return castedState === VideoState.PUBLISHED || castedState === VideoState.WAITING_FOR_LIVE || castedState === VideoState.LIVE_ENDED |
27 | } | 28 | } |
diff --git a/server/helpers/webtorrent.ts b/server/helpers/webtorrent.ts index 88bdb16b6..a3c93e6fe 100644 --- a/server/helpers/webtorrent.ts +++ b/server/helpers/webtorrent.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { decode, encode } from 'bencode' | 1 | import { decode, encode } from 'bencode' |
2 | import createTorrent from 'create-torrent' | 2 | import createTorrent from 'create-torrent' |
3 | import { createWriteStream, ensureDir, readFile, remove, writeFile } from 'fs-extra' | 3 | import { createWriteStream, ensureDir, pathExists, readFile, remove, writeFile } from 'fs-extra' |
4 | import magnetUtil from 'magnet-uri' | 4 | import magnetUtil from 'magnet-uri' |
5 | import parseTorrent from 'parse-torrent' | 5 | import parseTorrent from 'parse-torrent' |
6 | import { dirname, join } from 'path' | 6 | import { dirname, join } from 'path' |
@@ -134,6 +134,11 @@ async function updateTorrentMetadata (videoOrPlaylist: MVideo | MStreamingPlayli | |||
134 | 134 | ||
135 | const oldTorrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, videoFile.torrentFilename) | 135 | const oldTorrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, videoFile.torrentFilename) |
136 | 136 | ||
137 | if (!await pathExists(oldTorrentPath)) { | ||
138 | logger.info('Do not update torrent metadata %s of video %s because the file does not exist anymore.', video.uuid, oldTorrentPath) | ||
139 | return | ||
140 | } | ||
141 | |||
137 | const torrentContent = await readFile(oldTorrentPath) | 142 | const torrentContent = await readFile(oldTorrentPath) |
138 | const decoded = decode(torrentContent) | 143 | const decoded = decode(torrentContent) |
139 | 144 | ||
@@ -151,7 +156,7 @@ async function updateTorrentMetadata (videoOrPlaylist: MVideo | MStreamingPlayli | |||
151 | logger.info('Updating torrent metadata %s -> %s.', oldTorrentPath, newTorrentPath) | 156 | logger.info('Updating torrent metadata %s -> %s.', oldTorrentPath, newTorrentPath) |
152 | 157 | ||
153 | await writeFile(newTorrentPath, encode(decoded)) | 158 | await writeFile(newTorrentPath, encode(decoded)) |
154 | await remove(join(CONFIG.STORAGE.TORRENTS_DIR, videoFile.torrentFilename)) | 159 | await remove(oldTorrentPath) |
155 | 160 | ||
156 | videoFile.torrentFilename = newTorrentFilename | 161 | videoFile.torrentFilename = newTorrentFilename |
157 | videoFile.infoHash = sha1(encode(decoded.info)) | 162 | videoFile.infoHash = sha1(encode(decoded.info)) |
@@ -164,7 +169,10 @@ function generateMagnetUri ( | |||
164 | ) { | 169 | ) { |
165 | const xs = videoFile.getTorrentUrl() | 170 | const xs = videoFile.getTorrentUrl() |
166 | const announce = trackerUrls | 171 | const announce = trackerUrls |
167 | let urlList = [ videoFile.getFileUrl(video) ] | 172 | |
173 | let urlList = video.hasPrivateStaticPath() | ||
174 | ? [] | ||
175 | : [ videoFile.getFileUrl(video) ] | ||
168 | 176 | ||
169 | const redundancies = videoFile.RedundancyVideos | 177 | const redundancies = videoFile.RedundancyVideos |
170 | if (isArray(redundancies)) urlList = urlList.concat(redundancies.map(r => r.fileUrl)) | 178 | if (isArray(redundancies)) urlList = urlList.concat(redundancies.map(r => r.fileUrl)) |
@@ -240,6 +248,8 @@ function buildAnnounceList () { | |||
240 | } | 248 | } |
241 | 249 | ||
242 | function buildUrlList (video: MVideo, videoFile: MVideoFile) { | 250 | function buildUrlList (video: MVideo, videoFile: MVideoFile) { |
251 | if (video.hasPrivateStaticPath()) return [] | ||
252 | |||
243 | return [ videoFile.getFileUrl(video) ] | 253 | return [ videoFile.getFileUrl(video) ] |
244 | } | 254 | } |
245 | 255 | ||
diff --git a/server/helpers/youtube-dl/youtube-dl-cli.ts b/server/helpers/youtube-dl/youtube-dl-cli.ts index fc4c40787..a2f630953 100644 --- a/server/helpers/youtube-dl/youtube-dl-cli.ts +++ b/server/helpers/youtube-dl/youtube-dl-cli.ts | |||
@@ -128,14 +128,14 @@ export class YoutubeDLCLI { | |||
128 | const data = await this.run({ url, args: completeArgs, processOptions }) | 128 | const data = await this.run({ url, args: completeArgs, processOptions }) |
129 | if (!data) return undefined | 129 | if (!data) return undefined |
130 | 130 | ||
131 | const info = data.map(this.parseInfo) | 131 | const info = data.map(d => JSON.parse(d)) |
132 | 132 | ||
133 | return info.length === 1 | 133 | return info.length === 1 |
134 | ? info[0] | 134 | ? info[0] |
135 | : info | 135 | : info |
136 | } | 136 | } |
137 | 137 | ||
138 | getListInfo (options: { | 138 | async getListInfo (options: { |
139 | url: string | 139 | url: string |
140 | latestVideosCount?: number | 140 | latestVideosCount?: number |
141 | processOptions: execa.NodeOptions | 141 | processOptions: execa.NodeOptions |
@@ -151,12 +151,17 @@ export class YoutubeDLCLI { | |||
151 | additionalYoutubeDLArgs.push('--playlist-end', options.latestVideosCount.toString()) | 151 | additionalYoutubeDLArgs.push('--playlist-end', options.latestVideosCount.toString()) |
152 | } | 152 | } |
153 | 153 | ||
154 | return this.getInfo({ | 154 | const result = await this.getInfo({ |
155 | url: options.url, | 155 | url: options.url, |
156 | format: YoutubeDLCLI.getYoutubeDLVideoFormat([], false), | 156 | format: YoutubeDLCLI.getYoutubeDLVideoFormat([], false), |
157 | processOptions: options.processOptions, | 157 | processOptions: options.processOptions, |
158 | additionalYoutubeDLArgs | 158 | additionalYoutubeDLArgs |
159 | }) | 159 | }) |
160 | |||
161 | if (!result) return result | ||
162 | if (!Array.isArray(result)) return [ result ] | ||
163 | |||
164 | return result | ||
160 | } | 165 | } |
161 | 166 | ||
162 | async getSubs (options: { | 167 | async getSubs (options: { |
@@ -241,8 +246,4 @@ export class YoutubeDLCLI { | |||
241 | 246 | ||
242 | return args | 247 | return args |
243 | } | 248 | } |
244 | |||
245 | private parseInfo (data: string) { | ||
246 | return JSON.parse(data) | ||
247 | } | ||
248 | } | 249 | } |
diff --git a/server/helpers/youtube-dl/youtube-dl-wrapper.ts b/server/helpers/youtube-dl/youtube-dl-wrapper.ts index 966b8df78..ac3cd190e 100644 --- a/server/helpers/youtube-dl/youtube-dl-wrapper.ts +++ b/server/helpers/youtube-dl/youtube-dl-wrapper.ts | |||
@@ -77,7 +77,7 @@ class YoutubeDLWrapper { | |||
77 | 77 | ||
78 | const subtitles = files.reduce((acc, filename) => { | 78 | const subtitles = files.reduce((acc, filename) => { |
79 | const matched = filename.match(/\.([a-z]{2})(-[a-z]+)?\.(vtt|ttml)/i) | 79 | const matched = filename.match(/\.([a-z]{2})(-[a-z]+)?\.(vtt|ttml)/i) |
80 | if (!matched || !matched[1]) return acc | 80 | if (!matched?.[1]) return acc |
81 | 81 | ||
82 | return [ | 82 | return [ |
83 | ...acc, | 83 | ...acc, |