diff options
Diffstat (limited to 'server/helpers')
32 files changed, 658 insertions, 343 deletions
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts index 2469b37b1..62d78373e 100644 --- a/server/helpers/activitypub.ts +++ b/server/helpers/activitypub.ts | |||
@@ -1,11 +1,12 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import * as validator from 'validator' | 2 | import * as validator from 'validator' |
3 | import { ResultList } from '../../shared/models' | 3 | import { ResultList } from '../../shared/models' |
4 | import { Activity, ActivityPubActor } from '../../shared/models/activitypub' | 4 | import { Activity } from '../../shared/models/activitypub' |
5 | import { ACTIVITY_PUB } from '../initializers' | 5 | import { ACTIVITY_PUB } from '../initializers' |
6 | import { ActorModel } from '../models/activitypub/actor' | 6 | import { ActorModel } from '../models/activitypub/actor' |
7 | import { signObject } from './peertube-crypto' | 7 | import { signJsonLDObject } from './peertube-crypto' |
8 | import { pageToStartAndCount } from './core-utils' | 8 | import { pageToStartAndCount } from './core-utils' |
9 | import { parse } from 'url' | ||
9 | 10 | ||
10 | function activityPubContextify <T> (data: T) { | 11 | function activityPubContextify <T> (data: T) { |
11 | return Object.assign(data, { | 12 | return Object.assign(data, { |
@@ -14,25 +15,26 @@ function activityPubContextify <T> (data: T) { | |||
14 | 'https://w3id.org/security/v1', | 15 | 'https://w3id.org/security/v1', |
15 | { | 16 | { |
16 | RsaSignature2017: 'https://w3id.org/security#RsaSignature2017', | 17 | RsaSignature2017: 'https://w3id.org/security#RsaSignature2017', |
17 | pt: 'https://joinpeertube.org/ns', | 18 | pt: 'https://joinpeertube.org/ns#', |
18 | schema: 'http://schema.org#', | 19 | sc: 'http://schema.org#', |
19 | Hashtag: 'as:Hashtag', | 20 | Hashtag: 'as:Hashtag', |
20 | uuid: 'schema:identifier', | 21 | uuid: 'sc:identifier', |
21 | category: 'schema:category', | 22 | category: 'sc:category', |
22 | licence: 'schema:license', | 23 | licence: 'sc:license', |
23 | subtitleLanguage: 'schema:subtitleLanguage', | 24 | subtitleLanguage: 'sc:subtitleLanguage', |
24 | sensitive: 'as:sensitive', | 25 | sensitive: 'as:sensitive', |
25 | language: 'schema:inLanguage', | 26 | language: 'sc:inLanguage', |
26 | views: 'schema:Number', | 27 | views: 'sc:Number', |
27 | stats: 'schema:Number', | 28 | state: 'sc:Number', |
28 | size: 'schema:Number', | 29 | size: 'sc:Number', |
29 | fps: 'schema:Number', | 30 | fps: 'sc:Number', |
30 | commentsEnabled: 'schema:Boolean', | 31 | commentsEnabled: 'sc:Boolean', |
31 | downloadEnabled: 'schema:Boolean', | 32 | downloadEnabled: 'sc:Boolean', |
32 | waitTranscoding: 'schema:Boolean', | 33 | waitTranscoding: 'sc:Boolean', |
33 | expires: 'schema:expires', | 34 | expires: 'sc:expires', |
34 | support: 'schema:Text', | 35 | support: 'sc:Text', |
35 | CacheFile: 'pt:CacheFile' | 36 | CacheFile: 'pt:CacheFile', |
37 | Infohash: 'pt:Infohash' | ||
36 | }, | 38 | }, |
37 | { | 39 | { |
38 | likes: { | 40 | likes: { |
@@ -57,16 +59,16 @@ function activityPubContextify <T> (data: T) { | |||
57 | } | 59 | } |
58 | 60 | ||
59 | type ActivityPubCollectionPaginationHandler = (start: number, count: number) => Bluebird<ResultList<any>> | Promise<ResultList<any>> | 61 | type ActivityPubCollectionPaginationHandler = (start: number, count: number) => Bluebird<ResultList<any>> | Promise<ResultList<any>> |
60 | async function activityPubCollectionPagination (url: string, handler: ActivityPubCollectionPaginationHandler, page?: any) { | 62 | async function activityPubCollectionPagination (baseUrl: string, handler: ActivityPubCollectionPaginationHandler, page?: any) { |
61 | if (!page || !validator.isInt(page)) { | 63 | if (!page || !validator.isInt(page)) { |
62 | // We just display the first page URL, we only need the total items | 64 | // We just display the first page URL, we only need the total items |
63 | const result = await handler(0, 1) | 65 | const result = await handler(0, 1) |
64 | 66 | ||
65 | return { | 67 | return { |
66 | id: url, | 68 | id: baseUrl, |
67 | type: 'OrderedCollection', | 69 | type: 'OrderedCollection', |
68 | totalItems: result.total, | 70 | totalItems: result.total, |
69 | first: url + '?page=1' | 71 | first: baseUrl + '?page=1' |
70 | } | 72 | } |
71 | } | 73 | } |
72 | 74 | ||
@@ -81,19 +83,19 @@ async function activityPubCollectionPagination (url: string, handler: ActivityPu | |||
81 | 83 | ||
82 | // There are more results | 84 | // There are more results |
83 | if (result.total > page * ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE) { | 85 | if (result.total > page * ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE) { |
84 | next = url + '?page=' + (page + 1) | 86 | next = baseUrl + '?page=' + (page + 1) |
85 | } | 87 | } |
86 | 88 | ||
87 | if (page > 1) { | 89 | if (page > 1) { |
88 | prev = url + '?page=' + (page - 1) | 90 | prev = baseUrl + '?page=' + (page - 1) |
89 | } | 91 | } |
90 | 92 | ||
91 | return { | 93 | return { |
92 | id: url + '?page=' + page, | 94 | id: baseUrl + '?page=' + page, |
93 | type: 'OrderedCollectionPage', | 95 | type: 'OrderedCollectionPage', |
94 | prev, | 96 | prev, |
95 | next, | 97 | next, |
96 | partOf: url, | 98 | partOf: baseUrl, |
97 | orderedItems: result.data, | 99 | orderedItems: result.data, |
98 | totalItems: result.total | 100 | totalItems: result.total |
99 | } | 101 | } |
@@ -103,19 +105,27 @@ async function activityPubCollectionPagination (url: string, handler: ActivityPu | |||
103 | function buildSignedActivity (byActor: ActorModel, data: Object) { | 105 | function buildSignedActivity (byActor: ActorModel, data: Object) { |
104 | const activity = activityPubContextify(data) | 106 | const activity = activityPubContextify(data) |
105 | 107 | ||
106 | return signObject(byActor, activity) as Promise<Activity> | 108 | return signJsonLDObject(byActor, activity) as Promise<Activity> |
107 | } | 109 | } |
108 | 110 | ||
109 | function getActorUrl (activityActor: string | ActivityPubActor) { | 111 | function getAPId (activity: string | { id: string }) { |
110 | if (typeof activityActor === 'string') return activityActor | 112 | if (typeof activity === 'string') return activity |
111 | 113 | ||
112 | return activityActor.id | 114 | return activity.id |
115 | } | ||
116 | |||
117 | function checkUrlsSameHost (url1: string, url2: string) { | ||
118 | const idHost = parse(url1).host | ||
119 | const actorHost = parse(url2).host | ||
120 | |||
121 | return idHost && actorHost && idHost.toLowerCase() === actorHost.toLowerCase() | ||
113 | } | 122 | } |
114 | 123 | ||
115 | // --------------------------------------------------------------------------- | 124 | // --------------------------------------------------------------------------- |
116 | 125 | ||
117 | export { | 126 | export { |
118 | getActorUrl, | 127 | checkUrlsSameHost, |
128 | getAPId, | ||
119 | activityPubContextify, | 129 | activityPubContextify, |
120 | activityPubCollectionPagination, | 130 | activityPubCollectionPagination, |
121 | buildSignedActivity | 131 | buildSignedActivity |
diff --git a/server/helpers/captions-utils.ts b/server/helpers/captions-utils.ts index 660dce65c..0fb11a125 100644 --- a/server/helpers/captions-utils.ts +++ b/server/helpers/captions-utils.ts | |||
@@ -2,7 +2,7 @@ import { join } from 'path' | |||
2 | import { CONFIG } from '../initializers' | 2 | import { CONFIG } from '../initializers' |
3 | import { VideoCaptionModel } from '../models/video/video-caption' | 3 | import { VideoCaptionModel } from '../models/video/video-caption' |
4 | import * as srt2vtt from 'srt-to-vtt' | 4 | import * as srt2vtt from 'srt-to-vtt' |
5 | import { createReadStream, createWriteStream, remove, rename } from 'fs-extra' | 5 | import { createReadStream, createWriteStream, remove, move } from 'fs-extra' |
6 | 6 | ||
7 | async function moveAndProcessCaptionFile (physicalFile: { filename: string, path: string }, videoCaption: VideoCaptionModel) { | 7 | async function moveAndProcessCaptionFile (physicalFile: { filename: string, path: string }, videoCaption: VideoCaptionModel) { |
8 | const videoCaptionsDir = CONFIG.STORAGE.CAPTIONS_DIR | 8 | const videoCaptionsDir = CONFIG.STORAGE.CAPTIONS_DIR |
@@ -13,7 +13,7 @@ async function moveAndProcessCaptionFile (physicalFile: { filename: string, path | |||
13 | await convertSrtToVtt(physicalFile.path, destination) | 13 | await convertSrtToVtt(physicalFile.path, destination) |
14 | await remove(physicalFile.path) | 14 | await remove(physicalFile.path) |
15 | } else { // Just move the vtt file | 15 | } else { // Just move the vtt file |
16 | await rename(physicalFile.path, destination) | 16 | await move(physicalFile.path, destination, { overwrite: true }) |
17 | } | 17 | } |
18 | 18 | ||
19 | // This is important in case if there is another attempt in the retry process | 19 | // This is important in case if there is another attempt in the retry process |
diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts index 00bc0bdda..f38b82d97 100644 --- a/server/helpers/core-utils.ts +++ b/server/helpers/core-utils.ts | |||
@@ -5,12 +5,31 @@ | |||
5 | 5 | ||
6 | import * as bcrypt from 'bcrypt' | 6 | import * as bcrypt from 'bcrypt' |
7 | import * as createTorrent from 'create-torrent' | 7 | import * as createTorrent from 'create-torrent' |
8 | import { createHash, pseudoRandomBytes } from 'crypto' | 8 | import { createHash, HexBase64Latin1Encoding, pseudoRandomBytes } from 'crypto' |
9 | import { isAbsolute, join } from 'path' | 9 | import { isAbsolute, join } from 'path' |
10 | import * as pem from 'pem' | 10 | import * as pem from 'pem' |
11 | import { URL } from 'url' | 11 | import { URL } from 'url' |
12 | import { truncate } from 'lodash' | 12 | import { truncate } from 'lodash' |
13 | import { exec } from 'child_process' | 13 | import { exec } from 'child_process' |
14 | import { isArray } from './custom-validators/misc' | ||
15 | |||
16 | const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => { | ||
17 | if (!oldObject || typeof oldObject !== 'object') { | ||
18 | return valueConverter(oldObject) | ||
19 | } | ||
20 | |||
21 | if (isArray(oldObject)) { | ||
22 | return oldObject.map(e => objectConverter(e, keyConverter, valueConverter)) | ||
23 | } | ||
24 | |||
25 | const newObject = {} | ||
26 | Object.keys(oldObject).forEach(oldKey => { | ||
27 | const newKey = keyConverter(oldKey) | ||
28 | newObject[ newKey ] = objectConverter(oldObject[ oldKey ], keyConverter, valueConverter) | ||
29 | }) | ||
30 | |||
31 | return newObject | ||
32 | } | ||
14 | 33 | ||
15 | const timeTable = { | 34 | const timeTable = { |
16 | ms: 1, | 35 | ms: 1, |
@@ -21,6 +40,7 @@ const timeTable = { | |||
21 | week: 3600000 * 24 * 7, | 40 | week: 3600000 * 24 * 7, |
22 | month: 3600000 * 24 * 30 | 41 | month: 3600000 * 24 * 30 |
23 | } | 42 | } |
43 | |||
24 | export function parseDuration (duration: number | string): number { | 44 | export function parseDuration (duration: number | string): number { |
25 | if (typeof duration === 'number') return duration | 45 | if (typeof duration === 'number') return duration |
26 | 46 | ||
@@ -41,6 +61,53 @@ export function parseDuration (duration: number | string): number { | |||
41 | throw new Error('Duration could not be properly parsed') | 61 | throw new Error('Duration could not be properly parsed') |
42 | } | 62 | } |
43 | 63 | ||
64 | export function parseBytes (value: string | number): number { | ||
65 | if (typeof value === 'number') return value | ||
66 | |||
67 | const tgm = /^(\d+)\s*TB\s*(\d+)\s*GB\s*(\d+)\s*MB$/ | ||
68 | const tg = /^(\d+)\s*TB\s*(\d+)\s*GB$/ | ||
69 | const tm = /^(\d+)\s*TB\s*(\d+)\s*MB$/ | ||
70 | const gm = /^(\d+)\s*GB\s*(\d+)\s*MB$/ | ||
71 | const t = /^(\d+)\s*TB$/ | ||
72 | const g = /^(\d+)\s*GB$/ | ||
73 | const m = /^(\d+)\s*MB$/ | ||
74 | const b = /^(\d+)\s*B$/ | ||
75 | let match | ||
76 | |||
77 | if (value.match(tgm)) { | ||
78 | match = value.match(tgm) | ||
79 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 | ||
80 | + parseInt(match[2], 10) * 1024 * 1024 * 1024 | ||
81 | + parseInt(match[3], 10) * 1024 * 1024 | ||
82 | } else if (value.match(tg)) { | ||
83 | match = value.match(tg) | ||
84 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 | ||
85 | + parseInt(match[2], 10) * 1024 * 1024 * 1024 | ||
86 | } else if (value.match(tm)) { | ||
87 | match = value.match(tm) | ||
88 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 | ||
89 | + parseInt(match[2], 10) * 1024 * 1024 | ||
90 | } else if (value.match(gm)) { | ||
91 | match = value.match(gm) | ||
92 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 | ||
93 | + parseInt(match[2], 10) * 1024 * 1024 | ||
94 | } else if (value.match(t)) { | ||
95 | match = value.match(t) | ||
96 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 | ||
97 | } else if (value.match(g)) { | ||
98 | match = value.match(g) | ||
99 | return parseInt(match[1], 10) * 1024 * 1024 * 1024 | ||
100 | } else if (value.match(m)) { | ||
101 | match = value.match(m) | ||
102 | return parseInt(match[1], 10) * 1024 * 1024 | ||
103 | } else if (value.match(b)) { | ||
104 | match = value.match(b) | ||
105 | return parseInt(match[1], 10) * 1024 | ||
106 | } else { | ||
107 | return parseInt(value, 10) | ||
108 | } | ||
109 | } | ||
110 | |||
44 | function sanitizeUrl (url: string) { | 111 | function sanitizeUrl (url: string) { |
45 | const urlObject = new URL(url) | 112 | const urlObject = new URL(url) |
46 | 113 | ||
@@ -126,8 +193,12 @@ function peertubeTruncate (str: string, maxLength: number) { | |||
126 | return truncate(str, options) | 193 | return truncate(str, options) |
127 | } | 194 | } |
128 | 195 | ||
129 | function sha256 (str: string) { | 196 | function sha256 (str: string | Buffer, encoding: HexBase64Latin1Encoding = 'hex') { |
130 | return createHash('sha256').update(str).digest('hex') | 197 | return createHash('sha256').update(str).digest(encoding) |
198 | } | ||
199 | |||
200 | function sha1 (str: string | Buffer, encoding: HexBase64Latin1Encoding = 'hex') { | ||
201 | return createHash('sha1').update(str).digest(encoding) | ||
131 | } | 202 | } |
132 | 203 | ||
133 | function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> { | 204 | function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> { |
@@ -187,6 +258,7 @@ export { | |||
187 | isTestInstance, | 258 | isTestInstance, |
188 | isProdInstance, | 259 | isProdInstance, |
189 | 260 | ||
261 | objectConverter, | ||
190 | root, | 262 | root, |
191 | escapeHTML, | 263 | escapeHTML, |
192 | pageToStartAndCount, | 264 | pageToStartAndCount, |
@@ -194,7 +266,9 @@ export { | |||
194 | sanitizeHost, | 266 | sanitizeHost, |
195 | buildPath, | 267 | buildPath, |
196 | peertubeTruncate, | 268 | peertubeTruncate, |
269 | |||
197 | sha256, | 270 | sha256, |
271 | sha1, | ||
198 | 272 | ||
199 | promisify0, | 273 | promisify0, |
200 | promisify1, | 274 | promisify1, |
diff --git a/server/helpers/custom-jsonld-signature.ts b/server/helpers/custom-jsonld-signature.ts index e4f28018e..27a187db1 100644 --- a/server/helpers/custom-jsonld-signature.ts +++ b/server/helpers/custom-jsonld-signature.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import * as AsyncLRU from 'async-lru' | 1 | import * as AsyncLRU from 'async-lru' |
2 | import * as jsonld from 'jsonld/' | 2 | import * as jsonld from 'jsonld' |
3 | import * as jsig from 'jsonld-signatures' | 3 | import * as jsig from 'jsonld-signatures' |
4 | 4 | ||
5 | const nodeDocumentLoader = jsonld.documentLoaders.node() | 5 | const nodeDocumentLoader = jsonld.documentLoaders.node() |
@@ -17,4 +17,4 @@ jsonld.documentLoader = (url, cb) => { | |||
17 | 17 | ||
18 | jsig.use('jsonld', jsonld) | 18 | jsig.use('jsonld', jsonld) |
19 | 19 | ||
20 | export { jsig } | 20 | export { jsig, jsonld } |
diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts index 2562ead9b..b24590d9d 100644 --- a/server/helpers/custom-validators/activitypub/activity.ts +++ b/server/helpers/custom-validators/activitypub/activity.ts | |||
@@ -1,26 +1,14 @@ | |||
1 | import * as validator from 'validator' | 1 | import * as validator from 'validator' |
2 | import { Activity, ActivityType } from '../../../../shared/models/activitypub' | 2 | import { Activity, ActivityType } from '../../../../shared/models/activitypub' |
3 | import { | 3 | import { sanitizeAndCheckActorObject } from './actor' |
4 | isActorAcceptActivityValid, | 4 | import { isActivityPubUrlValid, isBaseActivityValid, isObjectValid } from './misc' |
5 | isActorDeleteActivityValid, | 5 | import { isDislikeActivityValid } from './rate' |
6 | isActorFollowActivityValid, | 6 | import { sanitizeAndCheckVideoCommentObject } from './video-comments' |
7 | isActorRejectActivityValid, | 7 | import { sanitizeAndCheckVideoTorrentObject } from './videos' |
8 | isActorUpdateActivityValid | ||
9 | } from './actor' | ||
10 | import { isAnnounceActivityValid } from './announce' | ||
11 | import { isActivityPubUrlValid } from './misc' | ||
12 | import { isDislikeActivityValid, isLikeActivityValid } from './rate' | ||
13 | import { isUndoActivityValid } from './undo' | ||
14 | import { isVideoCommentCreateActivityValid, isVideoCommentDeleteActivityValid } from './video-comments' | ||
15 | import { | ||
16 | isVideoFlagValid, | ||
17 | isVideoTorrentDeleteActivityValid, | ||
18 | sanitizeAndCheckVideoTorrentCreateActivity, | ||
19 | sanitizeAndCheckVideoTorrentUpdateActivity | ||
20 | } from './videos' | ||
21 | import { isViewActivityValid } from './view' | 8 | import { isViewActivityValid } from './view' |
22 | import { exists } from '../misc' | 9 | import { exists } from '../misc' |
23 | import { isCacheFileCreateActivityValid, isCacheFileUpdateActivityValid } from './cache-file' | 10 | import { isCacheFileObjectValid } from './cache-file' |
11 | import { isFlagActivityValid } from './flag' | ||
24 | 12 | ||
25 | function isRootActivityValid (activity: any) { | 13 | function isRootActivityValid (activity: any) { |
26 | return Array.isArray(activity['@context']) && ( | 14 | return Array.isArray(activity['@context']) && ( |
@@ -46,7 +34,10 @@ const activityCheckers: { [ P in ActivityType ]: (activity: Activity) => boolean | |||
46 | Reject: checkRejectActivity, | 34 | Reject: checkRejectActivity, |
47 | Announce: checkAnnounceActivity, | 35 | Announce: checkAnnounceActivity, |
48 | Undo: checkUndoActivity, | 36 | Undo: checkUndoActivity, |
49 | Like: checkLikeActivity | 37 | Like: checkLikeActivity, |
38 | View: checkViewActivity, | ||
39 | Flag: checkFlagActivity, | ||
40 | Dislike: checkDislikeActivity | ||
50 | } | 41 | } |
51 | 42 | ||
52 | function isActivityValid (activity: any) { | 43 | function isActivityValid (activity: any) { |
@@ -66,47 +57,79 @@ export { | |||
66 | 57 | ||
67 | // --------------------------------------------------------------------------- | 58 | // --------------------------------------------------------------------------- |
68 | 59 | ||
60 | function checkViewActivity (activity: any) { | ||
61 | return isBaseActivityValid(activity, 'View') && | ||
62 | isViewActivityValid(activity) | ||
63 | } | ||
64 | |||
65 | function checkFlagActivity (activity: any) { | ||
66 | return isBaseActivityValid(activity, 'Flag') && | ||
67 | isFlagActivityValid(activity) | ||
68 | } | ||
69 | |||
70 | function checkDislikeActivity (activity: any) { | ||
71 | return isBaseActivityValid(activity, 'Dislike') && | ||
72 | isDislikeActivityValid(activity) | ||
73 | } | ||
74 | |||
69 | function checkCreateActivity (activity: any) { | 75 | function checkCreateActivity (activity: any) { |
70 | return isViewActivityValid(activity) || | 76 | return isBaseActivityValid(activity, 'Create') && |
71 | isDislikeActivityValid(activity) || | 77 | ( |
72 | sanitizeAndCheckVideoTorrentCreateActivity(activity) || | 78 | isViewActivityValid(activity.object) || |
73 | isVideoFlagValid(activity) || | 79 | isDislikeActivityValid(activity.object) || |
74 | isVideoCommentCreateActivityValid(activity) || | 80 | isFlagActivityValid(activity.object) || |
75 | isCacheFileCreateActivityValid(activity) | 81 | |
82 | isCacheFileObjectValid(activity.object) || | ||
83 | sanitizeAndCheckVideoCommentObject(activity.object) || | ||
84 | sanitizeAndCheckVideoTorrentObject(activity.object) | ||
85 | ) | ||
76 | } | 86 | } |
77 | 87 | ||
78 | function checkUpdateActivity (activity: any) { | 88 | function checkUpdateActivity (activity: any) { |
79 | return isCacheFileUpdateActivityValid(activity) || | 89 | return isBaseActivityValid(activity, 'Update') && |
80 | sanitizeAndCheckVideoTorrentUpdateActivity(activity) || | 90 | ( |
81 | isActorUpdateActivityValid(activity) | 91 | isCacheFileObjectValid(activity.object) || |
92 | sanitizeAndCheckVideoTorrentObject(activity.object) || | ||
93 | sanitizeAndCheckActorObject(activity.object) | ||
94 | ) | ||
82 | } | 95 | } |
83 | 96 | ||
84 | function checkDeleteActivity (activity: any) { | 97 | function checkDeleteActivity (activity: any) { |
85 | return isVideoTorrentDeleteActivityValid(activity) || | 98 | // We don't really check objects |
86 | isActorDeleteActivityValid(activity) || | 99 | return isBaseActivityValid(activity, 'Delete') && |
87 | isVideoCommentDeleteActivityValid(activity) | 100 | isObjectValid(activity.object) |
88 | } | 101 | } |
89 | 102 | ||
90 | function checkFollowActivity (activity: any) { | 103 | function checkFollowActivity (activity: any) { |
91 | return isActorFollowActivityValid(activity) | 104 | return isBaseActivityValid(activity, 'Follow') && |
105 | isObjectValid(activity.object) | ||
92 | } | 106 | } |
93 | 107 | ||
94 | function checkAcceptActivity (activity: any) { | 108 | function checkAcceptActivity (activity: any) { |
95 | return isActorAcceptActivityValid(activity) | 109 | return isBaseActivityValid(activity, 'Accept') |
96 | } | 110 | } |
97 | 111 | ||
98 | function checkRejectActivity (activity: any) { | 112 | function checkRejectActivity (activity: any) { |
99 | return isActorRejectActivityValid(activity) | 113 | return isBaseActivityValid(activity, 'Reject') |
100 | } | 114 | } |
101 | 115 | ||
102 | function checkAnnounceActivity (activity: any) { | 116 | function checkAnnounceActivity (activity: any) { |
103 | return isAnnounceActivityValid(activity) | 117 | return isBaseActivityValid(activity, 'Announce') && |
118 | isObjectValid(activity.object) | ||
104 | } | 119 | } |
105 | 120 | ||
106 | function checkUndoActivity (activity: any) { | 121 | function checkUndoActivity (activity: any) { |
107 | return isUndoActivityValid(activity) | 122 | return isBaseActivityValid(activity, 'Undo') && |
123 | ( | ||
124 | checkFollowActivity(activity.object) || | ||
125 | checkLikeActivity(activity.object) || | ||
126 | checkDislikeActivity(activity.object) || | ||
127 | checkAnnounceActivity(activity.object) || | ||
128 | checkCreateActivity(activity.object) | ||
129 | ) | ||
108 | } | 130 | } |
109 | 131 | ||
110 | function checkLikeActivity (activity: any) { | 132 | function checkLikeActivity (activity: any) { |
111 | return isLikeActivityValid(activity) | 133 | return isBaseActivityValid(activity, 'Like') && |
134 | isObjectValid(activity.object) | ||
112 | } | 135 | } |
diff --git a/server/helpers/custom-validators/activitypub/actor.ts b/server/helpers/custom-validators/activitypub/actor.ts index 77c003cdf..c05f60f14 100644 --- a/server/helpers/custom-validators/activitypub/actor.ts +++ b/server/helpers/custom-validators/activitypub/actor.ts | |||
@@ -27,7 +27,8 @@ function isActorPublicKeyValid (publicKey: string) { | |||
27 | validator.isLength(publicKey, CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY) | 27 | validator.isLength(publicKey, CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY) |
28 | } | 28 | } |
29 | 29 | ||
30 | const actorNameRegExp = new RegExp('^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\\-_\.]+$') | 30 | const actorNameAlphabet = '[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\\-_.]' |
31 | const actorNameRegExp = new RegExp(`^${actorNameAlphabet}+$`) | ||
31 | function isActorPreferredUsernameValid (preferredUsername: string) { | 32 | function isActorPreferredUsernameValid (preferredUsername: string) { |
32 | return exists(preferredUsername) && validator.matches(preferredUsername, actorNameRegExp) | 33 | return exists(preferredUsername) && validator.matches(preferredUsername, actorNameRegExp) |
33 | } | 34 | } |
@@ -72,24 +73,10 @@ function isActorDeleteActivityValid (activity: any) { | |||
72 | return isBaseActivityValid(activity, 'Delete') | 73 | return isBaseActivityValid(activity, 'Delete') |
73 | } | 74 | } |
74 | 75 | ||
75 | function isActorFollowActivityValid (activity: any) { | 76 | function sanitizeAndCheckActorObject (object: any) { |
76 | return isBaseActivityValid(activity, 'Follow') && | 77 | normalizeActor(object) |
77 | isActivityPubUrlValid(activity.object) | ||
78 | } | ||
79 | |||
80 | function isActorAcceptActivityValid (activity: any) { | ||
81 | return isBaseActivityValid(activity, 'Accept') | ||
82 | } | ||
83 | |||
84 | function isActorRejectActivityValid (activity: any) { | ||
85 | return isBaseActivityValid(activity, 'Reject') | ||
86 | } | ||
87 | |||
88 | function isActorUpdateActivityValid (activity: any) { | ||
89 | normalizeActor(activity.object) | ||
90 | 78 | ||
91 | return isBaseActivityValid(activity, 'Update') && | 79 | return isActorObjectValid(object) |
92 | isActorObjectValid(activity.object) | ||
93 | } | 80 | } |
94 | 81 | ||
95 | function normalizeActor (actor: any) { | 82 | function normalizeActor (actor: any) { |
@@ -127,6 +114,7 @@ function areValidActorHandles (handles: string[]) { | |||
127 | 114 | ||
128 | export { | 115 | export { |
129 | normalizeActor, | 116 | normalizeActor, |
117 | actorNameAlphabet, | ||
130 | areValidActorHandles, | 118 | areValidActorHandles, |
131 | isActorEndpointsObjectValid, | 119 | isActorEndpointsObjectValid, |
132 | isActorPublicKeyObjectValid, | 120 | isActorPublicKeyObjectValid, |
@@ -137,10 +125,7 @@ export { | |||
137 | isActorObjectValid, | 125 | isActorObjectValid, |
138 | isActorFollowingCountValid, | 126 | isActorFollowingCountValid, |
139 | isActorFollowersCountValid, | 127 | isActorFollowersCountValid, |
140 | isActorFollowActivityValid, | ||
141 | isActorAcceptActivityValid, | ||
142 | isActorRejectActivityValid, | ||
143 | isActorDeleteActivityValid, | 128 | isActorDeleteActivityValid, |
144 | isActorUpdateActivityValid, | 129 | sanitizeAndCheckActorObject, |
145 | isValidActorHandle | 130 | isValidActorHandle |
146 | } | 131 | } |
diff --git a/server/helpers/custom-validators/activitypub/announce.ts b/server/helpers/custom-validators/activitypub/announce.ts deleted file mode 100644 index 0519c6026..000000000 --- a/server/helpers/custom-validators/activitypub/announce.ts +++ /dev/null | |||
@@ -1,13 +0,0 @@ | |||
1 | import { isActivityPubUrlValid, isBaseActivityValid } from './misc' | ||
2 | |||
3 | function isAnnounceActivityValid (activity: any) { | ||
4 | return isBaseActivityValid(activity, 'Announce') && | ||
5 | ( | ||
6 | isActivityPubUrlValid(activity.object) || | ||
7 | (activity.object && isActivityPubUrlValid(activity.object.id)) | ||
8 | ) | ||
9 | } | ||
10 | |||
11 | export { | ||
12 | isAnnounceActivityValid | ||
13 | } | ||
diff --git a/server/helpers/custom-validators/activitypub/cache-file.ts b/server/helpers/custom-validators/activitypub/cache-file.ts index bd70934c8..21d5c53ca 100644 --- a/server/helpers/custom-validators/activitypub/cache-file.ts +++ b/server/helpers/custom-validators/activitypub/cache-file.ts | |||
@@ -1,28 +1,26 @@ | |||
1 | import { isActivityPubUrlValid, isBaseActivityValid } from './misc' | 1 | import { isActivityPubUrlValid } from './misc' |
2 | import { isRemoteVideoUrlValid } from './videos' | 2 | import { isRemoteVideoUrlValid } from './videos' |
3 | import { isDateValid, exists } from '../misc' | 3 | import { exists, isDateValid } from '../misc' |
4 | import { CacheFileObject } from '../../../../shared/models/activitypub/objects' | 4 | import { CacheFileObject } from '../../../../shared/models/activitypub/objects' |
5 | 5 | ||
6 | function isCacheFileCreateActivityValid (activity: any) { | ||
7 | return isBaseActivityValid(activity, 'Create') && | ||
8 | isCacheFileObjectValid(activity.object) | ||
9 | } | ||
10 | |||
11 | function isCacheFileUpdateActivityValid (activity: any) { | ||
12 | return isBaseActivityValid(activity, 'Update') && | ||
13 | isCacheFileObjectValid(activity.object) | ||
14 | } | ||
15 | |||
16 | function isCacheFileObjectValid (object: CacheFileObject) { | 6 | function isCacheFileObjectValid (object: CacheFileObject) { |
17 | return exists(object) && | 7 | return exists(object) && |
18 | object.type === 'CacheFile' && | 8 | object.type === 'CacheFile' && |
19 | isDateValid(object.expires) && | 9 | isDateValid(object.expires) && |
20 | isActivityPubUrlValid(object.object) && | 10 | isActivityPubUrlValid(object.object) && |
21 | isRemoteVideoUrlValid(object.url) | 11 | (isRemoteVideoUrlValid(object.url) || isPlaylistRedundancyUrlValid(object.url)) |
22 | } | 12 | } |
23 | 13 | ||
14 | // --------------------------------------------------------------------------- | ||
15 | |||
24 | export { | 16 | export { |
25 | isCacheFileUpdateActivityValid, | ||
26 | isCacheFileCreateActivityValid, | ||
27 | isCacheFileObjectValid | 17 | isCacheFileObjectValid |
28 | } | 18 | } |
19 | |||
20 | // --------------------------------------------------------------------------- | ||
21 | |||
22 | function isPlaylistRedundancyUrlValid (url: any) { | ||
23 | return url.type === 'Link' && | ||
24 | (url.mediaType || url.mimeType) === 'application/x-mpegURL' && | ||
25 | isActivityPubUrlValid(url.href) | ||
26 | } | ||
diff --git a/server/helpers/custom-validators/activitypub/flag.ts b/server/helpers/custom-validators/activitypub/flag.ts new file mode 100644 index 000000000..6452e297c --- /dev/null +++ b/server/helpers/custom-validators/activitypub/flag.ts | |||
@@ -0,0 +1,14 @@ | |||
1 | import { isActivityPubUrlValid } from './misc' | ||
2 | import { isVideoAbuseReasonValid } from '../video-abuses' | ||
3 | |||
4 | function isFlagActivityValid (activity: any) { | ||
5 | return activity.type === 'Flag' && | ||
6 | isVideoAbuseReasonValid(activity.content) && | ||
7 | isActivityPubUrlValid(activity.object) | ||
8 | } | ||
9 | |||
10 | // --------------------------------------------------------------------------- | ||
11 | |||
12 | export { | ||
13 | isFlagActivityValid | ||
14 | } | ||
diff --git a/server/helpers/custom-validators/activitypub/misc.ts b/server/helpers/custom-validators/activitypub/misc.ts index 4e2c57f04..f1762d11c 100644 --- a/server/helpers/custom-validators/activitypub/misc.ts +++ b/server/helpers/custom-validators/activitypub/misc.ts | |||
@@ -28,15 +28,20 @@ function isBaseActivityValid (activity: any, type: string) { | |||
28 | return (activity['@context'] === undefined || Array.isArray(activity['@context'])) && | 28 | return (activity['@context'] === undefined || Array.isArray(activity['@context'])) && |
29 | activity.type === type && | 29 | activity.type === type && |
30 | isActivityPubUrlValid(activity.id) && | 30 | isActivityPubUrlValid(activity.id) && |
31 | exists(activity.actor) && | 31 | isObjectValid(activity.actor) && |
32 | (isActivityPubUrlValid(activity.actor) || isActivityPubUrlValid(activity.actor.id)) && | 32 | isUrlCollectionValid(activity.to) && |
33 | ( | 33 | isUrlCollectionValid(activity.cc) |
34 | activity.to === undefined || | 34 | } |
35 | (Array.isArray(activity.to) && activity.to.every(t => isActivityPubUrlValid(t))) | 35 | |
36 | ) && | 36 | function isUrlCollectionValid (collection: any) { |
37 | return collection === undefined || | ||
38 | (Array.isArray(collection) && collection.every(t => isActivityPubUrlValid(t))) | ||
39 | } | ||
40 | |||
41 | function isObjectValid (object: any) { | ||
42 | return exists(object) && | ||
37 | ( | 43 | ( |
38 | activity.cc === undefined || | 44 | isActivityPubUrlValid(object) || isActivityPubUrlValid(object.id) |
39 | (Array.isArray(activity.cc) && activity.cc.every(t => isActivityPubUrlValid(t))) | ||
40 | ) | 45 | ) |
41 | } | 46 | } |
42 | 47 | ||
@@ -57,5 +62,6 @@ export { | |||
57 | isUrlValid, | 62 | isUrlValid, |
58 | isActivityPubUrlValid, | 63 | isActivityPubUrlValid, |
59 | isBaseActivityValid, | 64 | isBaseActivityValid, |
60 | setValidAttributedTo | 65 | setValidAttributedTo, |
66 | isObjectValid | ||
61 | } | 67 | } |
diff --git a/server/helpers/custom-validators/activitypub/rate.ts b/server/helpers/custom-validators/activitypub/rate.ts index e70bd94b8..ba68e8074 100644 --- a/server/helpers/custom-validators/activitypub/rate.ts +++ b/server/helpers/custom-validators/activitypub/rate.ts | |||
@@ -1,20 +1,13 @@ | |||
1 | import { isActivityPubUrlValid, isBaseActivityValid } from './misc' | 1 | import { isActivityPubUrlValid, isObjectValid } from './misc' |
2 | |||
3 | function isLikeActivityValid (activity: any) { | ||
4 | return isBaseActivityValid(activity, 'Like') && | ||
5 | isActivityPubUrlValid(activity.object) | ||
6 | } | ||
7 | 2 | ||
8 | function isDislikeActivityValid (activity: any) { | 3 | function isDislikeActivityValid (activity: any) { |
9 | return isBaseActivityValid(activity, 'Create') && | 4 | return activity.type === 'Dislike' && |
10 | activity.object.type === 'Dislike' && | 5 | isActivityPubUrlValid(activity.actor) && |
11 | isActivityPubUrlValid(activity.object.actor) && | 6 | isObjectValid(activity.object) |
12 | isActivityPubUrlValid(activity.object.object) | ||
13 | } | 7 | } |
14 | 8 | ||
15 | // --------------------------------------------------------------------------- | 9 | // --------------------------------------------------------------------------- |
16 | 10 | ||
17 | export { | 11 | export { |
18 | isLikeActivityValid, | ||
19 | isDislikeActivityValid | 12 | isDislikeActivityValid |
20 | } | 13 | } |
diff --git a/server/helpers/custom-validators/activitypub/undo.ts b/server/helpers/custom-validators/activitypub/undo.ts deleted file mode 100644 index 578035893..000000000 --- a/server/helpers/custom-validators/activitypub/undo.ts +++ /dev/null | |||
@@ -1,20 +0,0 @@ | |||
1 | import { isActorFollowActivityValid } from './actor' | ||
2 | import { isBaseActivityValid } from './misc' | ||
3 | import { isDislikeActivityValid, isLikeActivityValid } from './rate' | ||
4 | import { isAnnounceActivityValid } from './announce' | ||
5 | import { isCacheFileCreateActivityValid } from './cache-file' | ||
6 | |||
7 | function isUndoActivityValid (activity: any) { | ||
8 | return isBaseActivityValid(activity, 'Undo') && | ||
9 | ( | ||
10 | isActorFollowActivityValid(activity.object) || | ||
11 | isLikeActivityValid(activity.object) || | ||
12 | isDislikeActivityValid(activity.object) || | ||
13 | isAnnounceActivityValid(activity.object) || | ||
14 | isCacheFileCreateActivityValid(activity.object) | ||
15 | ) | ||
16 | } | ||
17 | |||
18 | export { | ||
19 | isUndoActivityValid | ||
20 | } | ||
diff --git a/server/helpers/custom-validators/activitypub/video-comments.ts b/server/helpers/custom-validators/activitypub/video-comments.ts index 051c4565a..0415db21c 100644 --- a/server/helpers/custom-validators/activitypub/video-comments.ts +++ b/server/helpers/custom-validators/activitypub/video-comments.ts | |||
@@ -3,11 +3,6 @@ import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers' | |||
3 | import { exists, isArray, isDateValid } from '../misc' | 3 | import { exists, isArray, isDateValid } from '../misc' |
4 | import { isActivityPubUrlValid, isBaseActivityValid } from './misc' | 4 | import { isActivityPubUrlValid, isBaseActivityValid } from './misc' |
5 | 5 | ||
6 | function isVideoCommentCreateActivityValid (activity: any) { | ||
7 | return isBaseActivityValid(activity, 'Create') && | ||
8 | sanitizeAndCheckVideoCommentObject(activity.object) | ||
9 | } | ||
10 | |||
11 | function sanitizeAndCheckVideoCommentObject (comment: any) { | 6 | function sanitizeAndCheckVideoCommentObject (comment: any) { |
12 | if (!comment || comment.type !== 'Note') return false | 7 | if (!comment || comment.type !== 'Note') return false |
13 | 8 | ||
@@ -25,15 +20,9 @@ function sanitizeAndCheckVideoCommentObject (comment: any) { | |||
25 | ) // Only accept public comments | 20 | ) // Only accept public comments |
26 | } | 21 | } |
27 | 22 | ||
28 | function isVideoCommentDeleteActivityValid (activity: any) { | ||
29 | return isBaseActivityValid(activity, 'Delete') | ||
30 | } | ||
31 | |||
32 | // --------------------------------------------------------------------------- | 23 | // --------------------------------------------------------------------------- |
33 | 24 | ||
34 | export { | 25 | export { |
35 | isVideoCommentCreateActivityValid, | ||
36 | isVideoCommentDeleteActivityValid, | ||
37 | sanitizeAndCheckVideoCommentObject | 26 | sanitizeAndCheckVideoCommentObject |
38 | } | 27 | } |
39 | 28 | ||
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts index 5015c59dd..53ad0588d 100644 --- a/server/helpers/custom-validators/activitypub/videos.ts +++ b/server/helpers/custom-validators/activitypub/videos.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import * as validator from 'validator' | 1 | import * as validator from 'validator' |
2 | import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers' | 2 | import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers' |
3 | import { peertubeTruncate } from '../../core-utils' | 3 | import { peertubeTruncate } from '../../core-utils' |
4 | import { exists, isBooleanValid, isDateValid, isUUIDValid } from '../misc' | 4 | import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc' |
5 | import { | 5 | import { |
6 | isVideoDurationValid, | 6 | isVideoDurationValid, |
7 | isVideoNameValid, | 7 | isVideoNameValid, |
@@ -12,29 +12,12 @@ import { | |||
12 | } from '../videos' | 12 | } from '../videos' |
13 | import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' | 13 | import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' |
14 | import { VideoState } from '../../../../shared/models/videos' | 14 | import { VideoState } from '../../../../shared/models/videos' |
15 | import { isVideoAbuseReasonValid } from '../video-abuses' | ||
16 | |||
17 | function sanitizeAndCheckVideoTorrentCreateActivity (activity: any) { | ||
18 | return isBaseActivityValid(activity, 'Create') && | ||
19 | sanitizeAndCheckVideoTorrentObject(activity.object) | ||
20 | } | ||
21 | 15 | ||
22 | function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) { | 16 | function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) { |
23 | return isBaseActivityValid(activity, 'Update') && | 17 | return isBaseActivityValid(activity, 'Update') && |
24 | sanitizeAndCheckVideoTorrentObject(activity.object) | 18 | sanitizeAndCheckVideoTorrentObject(activity.object) |
25 | } | 19 | } |
26 | 20 | ||
27 | function isVideoTorrentDeleteActivityValid (activity: any) { | ||
28 | return isBaseActivityValid(activity, 'Delete') | ||
29 | } | ||
30 | |||
31 | function isVideoFlagValid (activity: any) { | ||
32 | return isBaseActivityValid(activity, 'Create') && | ||
33 | activity.object.type === 'Flag' && | ||
34 | isVideoAbuseReasonValid(activity.object.content) && | ||
35 | isActivityPubUrlValid(activity.object.object) | ||
36 | } | ||
37 | |||
38 | function isActivityPubVideoDurationValid (value: string) { | 21 | function isActivityPubVideoDurationValid (value: string) { |
39 | // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration | 22 | // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration |
40 | return exists(value) && | 23 | return exists(value) && |
@@ -83,32 +66,35 @@ function isRemoteVideoUrlValid (url: any) { | |||
83 | 66 | ||
84 | return url.type === 'Link' && | 67 | return url.type === 'Link' && |
85 | ( | 68 | ( |
86 | ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.indexOf(url.mimeType) !== -1 && | 69 | // TODO: remove mimeType (backward compatibility, introduced in v1.1.0) |
70 | ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.indexOf(url.mediaType || url.mimeType) !== -1 && | ||
87 | isActivityPubUrlValid(url.href) && | 71 | isActivityPubUrlValid(url.href) && |
88 | validator.isInt(url.height + '', { min: 0 }) && | 72 | validator.isInt(url.height + '', { min: 0 }) && |
89 | validator.isInt(url.size + '', { min: 0 }) && | 73 | validator.isInt(url.size + '', { min: 0 }) && |
90 | (!url.fps || validator.isInt(url.fps + '', { min: -1 })) | 74 | (!url.fps || validator.isInt(url.fps + '', { min: -1 })) |
91 | ) || | 75 | ) || |
92 | ( | 76 | ( |
93 | ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.indexOf(url.mimeType) !== -1 && | 77 | ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.indexOf(url.mediaType || url.mimeType) !== -1 && |
94 | isActivityPubUrlValid(url.href) && | 78 | isActivityPubUrlValid(url.href) && |
95 | validator.isInt(url.height + '', { min: 0 }) | 79 | validator.isInt(url.height + '', { min: 0 }) |
96 | ) || | 80 | ) || |
97 | ( | 81 | ( |
98 | ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mimeType) !== -1 && | 82 | ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mediaType || url.mimeType) !== -1 && |
99 | validator.isLength(url.href, { min: 5 }) && | 83 | validator.isLength(url.href, { min: 5 }) && |
100 | validator.isInt(url.height + '', { min: 0 }) | 84 | validator.isInt(url.height + '', { min: 0 }) |
85 | ) || | ||
86 | ( | ||
87 | (url.mediaType || url.mimeType) === 'application/x-mpegURL' && | ||
88 | isActivityPubUrlValid(url.href) && | ||
89 | isArray(url.tag) | ||
101 | ) | 90 | ) |
102 | } | 91 | } |
103 | 92 | ||
104 | // --------------------------------------------------------------------------- | 93 | // --------------------------------------------------------------------------- |
105 | 94 | ||
106 | export { | 95 | export { |
107 | sanitizeAndCheckVideoTorrentCreateActivity, | ||
108 | sanitizeAndCheckVideoTorrentUpdateActivity, | 96 | sanitizeAndCheckVideoTorrentUpdateActivity, |
109 | isVideoTorrentDeleteActivityValid, | ||
110 | isRemoteStringIdentifierValid, | 97 | isRemoteStringIdentifierValid, |
111 | isVideoFlagValid, | ||
112 | sanitizeAndCheckVideoTorrentObject, | 98 | sanitizeAndCheckVideoTorrentObject, |
113 | isRemoteVideoUrlValid | 99 | isRemoteVideoUrlValid |
114 | } | 100 | } |
diff --git a/server/helpers/custom-validators/activitypub/view.ts b/server/helpers/custom-validators/activitypub/view.ts index 7a3aca6f5..41d16469f 100644 --- a/server/helpers/custom-validators/activitypub/view.ts +++ b/server/helpers/custom-validators/activitypub/view.ts | |||
@@ -1,11 +1,11 @@ | |||
1 | import { isActivityPubUrlValid, isBaseActivityValid } from './misc' | 1 | import { isActivityPubUrlValid } from './misc' |
2 | 2 | ||
3 | function isViewActivityValid (activity: any) { | 3 | function isViewActivityValid (activity: any) { |
4 | return isBaseActivityValid(activity, 'Create') && | 4 | return activity.type === 'View' && |
5 | activity.object.type === 'View' && | 5 | isActivityPubUrlValid(activity.actor) && |
6 | isActivityPubUrlValid(activity.object.actor) && | 6 | isActivityPubUrlValid(activity.object) |
7 | isActivityPubUrlValid(activity.object.object) | ||
8 | } | 7 | } |
8 | |||
9 | // --------------------------------------------------------------------------- | 9 | // --------------------------------------------------------------------------- |
10 | 10 | ||
11 | export { | 11 | export { |
diff --git a/server/helpers/custom-validators/misc.ts b/server/helpers/custom-validators/misc.ts index 6d10a65a8..76647fea2 100644 --- a/server/helpers/custom-validators/misc.ts +++ b/server/helpers/custom-validators/misc.ts | |||
@@ -9,6 +9,14 @@ function isArray (value: any) { | |||
9 | return Array.isArray(value) | 9 | return Array.isArray(value) |
10 | } | 10 | } |
11 | 11 | ||
12 | function isNotEmptyIntArray (value: any) { | ||
13 | return Array.isArray(value) && value.every(v => validator.isInt('' + v)) && value.length !== 0 | ||
14 | } | ||
15 | |||
16 | function isArrayOf (value: any, validator: (value: any) => boolean) { | ||
17 | return isArray(value) && value.every(v => validator(v)) | ||
18 | } | ||
19 | |||
12 | function isDateValid (value: string) { | 20 | function isDateValid (value: string) { |
13 | return exists(value) && validator.isISO8601(value) | 21 | return exists(value) && validator.isISO8601(value) |
14 | } | 22 | } |
@@ -78,6 +86,8 @@ function isFileValid ( | |||
78 | 86 | ||
79 | export { | 87 | export { |
80 | exists, | 88 | exists, |
89 | isArrayOf, | ||
90 | isNotEmptyIntArray, | ||
81 | isArray, | 91 | isArray, |
82 | isIdValid, | 92 | isIdValid, |
83 | isUUIDValid, | 93 | isUUIDValid, |
diff --git a/server/helpers/custom-validators/servers.ts b/server/helpers/custom-validators/servers.ts index d5021bf38..18c80ec8f 100644 --- a/server/helpers/custom-validators/servers.ts +++ b/server/helpers/custom-validators/servers.ts | |||
@@ -3,6 +3,7 @@ import 'express-validator' | |||
3 | 3 | ||
4 | import { isArray, exists } from './misc' | 4 | import { isArray, exists } from './misc' |
5 | import { isTestInstance } from '../core-utils' | 5 | import { isTestInstance } from '../core-utils' |
6 | import { CONSTRAINTS_FIELDS } from '../../initializers' | ||
6 | 7 | ||
7 | function isHostValid (host: string) { | 8 | function isHostValid (host: string) { |
8 | const isURLOptions = { | 9 | const isURLOptions = { |
@@ -26,9 +27,19 @@ function isEachUniqueHostValid (hosts: string[]) { | |||
26 | }) | 27 | }) |
27 | } | 28 | } |
28 | 29 | ||
30 | function isValidContactBody (value: any) { | ||
31 | return exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.CONTACT_FORM.BODY) | ||
32 | } | ||
33 | |||
34 | function isValidContactFromName (value: any) { | ||
35 | return exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.CONTACT_FORM.FROM_NAME) | ||
36 | } | ||
37 | |||
29 | // --------------------------------------------------------------------------- | 38 | // --------------------------------------------------------------------------- |
30 | 39 | ||
31 | export { | 40 | export { |
41 | isValidContactBody, | ||
42 | isValidContactFromName, | ||
32 | isEachUniqueHostValid, | 43 | isEachUniqueHostValid, |
33 | isHostValid | 44 | isHostValid |
34 | } | 45 | } |
diff --git a/server/helpers/custom-validators/user-notifications.ts b/server/helpers/custom-validators/user-notifications.ts new file mode 100644 index 000000000..02ea3bbc2 --- /dev/null +++ b/server/helpers/custom-validators/user-notifications.ts | |||
@@ -0,0 +1,23 @@ | |||
1 | import { exists } from './misc' | ||
2 | import * as validator from 'validator' | ||
3 | import { UserNotificationType } from '../../../shared/models/users' | ||
4 | import { UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' | ||
5 | |||
6 | function isUserNotificationTypeValid (value: any) { | ||
7 | return exists(value) && validator.isInt('' + value) && UserNotificationType[value] !== undefined | ||
8 | } | ||
9 | |||
10 | function isUserNotificationSettingValid (value: any) { | ||
11 | return exists(value) && | ||
12 | validator.isInt('' + value) && ( | ||
13 | value === UserNotificationSettingValue.NONE || | ||
14 | value === UserNotificationSettingValue.WEB || | ||
15 | value === UserNotificationSettingValue.EMAIL || | ||
16 | value === (UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL) | ||
17 | ) | ||
18 | } | ||
19 | |||
20 | export { | ||
21 | isUserNotificationSettingValid, | ||
22 | isUserNotificationTypeValid | ||
23 | } | ||
diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts index 90fc74a48..80652b479 100644 --- a/server/helpers/custom-validators/users.ts +++ b/server/helpers/custom-validators/users.ts | |||
@@ -42,6 +42,14 @@ function isUserNSFWPolicyValid (value: any) { | |||
42 | return exists(value) && nsfwPolicies.indexOf(value) !== -1 | 42 | return exists(value) && nsfwPolicies.indexOf(value) !== -1 |
43 | } | 43 | } |
44 | 44 | ||
45 | function isUserWebTorrentEnabledValid (value: any) { | ||
46 | return isBooleanValid(value) | ||
47 | } | ||
48 | |||
49 | function isUserVideosHistoryEnabledValid (value: any) { | ||
50 | return isBooleanValid(value) | ||
51 | } | ||
52 | |||
45 | function isUserAutoPlayVideoValid (value: any) { | 53 | function isUserAutoPlayVideoValid (value: any) { |
46 | return isBooleanValid(value) | 54 | return isBooleanValid(value) |
47 | } | 55 | } |
@@ -69,6 +77,7 @@ function isAvatarFile (files: { [ fieldname: string ]: Express.Multer.File[] } | | |||
69 | // --------------------------------------------------------------------------- | 77 | // --------------------------------------------------------------------------- |
70 | 78 | ||
71 | export { | 79 | export { |
80 | isUserVideosHistoryEnabledValid, | ||
72 | isUserBlockedValid, | 81 | isUserBlockedValid, |
73 | isUserPasswordValid, | 82 | isUserPasswordValid, |
74 | isUserBlockedReasonValid, | 83 | isUserBlockedReasonValid, |
@@ -78,6 +87,7 @@ export { | |||
78 | isUserUsernameValid, | 87 | isUserUsernameValid, |
79 | isUserEmailVerifiedValid, | 88 | isUserEmailVerifiedValid, |
80 | isUserNSFWPolicyValid, | 89 | isUserNSFWPolicyValid, |
90 | isUserWebTorrentEnabledValid, | ||
81 | isUserAutoPlayVideoValid, | 91 | isUserAutoPlayVideoValid, |
82 | isUserDisplayNameValid, | 92 | isUserDisplayNameValid, |
83 | isUserDescriptionValid, | 93 | isUserDescriptionValid, |
diff --git a/server/helpers/custom-validators/video-captions.ts b/server/helpers/custom-validators/video-captions.ts index 177e9e86e..b33d90e18 100644 --- a/server/helpers/custom-validators/video-captions.ts +++ b/server/helpers/custom-validators/video-captions.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { CONSTRAINTS_FIELDS, VIDEO_CAPTIONS_MIMETYPE_EXT, VIDEO_LANGUAGES } from '../../initializers' | 1 | import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_LANGUAGES } from '../../initializers' |
2 | import { exists, isFileValid } from './misc' | 2 | import { exists, isFileValid } from './misc' |
3 | import { Response } from 'express' | 3 | import { Response } from 'express' |
4 | import { VideoModel } from '../../models/video/video' | 4 | import { VideoModel } from '../../models/video/video' |
@@ -8,7 +8,7 @@ function isVideoCaptionLanguageValid (value: any) { | |||
8 | return exists(value) && VIDEO_LANGUAGES[ value ] !== undefined | 8 | return exists(value) && VIDEO_LANGUAGES[ value ] !== undefined |
9 | } | 9 | } |
10 | 10 | ||
11 | const videoCaptionTypes = Object.keys(VIDEO_CAPTIONS_MIMETYPE_EXT) | 11 | const videoCaptionTypes = Object.keys(MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT) |
12 | .concat([ 'application/octet-stream' ]) // MacOS sends application/octet-stream >< | 12 | .concat([ 'application/octet-stream' ]) // MacOS sends application/octet-stream >< |
13 | .map(m => `(${m})`) | 13 | .map(m => `(${m})`) |
14 | const videoCaptionTypesRegex = videoCaptionTypes.join('|') | 14 | const videoCaptionTypesRegex = videoCaptionTypes.join('|') |
diff --git a/server/helpers/custom-validators/video-imports.ts b/server/helpers/custom-validators/video-imports.ts index 4d6ab1fa4..ce9e9193c 100644 --- a/server/helpers/custom-validators/video-imports.ts +++ b/server/helpers/custom-validators/video-imports.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import 'express-validator' | 1 | import 'express-validator' |
2 | import 'multer' | 2 | import 'multer' |
3 | import * as validator from 'validator' | 3 | import * as validator from 'validator' |
4 | import { CONSTRAINTS_FIELDS, TORRENT_MIMETYPE_EXT, VIDEO_IMPORT_STATES } from '../../initializers' | 4 | import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_IMPORT_STATES } from '../../initializers' |
5 | import { exists, isFileValid } from './misc' | 5 | import { exists, isFileValid } from './misc' |
6 | import * as express from 'express' | 6 | import * as express from 'express' |
7 | import { VideoImportModel } from '../../models/video/video-import' | 7 | import { VideoImportModel } from '../../models/video/video-import' |
@@ -24,7 +24,7 @@ function isVideoImportStateValid (value: any) { | |||
24 | return exists(value) && VIDEO_IMPORT_STATES[ value ] !== undefined | 24 | return exists(value) && VIDEO_IMPORT_STATES[ value ] !== undefined |
25 | } | 25 | } |
26 | 26 | ||
27 | const videoTorrentImportTypes = Object.keys(TORRENT_MIMETYPE_EXT).map(m => `(${m})`) | 27 | const videoTorrentImportTypes = Object.keys(MIMETYPES.TORRENT.MIMETYPE_EXT).map(m => `(${m})`) |
28 | const videoTorrentImportRegex = videoTorrentImportTypes.join('|') | 28 | const videoTorrentImportRegex = videoTorrentImportTypes.join('|') |
29 | function isVideoImportTorrentFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { | 29 | function isVideoImportTorrentFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { |
30 | return isFileValid(files, videoTorrentImportRegex, 'torrentfile', CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_FILE.FILE_SIZE.max, true) | 30 | return isFileValid(files, videoTorrentImportRegex, 'torrentfile', CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_FILE.FILE_SIZE.max, true) |
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts index 714f7ac95..95e256b8f 100644 --- a/server/helpers/custom-validators/videos.ts +++ b/server/helpers/custom-validators/videos.ts | |||
@@ -3,12 +3,11 @@ import 'express-validator' | |||
3 | import { values } from 'lodash' | 3 | import { values } from 'lodash' |
4 | import 'multer' | 4 | import 'multer' |
5 | import * as validator from 'validator' | 5 | import * as validator from 'validator' |
6 | import { UserRight, VideoPrivacy, VideoRateType } from '../../../shared' | 6 | import { UserRight, VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared' |
7 | import { | 7 | import { |
8 | CONSTRAINTS_FIELDS, | 8 | CONSTRAINTS_FIELDS, MIMETYPES, |
9 | VIDEO_CATEGORIES, | 9 | VIDEO_CATEGORIES, |
10 | VIDEO_LICENCES, | 10 | VIDEO_LICENCES, |
11 | VIDEO_MIMETYPE_EXT, | ||
12 | VIDEO_PRIVACIES, | 11 | VIDEO_PRIVACIES, |
13 | VIDEO_RATE_TYPES, | 12 | VIDEO_RATE_TYPES, |
14 | VIDEO_STATES | 13 | VIDEO_STATES |
@@ -22,6 +21,10 @@ import { fetchVideo, VideoFetchType } from '../video' | |||
22 | 21 | ||
23 | const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS | 22 | const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS |
24 | 23 | ||
24 | function isVideoFilterValid (filter: VideoFilter) { | ||
25 | return filter === 'local' || filter === 'all-local' | ||
26 | } | ||
27 | |||
25 | function isVideoCategoryValid (value: any) { | 28 | function isVideoCategoryValid (value: any) { |
26 | return value === null || VIDEO_CATEGORIES[ value ] !== undefined | 29 | return value === null || VIDEO_CATEGORIES[ value ] !== undefined |
27 | } | 30 | } |
@@ -79,10 +82,15 @@ function isVideoRatingTypeValid (value: string) { | |||
79 | return value === 'none' || values(VIDEO_RATE_TYPES).indexOf(value as VideoRateType) !== -1 | 82 | return value === 'none' || values(VIDEO_RATE_TYPES).indexOf(value as VideoRateType) !== -1 |
80 | } | 83 | } |
81 | 84 | ||
82 | const videoFileTypes = Object.keys(VIDEO_MIMETYPE_EXT).map(m => `(${m})`) | 85 | function isVideoFileExtnameValid (value: string) { |
83 | const videoFileTypesRegex = videoFileTypes.join('|') | 86 | return exists(value) && MIMETYPES.VIDEO.EXT_MIMETYPE[value] !== undefined |
87 | } | ||
84 | 88 | ||
85 | function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { | 89 | function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { |
90 | const videoFileTypesRegex = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT) | ||
91 | .map(m => `(${m})`) | ||
92 | .join('|') | ||
93 | |||
86 | return isFileValid(files, videoFileTypesRegex, 'videofile', null) | 94 | return isFileValid(files, videoFileTypesRegex, 'videofile', null) |
87 | } | 95 | } |
88 | 96 | ||
@@ -217,6 +225,7 @@ export { | |||
217 | isVideoStateValid, | 225 | isVideoStateValid, |
218 | isVideoViewsValid, | 226 | isVideoViewsValid, |
219 | isVideoRatingTypeValid, | 227 | isVideoRatingTypeValid, |
228 | isVideoFileExtnameValid, | ||
220 | isVideoDurationValid, | 229 | isVideoDurationValid, |
221 | isVideoTagValid, | 230 | isVideoTagValid, |
222 | isVideoPrivacyValid, | 231 | isVideoPrivacyValid, |
@@ -225,5 +234,6 @@ export { | |||
225 | isVideoExist, | 234 | isVideoExist, |
226 | isVideoImage, | 235 | isVideoImage, |
227 | isVideoChannelOfAccountExist, | 236 | isVideoChannelOfAccountExist, |
228 | isVideoSupportValid | 237 | isVideoSupportValid, |
238 | isVideoFilterValid | ||
229 | } | 239 | } |
diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts index 8a9cee8c5..9a72ee96d 100644 --- a/server/helpers/express-utils.ts +++ b/server/helpers/express-utils.ts | |||
@@ -2,18 +2,17 @@ import * as express from 'express' | |||
2 | import * as multer from 'multer' | 2 | import * as multer from 'multer' |
3 | import { CONFIG, REMOTE_SCHEME } from '../initializers' | 3 | import { CONFIG, REMOTE_SCHEME } from '../initializers' |
4 | import { logger } from './logger' | 4 | import { logger } from './logger' |
5 | import { User } from '../../shared/models/users' | ||
6 | import { deleteFileAsync, generateRandomString } from './utils' | 5 | import { deleteFileAsync, generateRandomString } from './utils' |
7 | import { extname } from 'path' | 6 | import { extname } from 'path' |
8 | import { isArray } from './custom-validators/misc' | 7 | import { isArray } from './custom-validators/misc' |
9 | import { UserModel } from '../models/account/user' | 8 | import { UserModel } from '../models/account/user' |
10 | 9 | ||
11 | function buildNSFWFilter (res: express.Response, paramNSFW?: string) { | 10 | function buildNSFWFilter (res?: express.Response, paramNSFW?: string) { |
12 | if (paramNSFW === 'true') return true | 11 | if (paramNSFW === 'true') return true |
13 | if (paramNSFW === 'false') return false | 12 | if (paramNSFW === 'false') return false |
14 | if (paramNSFW === 'both') return undefined | 13 | if (paramNSFW === 'both') return undefined |
15 | 14 | ||
16 | if (res.locals.oauth) { | 15 | if (res && res.locals.oauth) { |
17 | const user: UserModel = res.locals.oauth.token.User | 16 | const user: UserModel = res.locals.oauth.token.User |
18 | 17 | ||
19 | // User does not want NSFW videos | 18 | // User does not want NSFW videos |
@@ -101,7 +100,7 @@ function createReqFiles ( | |||
101 | } | 100 | } |
102 | 101 | ||
103 | function isUserAbleToSearchRemoteURI (res: express.Response) { | 102 | function isUserAbleToSearchRemoteURI (res: express.Response) { |
104 | const user: User = res.locals.oauth ? res.locals.oauth.token.User : undefined | 103 | const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined |
105 | 104 | ||
106 | return CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true || | 105 | return CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true || |
107 | (CONFIG.SEARCH.REMOTE_URI.USERS === true && user !== undefined) | 106 | (CONFIG.SEARCH.REMOTE_URI.USERS === true && user !== undefined) |
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 22bc25476..133b1b03b 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import * as ffmpeg from 'fluent-ffmpeg' | 1 | import * as ffmpeg from 'fluent-ffmpeg' |
2 | import { join } from 'path' | 2 | import { dirname, join } from 'path' |
3 | import { VideoResolution } from '../../shared/models/videos' | 3 | import { getTargetBitrate, VideoResolution } from '../../shared/models/videos' |
4 | import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers' | 4 | import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants' |
5 | import { processImage } from './image-utils' | 5 | import { processImage } from './image-utils' |
6 | import { logger } from './logger' | 6 | import { logger } from './logger' |
7 | import { checkFFmpegEncoders } from '../initializers/checker-before-init' | 7 | import { checkFFmpegEncoders } from '../initializers/checker-before-init' |
@@ -29,19 +29,28 @@ function computeResolutionsToTranscode (videoFileHeight: number) { | |||
29 | return resolutionsEnabled | 29 | return resolutionsEnabled |
30 | } | 30 | } |
31 | 31 | ||
32 | async function getVideoFileResolution (path: string) { | 32 | async function getVideoFileSize (path: string) { |
33 | const videoStream = await getVideoFileStream(path) | 33 | const videoStream = await getVideoFileStream(path) |
34 | 34 | ||
35 | return { | 35 | return { |
36 | videoFileResolution: Math.min(videoStream.height, videoStream.width), | 36 | width: videoStream.width, |
37 | isPortraitMode: videoStream.height > videoStream.width | 37 | height: videoStream.height |
38 | } | ||
39 | } | ||
40 | |||
41 | async function getVideoFileResolution (path: string) { | ||
42 | const size = await getVideoFileSize(path) | ||
43 | |||
44 | return { | ||
45 | videoFileResolution: Math.min(size.height, size.width), | ||
46 | isPortraitMode: size.height > size.width | ||
38 | } | 47 | } |
39 | } | 48 | } |
40 | 49 | ||
41 | async function getVideoFileFPS (path: string) { | 50 | async function getVideoFileFPS (path: string) { |
42 | const videoStream = await getVideoFileStream(path) | 51 | const videoStream = await getVideoFileStream(path) |
43 | 52 | ||
44 | for (const key of [ 'r_frame_rate' , 'avg_frame_rate' ]) { | 53 | for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) { |
45 | const valuesText: string = videoStream[key] | 54 | const valuesText: string = videoStream[key] |
46 | if (!valuesText) continue | 55 | if (!valuesText) continue |
47 | 56 | ||
@@ -55,6 +64,16 @@ async function getVideoFileFPS (path: string) { | |||
55 | return 0 | 64 | return 0 |
56 | } | 65 | } |
57 | 66 | ||
67 | async function getVideoFileBitrate (path: string) { | ||
68 | return new Promise<number>((res, rej) => { | ||
69 | ffmpeg.ffprobe(path, (err, metadata) => { | ||
70 | if (err) return rej(err) | ||
71 | |||
72 | return res(metadata.format.bit_rate) | ||
73 | }) | ||
74 | }) | ||
75 | } | ||
76 | |||
58 | function getDurationFromVideoFile (path: string) { | 77 | function getDurationFromVideoFile (path: string) { |
59 | return new Promise<number>((res, rej) => { | 78 | return new Promise<number>((res, rej) => { |
60 | ffmpeg.ffprobe(path, (err, metadata) => { | 79 | ffmpeg.ffprobe(path, (err, metadata) => { |
@@ -100,64 +119,87 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima | |||
100 | type TranscodeOptions = { | 119 | type TranscodeOptions = { |
101 | inputPath: string | 120 | inputPath: string |
102 | outputPath: string | 121 | outputPath: string |
103 | resolution?: VideoResolution | 122 | resolution: VideoResolution |
104 | isPortraitMode?: boolean | 123 | isPortraitMode?: boolean |
124 | |||
125 | hlsPlaylist?: { | ||
126 | videoFilename: string | ||
127 | } | ||
105 | } | 128 | } |
106 | 129 | ||
107 | function transcode (options: TranscodeOptions) { | 130 | function transcode (options: TranscodeOptions) { |
108 | return new Promise<void>(async (res, rej) => { | 131 | return new Promise<void>(async (res, rej) => { |
109 | let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING }) | 132 | try { |
110 | .output(options.outputPath) | 133 | let fps = await getVideoFileFPS(options.inputPath) |
111 | .preset(standard) | ||
112 | |||
113 | if (CONFIG.TRANSCODING.THREADS > 0) { | ||
114 | // if we don't set any threads ffmpeg will chose automatically | ||
115 | command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) | ||
116 | } | ||
117 | |||
118 | let fps = await getVideoFileFPS(options.inputPath) | ||
119 | if (options.resolution !== undefined) { | ||
120 | // '?x720' or '720x?' for example | ||
121 | const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}` | ||
122 | command = command.size(size) | ||
123 | |||
124 | // On small/medium resolutions, limit FPS | 134 | // On small/medium resolutions, limit FPS |
125 | if ( | 135 | if ( |
136 | options.resolution !== undefined && | ||
126 | options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN && | 137 | options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN && |
127 | fps > VIDEO_TRANSCODING_FPS.AVERAGE | 138 | fps > VIDEO_TRANSCODING_FPS.AVERAGE |
128 | ) { | 139 | ) { |
129 | fps = VIDEO_TRANSCODING_FPS.AVERAGE | 140 | fps = VIDEO_TRANSCODING_FPS.AVERAGE |
130 | } | 141 | } |
131 | } | ||
132 | 142 | ||
133 | if (fps) { | 143 | let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING }) |
134 | // Hard FPS limits | 144 | .output(options.outputPath) |
135 | if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX | 145 | command = await presetH264(command, options.resolution, fps) |
136 | else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN | ||
137 | 146 | ||
138 | command = command.withFPS(fps) | 147 | if (CONFIG.TRANSCODING.THREADS > 0) { |
139 | } | 148 | // if we don't set any threads ffmpeg will chose automatically |
149 | command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) | ||
150 | } | ||
151 | |||
152 | if (options.resolution !== undefined) { | ||
153 | // '?x720' or '720x?' for example | ||
154 | const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}` | ||
155 | command = command.size(size) | ||
156 | } | ||
157 | |||
158 | if (fps) { | ||
159 | // Hard FPS limits | ||
160 | if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX | ||
161 | else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN | ||
162 | |||
163 | command = command.withFPS(fps) | ||
164 | } | ||
165 | |||
166 | if (options.hlsPlaylist) { | ||
167 | const videoPath = `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}` | ||
168 | |||
169 | command = command.outputOption('-hls_time 4') | ||
170 | .outputOption('-hls_list_size 0') | ||
171 | .outputOption('-hls_playlist_type vod') | ||
172 | .outputOption('-hls_segment_filename ' + videoPath) | ||
173 | .outputOption('-hls_segment_type fmp4') | ||
174 | .outputOption('-f hls') | ||
175 | .outputOption('-hls_flags single_file') | ||
176 | } | ||
140 | 177 | ||
141 | command | 178 | command |
142 | .on('error', (err, stdout, stderr) => { | 179 | .on('error', (err, stdout, stderr) => { |
143 | logger.error('Error in transcoding job.', { stdout, stderr }) | 180 | logger.error('Error in transcoding job.', { stdout, stderr }) |
144 | return rej(err) | 181 | return rej(err) |
145 | }) | 182 | }) |
146 | .on('end', res) | 183 | .on('end', res) |
147 | .run() | 184 | .run() |
185 | } catch (err) { | ||
186 | return rej(err) | ||
187 | } | ||
148 | }) | 188 | }) |
149 | } | 189 | } |
150 | 190 | ||
151 | // --------------------------------------------------------------------------- | 191 | // --------------------------------------------------------------------------- |
152 | 192 | ||
153 | export { | 193 | export { |
194 | getVideoFileSize, | ||
154 | getVideoFileResolution, | 195 | getVideoFileResolution, |
155 | getDurationFromVideoFile, | 196 | getDurationFromVideoFile, |
156 | generateImageFromVideoFile, | 197 | generateImageFromVideoFile, |
157 | transcode, | 198 | transcode, |
158 | getVideoFileFPS, | 199 | getVideoFileFPS, |
159 | computeResolutionsToTranscode, | 200 | computeResolutionsToTranscode, |
160 | audio | 201 | audio, |
202 | getVideoFileBitrate | ||
161 | } | 203 | } |
162 | 204 | ||
163 | // --------------------------------------------------------------------------- | 205 | // --------------------------------------------------------------------------- |
@@ -168,7 +210,7 @@ function getVideoFileStream (path: string) { | |||
168 | if (err) return rej(err) | 210 | if (err) return rej(err) |
169 | 211 | ||
170 | const videoStream = metadata.streams.find(s => s.codec_type === 'video') | 212 | const videoStream = metadata.streams.find(s => s.codec_type === 'video') |
171 | if (!videoStream) throw new Error('Cannot find video stream of ' + path) | 213 | if (!videoStream) return rej(new Error('Cannot find video stream of ' + path)) |
172 | 214 | ||
173 | return res(videoStream) | 215 | return res(videoStream) |
174 | }) | 216 | }) |
@@ -182,11 +224,10 @@ function getVideoFileStream (path: string) { | |||
182 | * and quality. Superfast and ultrafast will give you better | 224 | * and quality. Superfast and ultrafast will give you better |
183 | * performance, but then quality is noticeably worse. | 225 | * performance, but then quality is noticeably worse. |
184 | */ | 226 | */ |
185 | function veryfast (_ffmpeg) { | 227 | async function presetH264VeryFast (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise<ffmpeg.FfmpegCommand> { |
186 | _ffmpeg | 228 | let localCommand = await presetH264(command, resolution, fps) |
187 | .preset(standard) | 229 | localCommand = localCommand.outputOption('-preset:v veryfast') |
188 | .outputOption('-preset:v veryfast') | 230 | .outputOption([ '--aq-mode=2', '--aq-strength=1.3' ]) |
189 | .outputOption(['--aq-mode=2', '--aq-strength=1.3']) | ||
190 | /* | 231 | /* |
191 | MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html | 232 | MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html |
192 | Our target situation is closer to a livestream than a stream, | 233 | Our target situation is closer to a livestream than a stream, |
@@ -198,31 +239,39 @@ function veryfast (_ffmpeg) { | |||
198 | Make up for most of the loss of grain and macroblocking | 239 | Make up for most of the loss of grain and macroblocking |
199 | with less computing power. | 240 | with less computing power. |
200 | */ | 241 | */ |
242 | |||
243 | return localCommand | ||
201 | } | 244 | } |
202 | 245 | ||
203 | /** | 246 | /** |
204 | * A preset optimised for a stillimage audio video | 247 | * A preset optimised for a stillimage audio video |
205 | */ | 248 | */ |
206 | function audio (_ffmpeg) { | 249 | async function presetStillImageWithAudio ( |
207 | _ffmpeg | 250 | command: ffmpeg.FfmpegCommand, |
208 | .preset(veryfast) | 251 | resolution: VideoResolution, |
209 | .outputOption('-tune stillimage') | 252 | fps: number |
253 | ): Promise<ffmpeg.FfmpegCommand> { | ||
254 | let localCommand = await presetH264VeryFast(command, resolution, fps) | ||
255 | localCommand = localCommand.outputOption('-tune stillimage') | ||
256 | |||
257 | return localCommand | ||
210 | } | 258 | } |
211 | 259 | ||
212 | /** | 260 | /** |
213 | * A toolbox to play with audio | 261 | * A toolbox to play with audio |
214 | */ | 262 | */ |
215 | namespace audio { | 263 | namespace audio { |
216 | export const get = (_ffmpeg, pos: number | string = 0) => { | 264 | export const get = (option: ffmpeg.FfmpegCommand | string) => { |
217 | // without position, ffprobe considers the last input only | 265 | // without position, ffprobe considers the last input only |
218 | // we make it consider the first input only | 266 | // we make it consider the first input only |
219 | // if you pass a file path to pos, then ffprobe acts on that file directly | 267 | // if you pass a file path to pos, then ffprobe acts on that file directly |
220 | return new Promise<{ absolutePath: string, audioStream?: any }>((res, rej) => { | 268 | return new Promise<{ absolutePath: string, audioStream?: any }>((res, rej) => { |
221 | _ffmpeg.ffprobe(pos, (err,data) => { | 269 | |
270 | function parseFfprobe (err: any, data: ffmpeg.FfprobeData) { | ||
222 | if (err) return rej(err) | 271 | if (err) return rej(err) |
223 | 272 | ||
224 | if ('streams' in data) { | 273 | if ('streams' in data) { |
225 | const audioStream = data['streams'].find(stream => stream['codec_type'] === 'audio') | 274 | const audioStream = data.streams.find(stream => stream['codec_type'] === 'audio') |
226 | if (audioStream) { | 275 | if (audioStream) { |
227 | return res({ | 276 | return res({ |
228 | absolutePath: data.format.filename, | 277 | absolutePath: data.format.filename, |
@@ -230,8 +279,15 @@ namespace audio { | |||
230 | }) | 279 | }) |
231 | } | 280 | } |
232 | } | 281 | } |
282 | |||
233 | return res({ absolutePath: data.format.filename }) | 283 | return res({ absolutePath: data.format.filename }) |
234 | }) | 284 | } |
285 | |||
286 | if (typeof option === 'string') { | ||
287 | return ffmpeg.ffprobe(option, parseFfprobe) | ||
288 | } | ||
289 | |||
290 | return option.ffprobe(parseFfprobe) | ||
235 | }) | 291 | }) |
236 | } | 292 | } |
237 | 293 | ||
@@ -273,39 +329,48 @@ namespace audio { | |||
273 | * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel | 329 | * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel |
274 | * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr | 330 | * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr |
275 | */ | 331 | */ |
276 | async function standard (_ffmpeg) { | 332 | async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise<ffmpeg.FfmpegCommand> { |
277 | let localFfmpeg = _ffmpeg | 333 | let localCommand = command |
278 | .format('mp4') | 334 | .format('mp4') |
279 | .videoCodec('libx264') | 335 | .videoCodec('libx264') |
280 | .outputOption('-level 3.1') // 3.1 is the minimal ressource allocation for our highest supported resolution | 336 | .outputOption('-level 3.1') // 3.1 is the minimal ressource allocation for our highest supported resolution |
281 | .outputOption('-b_strategy 1') // NOTE: b-strategy 1 - heuristic algorythm, 16 is optimal B-frames for it | 337 | .outputOption('-b_strategy 1') // NOTE: b-strategy 1 - heuristic algorythm, 16 is optimal B-frames for it |
282 | .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16 | 338 | .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16 |
339 | .outputOption('-pix_fmt yuv420p') // allows import of source material with incompatible pixel formats (e.g. MJPEG video) | ||
283 | .outputOption('-map_metadata -1') // strip all metadata | 340 | .outputOption('-map_metadata -1') // strip all metadata |
284 | .outputOption('-movflags faststart') | 341 | .outputOption('-movflags faststart') |
285 | const _audio = await audio.get(localFfmpeg) | ||
286 | 342 | ||
287 | if (!_audio.audioStream) { | 343 | const parsedAudio = await audio.get(localCommand) |
288 | return localFfmpeg.noAudio() | ||
289 | } | ||
290 | 344 | ||
291 | // we favor VBR, if a good AAC encoder is available | 345 | if (!parsedAudio.audioStream) { |
292 | if ((await checkFFmpegEncoders()).get('libfdk_aac')) { | 346 | localCommand = localCommand.noAudio() |
293 | return localFfmpeg | 347 | } else if ((await checkFFmpegEncoders()).get('libfdk_aac')) { // we favor VBR, if a good AAC encoder is available |
348 | localCommand = localCommand | ||
294 | .audioCodec('libfdk_aac') | 349 | .audioCodec('libfdk_aac') |
295 | .audioQuality(5) | 350 | .audioQuality(5) |
351 | } else { | ||
352 | // we try to reduce the ceiling bitrate by making rough correspondances of bitrates | ||
353 | // of course this is far from perfect, but it might save some space in the end | ||
354 | const audioCodecName = parsedAudio.audioStream[ 'codec_name' ] | ||
355 | let bitrate: number | ||
356 | if (audio.bitrate[ audioCodecName ]) { | ||
357 | localCommand = localCommand.audioCodec('aac') | ||
358 | |||
359 | bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ]) | ||
360 | if (bitrate !== undefined && bitrate !== -1) localCommand = localCommand.audioBitrate(bitrate) | ||
361 | } | ||
296 | } | 362 | } |
297 | 363 | ||
298 | // we try to reduce the ceiling bitrate by making rough correspondances of bitrates | 364 | // Constrained Encoding (VBV) |
299 | // of course this is far from perfect, but it might save some space in the end | 365 | // https://slhck.info/video/2017/03/01/rate-control.html |
300 | const audioCodecName = _audio.audioStream['codec_name'] | 366 | // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate |
301 | let bitrate: number | 367 | const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS) |
302 | if (audio.bitrate[audioCodecName]) { | 368 | localCommand = localCommand.outputOptions([`-maxrate ${ targetBitrate }`, `-bufsize ${ targetBitrate * 2 }`]) |
303 | bitrate = audio.bitrate[audioCodecName](_audio.audioStream['bit_rate']) | ||
304 | |||
305 | if (bitrate === -1) return localFfmpeg.audioCodec('copy') | ||
306 | } | ||
307 | 369 | ||
308 | if (bitrate !== undefined) return localFfmpeg.audioBitrate(bitrate) | 370 | // Keyframe interval of 2 seconds for faster seeking and resolution switching. |
371 | // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html | ||
372 | // https://superuser.com/a/908325 | ||
373 | localCommand = localCommand.outputOption(`-g ${ fps * 2 }`) | ||
309 | 374 | ||
310 | return localFfmpeg | 375 | return localCommand |
311 | } | 376 | } |
diff --git a/server/helpers/image-utils.ts b/server/helpers/image-utils.ts index 3eaa674ed..e43ea3f1d 100644 --- a/server/helpers/image-utils.ts +++ b/server/helpers/image-utils.ts | |||
@@ -1,13 +1,26 @@ | |||
1 | import 'multer' | 1 | import 'multer' |
2 | import * as sharp from 'sharp' | 2 | import * as sharp from 'sharp' |
3 | import { remove } from 'fs-extra' | 3 | import { readFile, remove } from 'fs-extra' |
4 | import { logger } from './logger' | ||
4 | 5 | ||
5 | async function processImage ( | 6 | async function processImage ( |
6 | physicalFile: { path: string }, | 7 | physicalFile: { path: string }, |
7 | destination: string, | 8 | destination: string, |
8 | newSize: { width: number, height: number } | 9 | newSize: { width: number, height: number } |
9 | ) { | 10 | ) { |
10 | await sharp(physicalFile.path) | 11 | if (physicalFile.path === destination) { |
12 | throw new Error('Sharp needs an input path different that the output path.') | ||
13 | } | ||
14 | |||
15 | logger.debug('Processing image %s to %s.', physicalFile.path, destination) | ||
16 | |||
17 | // Avoid sharp cache | ||
18 | const buf = await readFile(physicalFile.path) | ||
19 | const sharpInstance = sharp(buf) | ||
20 | |||
21 | await remove(destination) | ||
22 | |||
23 | await sharpInstance | ||
11 | .resize(newSize.width, newSize.height) | 24 | .resize(newSize.width, newSize.height) |
12 | .toFile(destination) | 25 | .toFile(destination) |
13 | 26 | ||
diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts index 5c182961d..ab9ec077e 100644 --- a/server/helpers/peertube-crypto.ts +++ b/server/helpers/peertube-crypto.ts | |||
@@ -1,8 +1,14 @@ | |||
1 | import { BCRYPT_SALT_SIZE, PRIVATE_RSA_KEY_SIZE } from '../initializers' | 1 | import { Request } from 'express' |
2 | import { BCRYPT_SALT_SIZE, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers' | ||
2 | import { ActorModel } from '../models/activitypub/actor' | 3 | import { ActorModel } from '../models/activitypub/actor' |
3 | import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey } from './core-utils' | 4 | import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey, sha256 } from './core-utils' |
4 | import { jsig } from './custom-jsonld-signature' | 5 | import { jsig, jsonld } from './custom-jsonld-signature' |
5 | import { logger } from './logger' | 6 | import { logger } from './logger' |
7 | import { cloneDeep } from 'lodash' | ||
8 | import { createVerify } from 'crypto' | ||
9 | import { buildDigest } from '../lib/job-queue/handlers/utils/activitypub-http-utils' | ||
10 | |||
11 | const httpSignature = require('http-signature') | ||
6 | 12 | ||
7 | async function createPrivateAndPublicKeys () { | 13 | async function createPrivateAndPublicKeys () { |
8 | logger.info('Generating a RSA key...') | 14 | logger.info('Generating a RSA key...') |
@@ -13,18 +19,57 @@ async function createPrivateAndPublicKeys () { | |||
13 | return { privateKey: key, publicKey } | 19 | return { privateKey: key, publicKey } |
14 | } | 20 | } |
15 | 21 | ||
16 | function isSignatureVerified (fromActor: ActorModel, signedDocument: object) { | 22 | // User password checks |
23 | |||
24 | function comparePassword (plainPassword: string, hashPassword: string) { | ||
25 | return bcryptComparePromise(plainPassword, hashPassword) | ||
26 | } | ||
27 | |||
28 | async function cryptPassword (password: string) { | ||
29 | const salt = await bcryptGenSaltPromise(BCRYPT_SALT_SIZE) | ||
30 | |||
31 | return bcryptHashPromise(password, salt) | ||
32 | } | ||
33 | |||
34 | // HTTP Signature | ||
35 | |||
36 | function isHTTPSignatureDigestValid (rawBody: Buffer, req: Request): boolean { | ||
37 | if (req.headers[HTTP_SIGNATURE.HEADER_NAME] && req.headers['digest']) { | ||
38 | return buildDigest(rawBody.toString()) === req.headers['digest'] | ||
39 | } | ||
40 | |||
41 | return true | ||
42 | } | ||
43 | |||
44 | function isHTTPSignatureVerified (httpSignatureParsed: any, actor: ActorModel): boolean { | ||
45 | return httpSignature.verifySignature(httpSignatureParsed, actor.publicKey) === true | ||
46 | } | ||
47 | |||
48 | function parseHTTPSignature (req: Request, clockSkew?: number) { | ||
49 | return httpSignature.parse(req, { authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, clockSkew }) | ||
50 | } | ||
51 | |||
52 | // JSONLD | ||
53 | |||
54 | async function isJsonLDSignatureVerified (fromActor: ActorModel, signedDocument: any): Promise<boolean> { | ||
55 | if (signedDocument.signature.type === 'RsaSignature2017') { | ||
56 | // Mastodon algorithm | ||
57 | const res = await isJsonLDRSA2017Verified(fromActor, signedDocument) | ||
58 | // Success? If no, try with our library | ||
59 | if (res === true) return true | ||
60 | } | ||
61 | |||
17 | const publicKeyObject = { | 62 | const publicKeyObject = { |
18 | '@context': jsig.SECURITY_CONTEXT_URL, | 63 | '@context': jsig.SECURITY_CONTEXT_URL, |
19 | '@id': fromActor.url, | 64 | id: fromActor.url, |
20 | '@type': 'CryptographicKey', | 65 | type: 'CryptographicKey', |
21 | owner: fromActor.url, | 66 | owner: fromActor.url, |
22 | publicKeyPem: fromActor.publicKey | 67 | publicKeyPem: fromActor.publicKey |
23 | } | 68 | } |
24 | 69 | ||
25 | const publicKeyOwnerObject = { | 70 | const publicKeyOwnerObject = { |
26 | '@context': jsig.SECURITY_CONTEXT_URL, | 71 | '@context': jsig.SECURITY_CONTEXT_URL, |
27 | '@id': fromActor.url, | 72 | id: fromActor.url, |
28 | publicKey: [ publicKeyObject ] | 73 | publicKey: [ publicKeyObject ] |
29 | } | 74 | } |
30 | 75 | ||
@@ -33,14 +78,54 @@ function isSignatureVerified (fromActor: ActorModel, signedDocument: object) { | |||
33 | publicKeyOwner: publicKeyOwnerObject | 78 | publicKeyOwner: publicKeyOwnerObject |
34 | } | 79 | } |
35 | 80 | ||
36 | return jsig.promises.verify(signedDocument, options) | 81 | return jsig.promises |
37 | .catch(err => { | 82 | .verify(signedDocument, options) |
38 | logger.error('Cannot check signature.', { err }) | 83 | .then((result: { verified: boolean }) => result.verified) |
39 | return false | 84 | .catch(err => { |
40 | }) | 85 | logger.error('Cannot check signature.', { err }) |
86 | return false | ||
87 | }) | ||
88 | } | ||
89 | |||
90 | // Backward compatibility with "other" implementations | ||
91 | async function isJsonLDRSA2017Verified (fromActor: ActorModel, signedDocument: any) { | ||
92 | function hash (obj: any): Promise<any> { | ||
93 | return jsonld.promises | ||
94 | .normalize(obj, { | ||
95 | algorithm: 'URDNA2015', | ||
96 | format: 'application/n-quads' | ||
97 | }) | ||
98 | .then(res => sha256(res)) | ||
99 | } | ||
100 | |||
101 | const signatureCopy = cloneDeep(signedDocument.signature) | ||
102 | Object.assign(signatureCopy, { | ||
103 | '@context': [ | ||
104 | 'https://w3id.org/security/v1', | ||
105 | { RsaSignature2017: 'https://w3id.org/security#RsaSignature2017' } | ||
106 | ] | ||
107 | }) | ||
108 | delete signatureCopy.type | ||
109 | delete signatureCopy.id | ||
110 | delete signatureCopy.signatureValue | ||
111 | |||
112 | const docWithoutSignature = cloneDeep(signedDocument) | ||
113 | delete docWithoutSignature.signature | ||
114 | |||
115 | const [ documentHash, optionsHash ] = await Promise.all([ | ||
116 | hash(docWithoutSignature), | ||
117 | hash(signatureCopy) | ||
118 | ]) | ||
119 | |||
120 | const toVerify = optionsHash + documentHash | ||
121 | |||
122 | const verify = createVerify('RSA-SHA256') | ||
123 | verify.update(toVerify, 'utf8') | ||
124 | |||
125 | return verify.verify(fromActor.publicKey, signedDocument.signature.signatureValue, 'base64') | ||
41 | } | 126 | } |
42 | 127 | ||
43 | function signObject (byActor: ActorModel, data: any) { | 128 | function signJsonLDObject (byActor: ActorModel, data: any) { |
44 | const options = { | 129 | const options = { |
45 | privateKeyPem: byActor.privateKey, | 130 | privateKeyPem: byActor.privateKey, |
46 | creator: byActor.url, | 131 | creator: byActor.url, |
@@ -50,22 +135,15 @@ function signObject (byActor: ActorModel, data: any) { | |||
50 | return jsig.promises.sign(data, options) | 135 | return jsig.promises.sign(data, options) |
51 | } | 136 | } |
52 | 137 | ||
53 | function comparePassword (plainPassword: string, hashPassword: string) { | ||
54 | return bcryptComparePromise(plainPassword, hashPassword) | ||
55 | } | ||
56 | |||
57 | async function cryptPassword (password: string) { | ||
58 | const salt = await bcryptGenSaltPromise(BCRYPT_SALT_SIZE) | ||
59 | |||
60 | return bcryptHashPromise(password, salt) | ||
61 | } | ||
62 | |||
63 | // --------------------------------------------------------------------------- | 138 | // --------------------------------------------------------------------------- |
64 | 139 | ||
65 | export { | 140 | export { |
66 | isSignatureVerified, | 141 | isHTTPSignatureDigestValid, |
142 | parseHTTPSignature, | ||
143 | isHTTPSignatureVerified, | ||
144 | isJsonLDSignatureVerified, | ||
67 | comparePassword, | 145 | comparePassword, |
68 | createPrivateAndPublicKeys, | 146 | createPrivateAndPublicKeys, |
69 | cryptPassword, | 147 | cryptPassword, |
70 | signObject | 148 | signJsonLDObject |
71 | } | 149 | } |
diff --git a/server/helpers/regexp.ts b/server/helpers/regexp.ts new file mode 100644 index 000000000..2336654b0 --- /dev/null +++ b/server/helpers/regexp.ts | |||
@@ -0,0 +1,23 @@ | |||
1 | // Thanks to https://regex101.com | ||
2 | function regexpCapture (str: string, regex: RegExp, maxIterations = 100) { | ||
3 | let m: RegExpExecArray | ||
4 | let i = 0 | ||
5 | let result: RegExpExecArray[] = [] | ||
6 | |||
7 | // tslint:disable:no-conditional-assignment | ||
8 | while ((m = regex.exec(str)) !== null && i < maxIterations) { | ||
9 | // This is necessary to avoid infinite loops with zero-width matches | ||
10 | if (m.index === regex.lastIndex) { | ||
11 | regex.lastIndex++ | ||
12 | } | ||
13 | |||
14 | result.push(m) | ||
15 | i++ | ||
16 | } | ||
17 | |||
18 | return result | ||
19 | } | ||
20 | |||
21 | export { | ||
22 | regexpCapture | ||
23 | } | ||
diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts index ee9e80404..5c6dc5e19 100644 --- a/server/helpers/requests.ts +++ b/server/helpers/requests.ts | |||
@@ -1,17 +1,19 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { createWriteStream } from 'fs-extra' | 2 | import { createWriteStream } from 'fs-extra' |
3 | import * as request from 'request' | 3 | import * as request from 'request' |
4 | import { ACTIVITY_PUB } from '../initializers' | 4 | import { ACTIVITY_PUB, CONFIG } from '../initializers' |
5 | import { processImage } from './image-utils' | ||
6 | import { join } from 'path' | ||
5 | 7 | ||
6 | function doRequest ( | 8 | function doRequest <T> ( |
7 | requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean } | 9 | requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean } |
8 | ): Bluebird<{ response: request.RequestResponse, body: any }> { | 10 | ): Bluebird<{ response: request.RequestResponse, body: T }> { |
9 | if (requestOptions.activityPub === true) { | 11 | if (requestOptions.activityPub === true) { |
10 | if (!Array.isArray(requestOptions.headers)) requestOptions.headers = {} | 12 | if (!Array.isArray(requestOptions.headers)) requestOptions.headers = {} |
11 | requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER | 13 | requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER |
12 | } | 14 | } |
13 | 15 | ||
14 | return new Bluebird<{ response: request.RequestResponse, body: any }>((res, rej) => { | 16 | return new Bluebird<{ response: request.RequestResponse, body: T }>((res, rej) => { |
15 | request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body })) | 17 | request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body })) |
16 | }) | 18 | }) |
17 | } | 19 | } |
@@ -27,9 +29,18 @@ function doRequestAndSaveToFile (requestOptions: request.CoreOptions & request.U | |||
27 | }) | 29 | }) |
28 | } | 30 | } |
29 | 31 | ||
32 | async function downloadImage (url: string, destDir: string, destName: string, size: { width: number, height: number }) { | ||
33 | const tmpPath = join(CONFIG.STORAGE.TMP_DIR, 'pending-' + destName) | ||
34 | await doRequestAndSaveToFile({ method: 'GET', uri: url }, tmpPath) | ||
35 | |||
36 | const destPath = join(destDir, destName) | ||
37 | await processImage({ path: tmpPath }, destPath, size) | ||
38 | } | ||
39 | |||
30 | // --------------------------------------------------------------------------- | 40 | // --------------------------------------------------------------------------- |
31 | 41 | ||
32 | export { | 42 | export { |
33 | doRequest, | 43 | doRequest, |
34 | doRequestAndSaveToFile | 44 | doRequestAndSaveToFile, |
45 | downloadImage | ||
35 | } | 46 | } |
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts index 6228fec04..cb0e823c5 100644 --- a/server/helpers/utils.ts +++ b/server/helpers/utils.ts | |||
@@ -19,10 +19,7 @@ async function generateRandomString (size: number) { | |||
19 | return raw.toString('hex') | 19 | return raw.toString('hex') |
20 | } | 20 | } |
21 | 21 | ||
22 | interface FormattableToJSON { | 22 | interface FormattableToJSON { toFormattedJSON (args?: any) } |
23 | toFormattedJSON (args?: any) | ||
24 | } | ||
25 | |||
26 | function getFormattedObjects<U, T extends FormattableToJSON> (objects: T[], objectsTotal: number, formattedArg?: any) { | 23 | function getFormattedObjects<U, T extends FormattableToJSON> (objects: T[], objectsTotal: number, formattedArg?: any) { |
27 | const formattedObjects: U[] = [] | 24 | const formattedObjects: U[] = [] |
28 | 25 | ||
@@ -40,21 +37,24 @@ const getServerActor = memoizee(async function () { | |||
40 | const application = await ApplicationModel.load() | 37 | const application = await ApplicationModel.load() |
41 | if (!application) throw Error('Could not load Application from database.') | 38 | if (!application) throw Error('Could not load Application from database.') |
42 | 39 | ||
43 | return application.Account.Actor | 40 | const actor = application.Account.Actor |
41 | actor.Account = application.Account | ||
42 | |||
43 | return actor | ||
44 | }) | 44 | }) |
45 | 45 | ||
46 | function generateVideoTmpPath (target: string | ParseTorrent) { | 46 | function generateVideoImportTmpPath (target: string | ParseTorrent) { |
47 | const id = typeof target === 'string' ? target : target.infoHash | 47 | const id = typeof target === 'string' ? target : target.infoHash |
48 | 48 | ||
49 | const hash = sha256(id) | 49 | const hash = sha256(id) |
50 | return join(CONFIG.STORAGE.VIDEOS_DIR, hash + '-import.mp4') | 50 | return join(CONFIG.STORAGE.TMP_DIR, hash + '-import.mp4') |
51 | } | 51 | } |
52 | 52 | ||
53 | function getSecureTorrentName (originalName: string) { | 53 | function getSecureTorrentName (originalName: string) { |
54 | return sha256(originalName) + '.torrent' | 54 | return sha256(originalName) + '.torrent' |
55 | } | 55 | } |
56 | 56 | ||
57 | async function getVersion () { | 57 | async function getServerCommit () { |
58 | try { | 58 | try { |
59 | const tag = await execPromise2( | 59 | const tag = await execPromise2( |
60 | '[ ! -d .git ] || git name-rev --name-only --tags --no-undefined HEAD 2>/dev/null || true', | 60 | '[ ! -d .git ] || git name-rev --name-only --tags --no-undefined HEAD 2>/dev/null || true', |
@@ -74,7 +74,21 @@ async function getVersion () { | |||
74 | logger.debug('Cannot get version from git HEAD.', { err }) | 74 | logger.debug('Cannot get version from git HEAD.', { err }) |
75 | } | 75 | } |
76 | 76 | ||
77 | return require('../../../package.json').version | 77 | return '' |
78 | } | ||
79 | |||
80 | /** | ||
81 | * From a filename like "ede4cba5-742b-46fa-a388-9a6eb3a3aeb3.mp4", returns | ||
82 | * only the "ede4cba5-742b-46fa-a388-9a6eb3a3aeb3" part. If the filename does | ||
83 | * not contain a UUID, returns null. | ||
84 | */ | ||
85 | function getUUIDFromFilename (filename: string) { | ||
86 | const regex = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/ | ||
87 | const result = filename.match(regex) | ||
88 | |||
89 | if (!result || Array.isArray(result) === false) return null | ||
90 | |||
91 | return result[0] | ||
78 | } | 92 | } |
79 | 93 | ||
80 | // --------------------------------------------------------------------------- | 94 | // --------------------------------------------------------------------------- |
@@ -85,6 +99,7 @@ export { | |||
85 | getFormattedObjects, | 99 | getFormattedObjects, |
86 | getSecureTorrentName, | 100 | getSecureTorrentName, |
87 | getServerActor, | 101 | getServerActor, |
88 | getVersion, | 102 | getServerCommit, |
89 | generateVideoTmpPath | 103 | generateVideoImportTmpPath, |
104 | getUUIDFromFilename | ||
90 | } | 105 | } |
diff --git a/server/helpers/video.ts b/server/helpers/video.ts index 1bd21467d..c90fe06c7 100644 --- a/server/helpers/video.ts +++ b/server/helpers/video.ts | |||
@@ -1,10 +1,12 @@ | |||
1 | import { VideoModel } from '../models/video/video' | 1 | import { VideoModel } from '../models/video/video' |
2 | 2 | ||
3 | type VideoFetchType = 'all' | 'only-video' | 'id' | 'none' | 3 | type VideoFetchType = 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none' |
4 | 4 | ||
5 | function fetchVideo (id: number | string, fetchType: VideoFetchType, userId?: number) { | 5 | function fetchVideo (id: number | string, fetchType: VideoFetchType, userId?: number) { |
6 | if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId) | 6 | if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId) |
7 | 7 | ||
8 | if (fetchType === 'only-video-with-rights') return VideoModel.loadWithRights(id) | ||
9 | |||
8 | if (fetchType === 'only-video') return VideoModel.load(id) | 10 | if (fetchType === 'only-video') return VideoModel.load(id) |
9 | 11 | ||
10 | if (fetchType === 'id' || fetchType === 'none') return VideoModel.loadOnlyId(id) | 12 | if (fetchType === 'id' || fetchType === 'none') return VideoModel.loadOnlyId(id) |
diff --git a/server/helpers/webtorrent.ts b/server/helpers/webtorrent.ts index ce35b87da..3c9a0b96a 100644 --- a/server/helpers/webtorrent.ts +++ b/server/helpers/webtorrent.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { logger } from './logger' | 1 | import { logger } from './logger' |
2 | import { generateVideoTmpPath } from './utils' | 2 | import { generateVideoImportTmpPath } from './utils' |
3 | import * as WebTorrent from 'webtorrent' | 3 | import * as WebTorrent from 'webtorrent' |
4 | import { createWriteStream, ensureDir, remove } from 'fs-extra' | 4 | import { createWriteStream, ensureDir, remove } from 'fs-extra' |
5 | import { CONFIG } from '../initializers' | 5 | import { CONFIG } from '../initializers' |
@@ -9,10 +9,10 @@ async function downloadWebTorrentVideo (target: { magnetUri: string, torrentName | |||
9 | const id = target.magnetUri || target.torrentName | 9 | const id = target.magnetUri || target.torrentName |
10 | let timer | 10 | let timer |
11 | 11 | ||
12 | const path = generateVideoTmpPath(id) | 12 | const path = generateVideoImportTmpPath(id) |
13 | logger.info('Importing torrent video %s', id) | 13 | logger.info('Importing torrent video %s', id) |
14 | 14 | ||
15 | const directoryPath = join(CONFIG.STORAGE.VIDEOS_DIR, 'import') | 15 | const directoryPath = join(CONFIG.STORAGE.TMP_DIR, 'webtorrent') |
16 | await ensureDir(directoryPath) | 16 | await ensureDir(directoryPath) |
17 | 17 | ||
18 | return new Promise<string>((res, rej) => { | 18 | return new Promise<string>((res, rej) => { |
diff --git a/server/helpers/youtube-dl.ts b/server/helpers/youtube-dl.ts index 70b4e1b78..b74351b42 100644 --- a/server/helpers/youtube-dl.ts +++ b/server/helpers/youtube-dl.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { truncate } from 'lodash' | 1 | import { truncate } from 'lodash' |
2 | import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES } from '../initializers' | 2 | import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES } from '../initializers' |
3 | import { logger } from './logger' | 3 | import { logger } from './logger' |
4 | import { generateVideoTmpPath } from './utils' | 4 | import { generateVideoImportTmpPath } from './utils' |
5 | import { join } from 'path' | 5 | import { join } from 'path' |
6 | import { root } from './core-utils' | 6 | import { root } from './core-utils' |
7 | import { ensureDir, writeFile, remove } from 'fs-extra' | 7 | import { ensureDir, writeFile, remove } from 'fs-extra' |
@@ -24,10 +24,10 @@ const processOptions = { | |||
24 | 24 | ||
25 | function getYoutubeDLInfo (url: string, opts?: string[]): Promise<YoutubeDLInfo> { | 25 | function getYoutubeDLInfo (url: string, opts?: string[]): Promise<YoutubeDLInfo> { |
26 | return new Promise<YoutubeDLInfo>(async (res, rej) => { | 26 | return new Promise<YoutubeDLInfo>(async (res, rej) => { |
27 | const options = opts || [ '-j', '--flat-playlist' ] | 27 | const args = opts || [ '-j', '--flat-playlist' ] |
28 | 28 | ||
29 | const youtubeDL = await safeGetYoutubeDL() | 29 | const youtubeDL = await safeGetYoutubeDL() |
30 | youtubeDL.getInfo(url, options, (err, info) => { | 30 | youtubeDL.getInfo(url, args, processOptions, (err, info) => { |
31 | if (err) return rej(err) | 31 | if (err) return rej(err) |
32 | if (info.is_live === true) return rej(new Error('Cannot download a live streaming.')) | 32 | if (info.is_live === true) return rej(new Error('Cannot download a live streaming.')) |
33 | 33 | ||
@@ -40,7 +40,7 @@ function getYoutubeDLInfo (url: string, opts?: string[]): Promise<YoutubeDLInfo> | |||
40 | } | 40 | } |
41 | 41 | ||
42 | function downloadYoutubeDLVideo (url: string, timeout: number) { | 42 | function downloadYoutubeDLVideo (url: string, timeout: number) { |
43 | const path = generateVideoTmpPath(url) | 43 | const path = generateVideoImportTmpPath(url) |
44 | let timer | 44 | let timer |
45 | 45 | ||
46 | logger.info('Importing youtubeDL video %s', url) | 46 | logger.info('Importing youtubeDL video %s', url) |