Merge branch 'develop' into pr/1217

This commit is contained in:
Chocobozzz
2019-02-11 11:52:34 +01:00
888 changed files with 54348 additions and 33154 deletions

View File

@@ -1,11 +1,12 @@
import * as Bluebird from 'bluebird'
import * as validator from 'validator'
import { ResultList } from '../../shared/models'
import { Activity, ActivityPubActor } from '../../shared/models/activitypub'
import { Activity } from '../../shared/models/activitypub'
import { ACTIVITY_PUB } from '../initializers'
import { ActorModel } from '../models/activitypub/actor'
import { signObject } from './peertube-crypto'
import { signJsonLDObject } from './peertube-crypto'
import { pageToStartAndCount } from './core-utils'
import { parse } from 'url'
function activityPubContextify <T> (data: T) {
return Object.assign(data, {
@@ -14,25 +15,26 @@ function activityPubContextify <T> (data: T) {
'https://w3id.org/security/v1',
{
RsaSignature2017: 'https://w3id.org/security#RsaSignature2017',
pt: 'https://joinpeertube.org/ns',
schema: 'http://schema.org#',
pt: 'https://joinpeertube.org/ns#',
sc: 'http://schema.org#',
Hashtag: 'as:Hashtag',
uuid: 'schema:identifier',
category: 'schema:category',
licence: 'schema:license',
subtitleLanguage: 'schema:subtitleLanguage',
uuid: 'sc:identifier',
category: 'sc:category',
licence: 'sc:license',
subtitleLanguage: 'sc:subtitleLanguage',
sensitive: 'as:sensitive',
language: 'schema:inLanguage',
views: 'schema:Number',
stats: 'schema:Number',
size: 'schema:Number',
fps: 'schema:Number',
commentsEnabled: 'schema:Boolean',
downloadEnabled: 'schema:Boolean',
waitTranscoding: 'schema:Boolean',
expires: 'schema:expires',
support: 'schema:Text',
CacheFile: 'pt:CacheFile'
language: 'sc:inLanguage',
views: 'sc:Number',
state: 'sc:Number',
size: 'sc:Number',
fps: 'sc:Number',
commentsEnabled: 'sc:Boolean',
downloadEnabled: 'sc:Boolean',
waitTranscoding: 'sc:Boolean',
expires: 'sc:expires',
support: 'sc:Text',
CacheFile: 'pt:CacheFile',
Infohash: 'pt:Infohash'
},
{
likes: {
@@ -57,16 +59,16 @@ function activityPubContextify <T> (data: T) {
}
type ActivityPubCollectionPaginationHandler = (start: number, count: number) => Bluebird<ResultList<any>> | Promise<ResultList<any>>
async function activityPubCollectionPagination (url: string, handler: ActivityPubCollectionPaginationHandler, page?: any) {
async function activityPubCollectionPagination (baseUrl: string, handler: ActivityPubCollectionPaginationHandler, page?: any) {
if (!page || !validator.isInt(page)) {
// We just display the first page URL, we only need the total items
const result = await handler(0, 1)
return {
id: url,
id: baseUrl,
type: 'OrderedCollection',
totalItems: result.total,
first: url + '?page=1'
first: baseUrl + '?page=1'
}
}
@@ -81,19 +83,19 @@ async function activityPubCollectionPagination (url: string, handler: ActivityPu
// There are more results
if (result.total > page * ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE) {
next = url + '?page=' + (page + 1)
next = baseUrl + '?page=' + (page + 1)
}
if (page > 1) {
prev = url + '?page=' + (page - 1)
prev = baseUrl + '?page=' + (page - 1)
}
return {
id: url + '?page=' + page,
id: baseUrl + '?page=' + page,
type: 'OrderedCollectionPage',
prev,
next,
partOf: url,
partOf: baseUrl,
orderedItems: result.data,
totalItems: result.total
}
@@ -103,19 +105,27 @@ async function activityPubCollectionPagination (url: string, handler: ActivityPu
function buildSignedActivity (byActor: ActorModel, data: Object) {
const activity = activityPubContextify(data)
return signObject(byActor, activity) as Promise<Activity>
return signJsonLDObject(byActor, activity) as Promise<Activity>
}
function getActorUrl (activityActor: string | ActivityPubActor) {
if (typeof activityActor === 'string') return activityActor
function getAPId (activity: string | { id: string }) {
if (typeof activity === 'string') return activity
return activityActor.id
return activity.id
}
function checkUrlsSameHost (url1: string, url2: string) {
const idHost = parse(url1).host
const actorHost = parse(url2).host
return idHost && actorHost && idHost.toLowerCase() === actorHost.toLowerCase()
}
// ---------------------------------------------------------------------------
export {
getActorUrl,
checkUrlsSameHost,
getAPId,
activityPubContextify,
activityPubCollectionPagination,
buildSignedActivity

View File

@@ -2,7 +2,7 @@ import { join } from 'path'
import { CONFIG } from '../initializers'
import { VideoCaptionModel } from '../models/video/video-caption'
import * as srt2vtt from 'srt-to-vtt'
import { createReadStream, createWriteStream, remove, rename } from 'fs-extra'
import { createReadStream, createWriteStream, remove, move } from 'fs-extra'
async function moveAndProcessCaptionFile (physicalFile: { filename: string, path: string }, videoCaption: VideoCaptionModel) {
const videoCaptionsDir = CONFIG.STORAGE.CAPTIONS_DIR
@@ -13,7 +13,7 @@ async function moveAndProcessCaptionFile (physicalFile: { filename: string, path
await convertSrtToVtt(physicalFile.path, destination)
await remove(physicalFile.path)
} else { // Just move the vtt file
await rename(physicalFile.path, destination)
await move(physicalFile.path, destination, { overwrite: true })
}
// This is important in case if there is another attempt in the retry process

View File

@@ -5,12 +5,31 @@
import * as bcrypt from 'bcrypt'
import * as createTorrent from 'create-torrent'
import { createHash, pseudoRandomBytes } from 'crypto'
import { createHash, HexBase64Latin1Encoding, pseudoRandomBytes } from 'crypto'
import { isAbsolute, join } from 'path'
import * as pem from 'pem'
import { URL } from 'url'
import { truncate } from 'lodash'
import { exec } from 'child_process'
import { isArray } from './custom-validators/misc'
const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => {
if (!oldObject || typeof oldObject !== 'object') {
return valueConverter(oldObject)
}
if (isArray(oldObject)) {
return oldObject.map(e => objectConverter(e, keyConverter, valueConverter))
}
const newObject = {}
Object.keys(oldObject).forEach(oldKey => {
const newKey = keyConverter(oldKey)
newObject[ newKey ] = objectConverter(oldObject[ oldKey ], keyConverter, valueConverter)
})
return newObject
}
const timeTable = {
ms: 1,
@@ -21,6 +40,7 @@ const timeTable = {
week: 3600000 * 24 * 7,
month: 3600000 * 24 * 30
}
export function parseDuration (duration: number | string): number {
if (typeof duration === 'number') return duration
@@ -41,6 +61,53 @@ export function parseDuration (duration: number | string): number {
throw new Error('Duration could not be properly parsed')
}
export function parseBytes (value: string | number): number {
if (typeof value === 'number') return value
const tgm = /^(\d+)\s*TB\s*(\d+)\s*GB\s*(\d+)\s*MB$/
const tg = /^(\d+)\s*TB\s*(\d+)\s*GB$/
const tm = /^(\d+)\s*TB\s*(\d+)\s*MB$/
const gm = /^(\d+)\s*GB\s*(\d+)\s*MB$/
const t = /^(\d+)\s*TB$/
const g = /^(\d+)\s*GB$/
const m = /^(\d+)\s*MB$/
const b = /^(\d+)\s*B$/
let match
if (value.match(tgm)) {
match = value.match(tgm)
return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
+ parseInt(match[2], 10) * 1024 * 1024 * 1024
+ parseInt(match[3], 10) * 1024 * 1024
} else if (value.match(tg)) {
match = value.match(tg)
return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
+ parseInt(match[2], 10) * 1024 * 1024 * 1024
} else if (value.match(tm)) {
match = value.match(tm)
return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
+ parseInt(match[2], 10) * 1024 * 1024
} else if (value.match(gm)) {
match = value.match(gm)
return parseInt(match[1], 10) * 1024 * 1024 * 1024
+ parseInt(match[2], 10) * 1024 * 1024
} else if (value.match(t)) {
match = value.match(t)
return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
} else if (value.match(g)) {
match = value.match(g)
return parseInt(match[1], 10) * 1024 * 1024 * 1024
} else if (value.match(m)) {
match = value.match(m)
return parseInt(match[1], 10) * 1024 * 1024
} else if (value.match(b)) {
match = value.match(b)
return parseInt(match[1], 10) * 1024
} else {
return parseInt(value, 10)
}
}
function sanitizeUrl (url: string) {
const urlObject = new URL(url)
@@ -126,8 +193,12 @@ function peertubeTruncate (str: string, maxLength: number) {
return truncate(str, options)
}
function sha256 (str: string) {
return createHash('sha256').update(str).digest('hex')
function sha256 (str: string | Buffer, encoding: HexBase64Latin1Encoding = 'hex') {
return createHash('sha256').update(str).digest(encoding)
}
function sha1 (str: string | Buffer, encoding: HexBase64Latin1Encoding = 'hex') {
return createHash('sha1').update(str).digest(encoding)
}
function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> {
@@ -187,6 +258,7 @@ export {
isTestInstance,
isProdInstance,
objectConverter,
root,
escapeHTML,
pageToStartAndCount,
@@ -194,7 +266,9 @@ export {
sanitizeHost,
buildPath,
peertubeTruncate,
sha256,
sha1,
promisify0,
promisify1,

View File

@@ -1,5 +1,5 @@
import * as AsyncLRU from 'async-lru'
import * as jsonld from 'jsonld/'
import * as jsonld from 'jsonld'
import * as jsig from 'jsonld-signatures'
const nodeDocumentLoader = jsonld.documentLoaders.node()
@@ -17,4 +17,4 @@ jsonld.documentLoader = (url, cb) => {
jsig.use('jsonld', jsonld)
export { jsig }
export { jsig, jsonld }

View File

@@ -1,26 +1,14 @@
import * as validator from 'validator'
import { Activity, ActivityType } from '../../../../shared/models/activitypub'
import {
isActorAcceptActivityValid,
isActorDeleteActivityValid,
isActorFollowActivityValid,
isActorRejectActivityValid,
isActorUpdateActivityValid
} from './actor'
import { isAnnounceActivityValid } from './announce'
import { isActivityPubUrlValid } from './misc'
import { isDislikeActivityValid, isLikeActivityValid } from './rate'
import { isUndoActivityValid } from './undo'
import { isVideoCommentCreateActivityValid, isVideoCommentDeleteActivityValid } from './video-comments'
import {
isVideoFlagValid,
isVideoTorrentDeleteActivityValid,
sanitizeAndCheckVideoTorrentCreateActivity,
sanitizeAndCheckVideoTorrentUpdateActivity
} from './videos'
import { sanitizeAndCheckActorObject } from './actor'
import { isActivityPubUrlValid, isBaseActivityValid, isObjectValid } from './misc'
import { isDislikeActivityValid } from './rate'
import { sanitizeAndCheckVideoCommentObject } from './video-comments'
import { sanitizeAndCheckVideoTorrentObject } from './videos'
import { isViewActivityValid } from './view'
import { exists } from '../misc'
import { isCacheFileCreateActivityValid, isCacheFileUpdateActivityValid } from './cache-file'
import { isCacheFileObjectValid } from './cache-file'
import { isFlagActivityValid } from './flag'
function isRootActivityValid (activity: any) {
return Array.isArray(activity['@context']) && (
@@ -46,7 +34,10 @@ const activityCheckers: { [ P in ActivityType ]: (activity: Activity) => boolean
Reject: checkRejectActivity,
Announce: checkAnnounceActivity,
Undo: checkUndoActivity,
Like: checkLikeActivity
Like: checkLikeActivity,
View: checkViewActivity,
Flag: checkFlagActivity,
Dislike: checkDislikeActivity
}
function isActivityValid (activity: any) {
@@ -66,47 +57,79 @@ export {
// ---------------------------------------------------------------------------
function checkViewActivity (activity: any) {
return isBaseActivityValid(activity, 'View') &&
isViewActivityValid(activity)
}
function checkFlagActivity (activity: any) {
return isBaseActivityValid(activity, 'Flag') &&
isFlagActivityValid(activity)
}
function checkDislikeActivity (activity: any) {
return isBaseActivityValid(activity, 'Dislike') &&
isDislikeActivityValid(activity)
}
function checkCreateActivity (activity: any) {
return isViewActivityValid(activity) ||
isDislikeActivityValid(activity) ||
sanitizeAndCheckVideoTorrentCreateActivity(activity) ||
isVideoFlagValid(activity) ||
isVideoCommentCreateActivityValid(activity) ||
isCacheFileCreateActivityValid(activity)
return isBaseActivityValid(activity, 'Create') &&
(
isViewActivityValid(activity.object) ||
isDislikeActivityValid(activity.object) ||
isFlagActivityValid(activity.object) ||
isCacheFileObjectValid(activity.object) ||
sanitizeAndCheckVideoCommentObject(activity.object) ||
sanitizeAndCheckVideoTorrentObject(activity.object)
)
}
function checkUpdateActivity (activity: any) {
return isCacheFileUpdateActivityValid(activity) ||
sanitizeAndCheckVideoTorrentUpdateActivity(activity) ||
isActorUpdateActivityValid(activity)
return isBaseActivityValid(activity, 'Update') &&
(
isCacheFileObjectValid(activity.object) ||
sanitizeAndCheckVideoTorrentObject(activity.object) ||
sanitizeAndCheckActorObject(activity.object)
)
}
function checkDeleteActivity (activity: any) {
return isVideoTorrentDeleteActivityValid(activity) ||
isActorDeleteActivityValid(activity) ||
isVideoCommentDeleteActivityValid(activity)
// We don't really check objects
return isBaseActivityValid(activity, 'Delete') &&
isObjectValid(activity.object)
}
function checkFollowActivity (activity: any) {
return isActorFollowActivityValid(activity)
return isBaseActivityValid(activity, 'Follow') &&
isObjectValid(activity.object)
}
function checkAcceptActivity (activity: any) {
return isActorAcceptActivityValid(activity)
return isBaseActivityValid(activity, 'Accept')
}
function checkRejectActivity (activity: any) {
return isActorRejectActivityValid(activity)
return isBaseActivityValid(activity, 'Reject')
}
function checkAnnounceActivity (activity: any) {
return isAnnounceActivityValid(activity)
return isBaseActivityValid(activity, 'Announce') &&
isObjectValid(activity.object)
}
function checkUndoActivity (activity: any) {
return isUndoActivityValid(activity)
return isBaseActivityValid(activity, 'Undo') &&
(
checkFollowActivity(activity.object) ||
checkLikeActivity(activity.object) ||
checkDislikeActivity(activity.object) ||
checkAnnounceActivity(activity.object) ||
checkCreateActivity(activity.object)
)
}
function checkLikeActivity (activity: any) {
return isLikeActivityValid(activity)
return isBaseActivityValid(activity, 'Like') &&
isObjectValid(activity.object)
}

View File

@@ -27,7 +27,8 @@ function isActorPublicKeyValid (publicKey: string) {
validator.isLength(publicKey, CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY)
}
const actorNameRegExp = new RegExp('^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\\-_\.]+$')
const actorNameAlphabet = '[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\\-_.]'
const actorNameRegExp = new RegExp(`^${actorNameAlphabet}+$`)
function isActorPreferredUsernameValid (preferredUsername: string) {
return exists(preferredUsername) && validator.matches(preferredUsername, actorNameRegExp)
}
@@ -72,24 +73,10 @@ function isActorDeleteActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Delete')
}
function isActorFollowActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Follow') &&
isActivityPubUrlValid(activity.object)
}
function sanitizeAndCheckActorObject (object: any) {
normalizeActor(object)
function isActorAcceptActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Accept')
}
function isActorRejectActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Reject')
}
function isActorUpdateActivityValid (activity: any) {
normalizeActor(activity.object)
return isBaseActivityValid(activity, 'Update') &&
isActorObjectValid(activity.object)
return isActorObjectValid(object)
}
function normalizeActor (actor: any) {
@@ -127,6 +114,7 @@ function areValidActorHandles (handles: string[]) {
export {
normalizeActor,
actorNameAlphabet,
areValidActorHandles,
isActorEndpointsObjectValid,
isActorPublicKeyObjectValid,
@@ -137,10 +125,7 @@ export {
isActorObjectValid,
isActorFollowingCountValid,
isActorFollowersCountValid,
isActorFollowActivityValid,
isActorAcceptActivityValid,
isActorRejectActivityValid,
isActorDeleteActivityValid,
isActorUpdateActivityValid,
sanitizeAndCheckActorObject,
isValidActorHandle
}

View File

@@ -1,13 +0,0 @@
import { isActivityPubUrlValid, isBaseActivityValid } from './misc'
function isAnnounceActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Announce') &&
(
isActivityPubUrlValid(activity.object) ||
(activity.object && isActivityPubUrlValid(activity.object.id))
)
}
export {
isAnnounceActivityValid
}

View File

@@ -1,28 +1,26 @@
import { isActivityPubUrlValid, isBaseActivityValid } from './misc'
import { isActivityPubUrlValid } from './misc'
import { isRemoteVideoUrlValid } from './videos'
import { isDateValid, exists } from '../misc'
import { exists, isDateValid } from '../misc'
import { CacheFileObject } from '../../../../shared/models/activitypub/objects'
function isCacheFileCreateActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Create') &&
isCacheFileObjectValid(activity.object)
}
function isCacheFileUpdateActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Update') &&
isCacheFileObjectValid(activity.object)
}
function isCacheFileObjectValid (object: CacheFileObject) {
return exists(object) &&
object.type === 'CacheFile' &&
isDateValid(object.expires) &&
isActivityPubUrlValid(object.object) &&
isRemoteVideoUrlValid(object.url)
(isRemoteVideoUrlValid(object.url) || isPlaylistRedundancyUrlValid(object.url))
}
// ---------------------------------------------------------------------------
export {
isCacheFileUpdateActivityValid,
isCacheFileCreateActivityValid,
isCacheFileObjectValid
}
// ---------------------------------------------------------------------------
function isPlaylistRedundancyUrlValid (url: any) {
return url.type === 'Link' &&
(url.mediaType || url.mimeType) === 'application/x-mpegURL' &&
isActivityPubUrlValid(url.href)
}

View File

@@ -0,0 +1,14 @@
import { isActivityPubUrlValid } from './misc'
import { isVideoAbuseReasonValid } from '../video-abuses'
function isFlagActivityValid (activity: any) {
return activity.type === 'Flag' &&
isVideoAbuseReasonValid(activity.content) &&
isActivityPubUrlValid(activity.object)
}
// ---------------------------------------------------------------------------
export {
isFlagActivityValid
}

View File

@@ -28,15 +28,20 @@ function isBaseActivityValid (activity: any, type: string) {
return (activity['@context'] === undefined || Array.isArray(activity['@context'])) &&
activity.type === type &&
isActivityPubUrlValid(activity.id) &&
exists(activity.actor) &&
(isActivityPubUrlValid(activity.actor) || isActivityPubUrlValid(activity.actor.id)) &&
isObjectValid(activity.actor) &&
isUrlCollectionValid(activity.to) &&
isUrlCollectionValid(activity.cc)
}
function isUrlCollectionValid (collection: any) {
return collection === undefined ||
(Array.isArray(collection) && collection.every(t => isActivityPubUrlValid(t)))
}
function isObjectValid (object: any) {
return exists(object) &&
(
activity.to === undefined ||
(Array.isArray(activity.to) && activity.to.every(t => isActivityPubUrlValid(t)))
) &&
(
activity.cc === undefined ||
(Array.isArray(activity.cc) && activity.cc.every(t => isActivityPubUrlValid(t)))
isActivityPubUrlValid(object) || isActivityPubUrlValid(object.id)
)
}
@@ -57,5 +62,6 @@ export {
isUrlValid,
isActivityPubUrlValid,
isBaseActivityValid,
setValidAttributedTo
setValidAttributedTo,
isObjectValid
}

View File

@@ -1,20 +1,13 @@
import { isActivityPubUrlValid, isBaseActivityValid } from './misc'
function isLikeActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Like') &&
isActivityPubUrlValid(activity.object)
}
import { isActivityPubUrlValid, isObjectValid } from './misc'
function isDislikeActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Create') &&
activity.object.type === 'Dislike' &&
isActivityPubUrlValid(activity.object.actor) &&
isActivityPubUrlValid(activity.object.object)
return activity.type === 'Dislike' &&
isActivityPubUrlValid(activity.actor) &&
isObjectValid(activity.object)
}
// ---------------------------------------------------------------------------
export {
isLikeActivityValid,
isDislikeActivityValid
}

View File

@@ -1,20 +0,0 @@
import { isActorFollowActivityValid } from './actor'
import { isBaseActivityValid } from './misc'
import { isDislikeActivityValid, isLikeActivityValid } from './rate'
import { isAnnounceActivityValid } from './announce'
import { isCacheFileCreateActivityValid } from './cache-file'
function isUndoActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Undo') &&
(
isActorFollowActivityValid(activity.object) ||
isLikeActivityValid(activity.object) ||
isDislikeActivityValid(activity.object) ||
isAnnounceActivityValid(activity.object) ||
isCacheFileCreateActivityValid(activity.object)
)
}
export {
isUndoActivityValid
}

View File

@@ -3,11 +3,6 @@ import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers'
import { exists, isArray, isDateValid } from '../misc'
import { isActivityPubUrlValid, isBaseActivityValid } from './misc'
function isVideoCommentCreateActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Create') &&
sanitizeAndCheckVideoCommentObject(activity.object)
}
function sanitizeAndCheckVideoCommentObject (comment: any) {
if (!comment || comment.type !== 'Note') return false
@@ -25,15 +20,9 @@ function sanitizeAndCheckVideoCommentObject (comment: any) {
) // Only accept public comments
}
function isVideoCommentDeleteActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Delete')
}
// ---------------------------------------------------------------------------
export {
isVideoCommentCreateActivityValid,
isVideoCommentDeleteActivityValid,
sanitizeAndCheckVideoCommentObject
}

View File

@@ -1,7 +1,7 @@
import * as validator from 'validator'
import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers'
import { peertubeTruncate } from '../../core-utils'
import { exists, isBooleanValid, isDateValid, isUUIDValid } from '../misc'
import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc'
import {
isVideoDurationValid,
isVideoNameValid,
@@ -12,29 +12,12 @@ import {
} from '../videos'
import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
import { VideoState } from '../../../../shared/models/videos'
import { isVideoAbuseReasonValid } from '../video-abuses'
function sanitizeAndCheckVideoTorrentCreateActivity (activity: any) {
return isBaseActivityValid(activity, 'Create') &&
sanitizeAndCheckVideoTorrentObject(activity.object)
}
function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) {
return isBaseActivityValid(activity, 'Update') &&
sanitizeAndCheckVideoTorrentObject(activity.object)
}
function isVideoTorrentDeleteActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Delete')
}
function isVideoFlagValid (activity: any) {
return isBaseActivityValid(activity, 'Create') &&
activity.object.type === 'Flag' &&
isVideoAbuseReasonValid(activity.object.content) &&
isActivityPubUrlValid(activity.object.object)
}
function isActivityPubVideoDurationValid (value: string) {
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
return exists(value) &&
@@ -83,32 +66,35 @@ function isRemoteVideoUrlValid (url: any) {
return url.type === 'Link' &&
(
ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.indexOf(url.mimeType) !== -1 &&
// TODO: remove mimeType (backward compatibility, introduced in v1.1.0)
ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.indexOf(url.mediaType || url.mimeType) !== -1 &&
isActivityPubUrlValid(url.href) &&
validator.isInt(url.height + '', { min: 0 }) &&
validator.isInt(url.size + '', { min: 0 }) &&
(!url.fps || validator.isInt(url.fps + '', { min: -1 }))
) ||
(
ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.indexOf(url.mimeType) !== -1 &&
ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.indexOf(url.mediaType || url.mimeType) !== -1 &&
isActivityPubUrlValid(url.href) &&
validator.isInt(url.height + '', { min: 0 })
) ||
(
ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mimeType) !== -1 &&
ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mediaType || url.mimeType) !== -1 &&
validator.isLength(url.href, { min: 5 }) &&
validator.isInt(url.height + '', { min: 0 })
) ||
(
(url.mediaType || url.mimeType) === 'application/x-mpegURL' &&
isActivityPubUrlValid(url.href) &&
isArray(url.tag)
)
}
// ---------------------------------------------------------------------------
export {
sanitizeAndCheckVideoTorrentCreateActivity,
sanitizeAndCheckVideoTorrentUpdateActivity,
isVideoTorrentDeleteActivityValid,
isRemoteStringIdentifierValid,
isVideoFlagValid,
sanitizeAndCheckVideoTorrentObject,
isRemoteVideoUrlValid
}

View File

@@ -1,11 +1,11 @@
import { isActivityPubUrlValid, isBaseActivityValid } from './misc'
import { isActivityPubUrlValid } from './misc'
function isViewActivityValid (activity: any) {
return isBaseActivityValid(activity, 'Create') &&
activity.object.type === 'View' &&
isActivityPubUrlValid(activity.object.actor) &&
isActivityPubUrlValid(activity.object.object)
return activity.type === 'View' &&
isActivityPubUrlValid(activity.actor) &&
isActivityPubUrlValid(activity.object)
}
// ---------------------------------------------------------------------------
export {

View File

@@ -9,6 +9,14 @@ function isArray (value: any) {
return Array.isArray(value)
}
function isNotEmptyIntArray (value: any) {
return Array.isArray(value) && value.every(v => validator.isInt('' + v)) && value.length !== 0
}
function isArrayOf (value: any, validator: (value: any) => boolean) {
return isArray(value) && value.every(v => validator(v))
}
function isDateValid (value: string) {
return exists(value) && validator.isISO8601(value)
}
@@ -78,6 +86,8 @@ function isFileValid (
export {
exists,
isArrayOf,
isNotEmptyIntArray,
isArray,
isIdValid,
isUUIDValid,

View File

@@ -3,6 +3,7 @@ import 'express-validator'
import { isArray, exists } from './misc'
import { isTestInstance } from '../core-utils'
import { CONSTRAINTS_FIELDS } from '../../initializers'
function isHostValid (host: string) {
const isURLOptions = {
@@ -26,9 +27,19 @@ function isEachUniqueHostValid (hosts: string[]) {
})
}
function isValidContactBody (value: any) {
return exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.CONTACT_FORM.BODY)
}
function isValidContactFromName (value: any) {
return exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.CONTACT_FORM.FROM_NAME)
}
// ---------------------------------------------------------------------------
export {
isValidContactBody,
isValidContactFromName,
isEachUniqueHostValid,
isHostValid
}

View File

@@ -0,0 +1,23 @@
import { exists } from './misc'
import * as validator from 'validator'
import { UserNotificationType } from '../../../shared/models/users'
import { UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model'
function isUserNotificationTypeValid (value: any) {
return exists(value) && validator.isInt('' + value) && UserNotificationType[value] !== undefined
}
function isUserNotificationSettingValid (value: any) {
return exists(value) &&
validator.isInt('' + value) && (
value === UserNotificationSettingValue.NONE ||
value === UserNotificationSettingValue.WEB ||
value === UserNotificationSettingValue.EMAIL ||
value === (UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL)
)
}
export {
isUserNotificationSettingValid,
isUserNotificationTypeValid
}

View File

@@ -42,6 +42,14 @@ function isUserNSFWPolicyValid (value: any) {
return exists(value) && nsfwPolicies.indexOf(value) !== -1
}
function isUserWebTorrentEnabledValid (value: any) {
return isBooleanValid(value)
}
function isUserVideosHistoryEnabledValid (value: any) {
return isBooleanValid(value)
}
function isUserAutoPlayVideoValid (value: any) {
return isBooleanValid(value)
}
@@ -69,6 +77,7 @@ function isAvatarFile (files: { [ fieldname: string ]: Express.Multer.File[] } |
// ---------------------------------------------------------------------------
export {
isUserVideosHistoryEnabledValid,
isUserBlockedValid,
isUserPasswordValid,
isUserBlockedReasonValid,
@@ -78,6 +87,7 @@ export {
isUserUsernameValid,
isUserEmailVerifiedValid,
isUserNSFWPolicyValid,
isUserWebTorrentEnabledValid,
isUserAutoPlayVideoValid,
isUserDisplayNameValid,
isUserDescriptionValid,

View File

@@ -1,4 +1,4 @@
import { CONSTRAINTS_FIELDS, VIDEO_CAPTIONS_MIMETYPE_EXT, VIDEO_LANGUAGES } from '../../initializers'
import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_LANGUAGES } from '../../initializers'
import { exists, isFileValid } from './misc'
import { Response } from 'express'
import { VideoModel } from '../../models/video/video'
@@ -8,7 +8,7 @@ function isVideoCaptionLanguageValid (value: any) {
return exists(value) && VIDEO_LANGUAGES[ value ] !== undefined
}
const videoCaptionTypes = Object.keys(VIDEO_CAPTIONS_MIMETYPE_EXT)
const videoCaptionTypes = Object.keys(MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT)
.concat([ 'application/octet-stream' ]) // MacOS sends application/octet-stream ><
.map(m => `(${m})`)
const videoCaptionTypesRegex = videoCaptionTypes.join('|')

View File

@@ -1,7 +1,7 @@
import 'express-validator'
import 'multer'
import * as validator from 'validator'
import { CONSTRAINTS_FIELDS, TORRENT_MIMETYPE_EXT, VIDEO_IMPORT_STATES } from '../../initializers'
import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_IMPORT_STATES } from '../../initializers'
import { exists, isFileValid } from './misc'
import * as express from 'express'
import { VideoImportModel } from '../../models/video/video-import'
@@ -24,7 +24,7 @@ function isVideoImportStateValid (value: any) {
return exists(value) && VIDEO_IMPORT_STATES[ value ] !== undefined
}
const videoTorrentImportTypes = Object.keys(TORRENT_MIMETYPE_EXT).map(m => `(${m})`)
const videoTorrentImportTypes = Object.keys(MIMETYPES.TORRENT.MIMETYPE_EXT).map(m => `(${m})`)
const videoTorrentImportRegex = videoTorrentImportTypes.join('|')
function isVideoImportTorrentFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) {
return isFileValid(files, videoTorrentImportRegex, 'torrentfile', CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_FILE.FILE_SIZE.max, true)

View File

@@ -3,12 +3,11 @@ import 'express-validator'
import { values } from 'lodash'
import 'multer'
import * as validator from 'validator'
import { UserRight, VideoPrivacy, VideoRateType } from '../../../shared'
import { UserRight, VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared'
import {
CONSTRAINTS_FIELDS,
CONSTRAINTS_FIELDS, MIMETYPES,
VIDEO_CATEGORIES,
VIDEO_LICENCES,
VIDEO_MIMETYPE_EXT,
VIDEO_PRIVACIES,
VIDEO_RATE_TYPES,
VIDEO_STATES
@@ -22,6 +21,10 @@ import { fetchVideo, VideoFetchType } from '../video'
const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
function isVideoFilterValid (filter: VideoFilter) {
return filter === 'local' || filter === 'all-local'
}
function isVideoCategoryValid (value: any) {
return value === null || VIDEO_CATEGORIES[ value ] !== undefined
}
@@ -79,10 +82,15 @@ function isVideoRatingTypeValid (value: string) {
return value === 'none' || values(VIDEO_RATE_TYPES).indexOf(value as VideoRateType) !== -1
}
const videoFileTypes = Object.keys(VIDEO_MIMETYPE_EXT).map(m => `(${m})`)
const videoFileTypesRegex = videoFileTypes.join('|')
function isVideoFileExtnameValid (value: string) {
return exists(value) && MIMETYPES.VIDEO.EXT_MIMETYPE[value] !== undefined
}
function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) {
const videoFileTypesRegex = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT)
.map(m => `(${m})`)
.join('|')
return isFileValid(files, videoFileTypesRegex, 'videofile', null)
}
@@ -217,6 +225,7 @@ export {
isVideoStateValid,
isVideoViewsValid,
isVideoRatingTypeValid,
isVideoFileExtnameValid,
isVideoDurationValid,
isVideoTagValid,
isVideoPrivacyValid,
@@ -225,5 +234,6 @@ export {
isVideoExist,
isVideoImage,
isVideoChannelOfAccountExist,
isVideoSupportValid
isVideoSupportValid,
isVideoFilterValid
}

View File

@@ -2,18 +2,17 @@ import * as express from 'express'
import * as multer from 'multer'
import { CONFIG, REMOTE_SCHEME } from '../initializers'
import { logger } from './logger'
import { User } from '../../shared/models/users'
import { deleteFileAsync, generateRandomString } from './utils'
import { extname } from 'path'
import { isArray } from './custom-validators/misc'
import { UserModel } from '../models/account/user'
function buildNSFWFilter (res: express.Response, paramNSFW?: string) {
function buildNSFWFilter (res?: express.Response, paramNSFW?: string) {
if (paramNSFW === 'true') return true
if (paramNSFW === 'false') return false
if (paramNSFW === 'both') return undefined
if (res.locals.oauth) {
if (res && res.locals.oauth) {
const user: UserModel = res.locals.oauth.token.User
// User does not want NSFW videos
@@ -101,7 +100,7 @@ function createReqFiles (
}
function isUserAbleToSearchRemoteURI (res: express.Response) {
const user: User = res.locals.oauth ? res.locals.oauth.token.User : undefined
const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined
return CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true ||
(CONFIG.SEARCH.REMOTE_URI.USERS === true && user !== undefined)

View File

@@ -1,7 +1,7 @@
import * as ffmpeg from 'fluent-ffmpeg'
import { join } from 'path'
import { VideoResolution } from '../../shared/models/videos'
import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers'
import { dirname, join } from 'path'
import { getTargetBitrate, VideoResolution } from '../../shared/models/videos'
import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants'
import { processImage } from './image-utils'
import { logger } from './logger'
import { checkFFmpegEncoders } from '../initializers/checker-before-init'
@@ -29,19 +29,28 @@ function computeResolutionsToTranscode (videoFileHeight: number) {
return resolutionsEnabled
}
async function getVideoFileResolution (path: string) {
async function getVideoFileSize (path: string) {
const videoStream = await getVideoFileStream(path)
return {
videoFileResolution: Math.min(videoStream.height, videoStream.width),
isPortraitMode: videoStream.height > videoStream.width
width: videoStream.width,
height: videoStream.height
}
}
async function getVideoFileResolution (path: string) {
const size = await getVideoFileSize(path)
return {
videoFileResolution: Math.min(size.height, size.width),
isPortraitMode: size.height > size.width
}
}
async function getVideoFileFPS (path: string) {
const videoStream = await getVideoFileStream(path)
for (const key of [ 'r_frame_rate' , 'avg_frame_rate' ]) {
for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) {
const valuesText: string = videoStream[key]
if (!valuesText) continue
@@ -55,6 +64,16 @@ async function getVideoFileFPS (path: string) {
return 0
}
async function getVideoFileBitrate (path: string) {
return new Promise<number>((res, rej) => {
ffmpeg.ffprobe(path, (err, metadata) => {
if (err) return rej(err)
return res(metadata.format.bit_rate)
})
})
}
function getDurationFromVideoFile (path: string) {
return new Promise<number>((res, rej) => {
ffmpeg.ffprobe(path, (err, metadata) => {
@@ -100,64 +119,87 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima
type TranscodeOptions = {
inputPath: string
outputPath: string
resolution?: VideoResolution
resolution: VideoResolution
isPortraitMode?: boolean
hlsPlaylist?: {
videoFilename: string
}
}
function transcode (options: TranscodeOptions) {
return new Promise<void>(async (res, rej) => {
let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING })
.output(options.outputPath)
.preset(standard)
if (CONFIG.TRANSCODING.THREADS > 0) {
// if we don't set any threads ffmpeg will chose automatically
command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
}
let fps = await getVideoFileFPS(options.inputPath)
if (options.resolution !== undefined) {
// '?x720' or '720x?' for example
const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}`
command = command.size(size)
try {
let fps = await getVideoFileFPS(options.inputPath)
// On small/medium resolutions, limit FPS
if (
options.resolution !== undefined &&
options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
fps > VIDEO_TRANSCODING_FPS.AVERAGE
) {
fps = VIDEO_TRANSCODING_FPS.AVERAGE
}
let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING })
.output(options.outputPath)
command = await presetH264(command, options.resolution, fps)
if (CONFIG.TRANSCODING.THREADS > 0) {
// if we don't set any threads ffmpeg will chose automatically
command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
}
if (options.resolution !== undefined) {
// '?x720' or '720x?' for example
const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}`
command = command.size(size)
}
if (fps) {
// Hard FPS limits
if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX
else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
command = command.withFPS(fps)
}
if (options.hlsPlaylist) {
const videoPath = `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
command = command.outputOption('-hls_time 4')
.outputOption('-hls_list_size 0')
.outputOption('-hls_playlist_type vod')
.outputOption('-hls_segment_filename ' + videoPath)
.outputOption('-hls_segment_type fmp4')
.outputOption('-f hls')
.outputOption('-hls_flags single_file')
}
command
.on('error', (err, stdout, stderr) => {
logger.error('Error in transcoding job.', { stdout, stderr })
return rej(err)
})
.on('end', res)
.run()
} catch (err) {
return rej(err)
}
if (fps) {
// Hard FPS limits
if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX
else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
command = command.withFPS(fps)
}
command
.on('error', (err, stdout, stderr) => {
logger.error('Error in transcoding job.', { stdout, stderr })
return rej(err)
})
.on('end', res)
.run()
})
}
// ---------------------------------------------------------------------------
export {
getVideoFileSize,
getVideoFileResolution,
getDurationFromVideoFile,
generateImageFromVideoFile,
transcode,
getVideoFileFPS,
computeResolutionsToTranscode,
audio
audio,
getVideoFileBitrate
}
// ---------------------------------------------------------------------------
@@ -168,7 +210,7 @@ function getVideoFileStream (path: string) {
if (err) return rej(err)
const videoStream = metadata.streams.find(s => s.codec_type === 'video')
if (!videoStream) throw new Error('Cannot find video stream of ' + path)
if (!videoStream) return rej(new Error('Cannot find video stream of ' + path))
return res(videoStream)
})
@@ -182,11 +224,10 @@ function getVideoFileStream (path: string) {
* and quality. Superfast and ultrafast will give you better
* performance, but then quality is noticeably worse.
*/
function veryfast (_ffmpeg) {
_ffmpeg
.preset(standard)
.outputOption('-preset:v veryfast')
.outputOption(['--aq-mode=2', '--aq-strength=1.3'])
async function presetH264VeryFast (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise<ffmpeg.FfmpegCommand> {
let localCommand = await presetH264(command, resolution, fps)
localCommand = localCommand.outputOption('-preset:v veryfast')
.outputOption([ '--aq-mode=2', '--aq-strength=1.3' ])
/*
MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html
Our target situation is closer to a livestream than a stream,
@@ -198,31 +239,39 @@ function veryfast (_ffmpeg) {
Make up for most of the loss of grain and macroblocking
with less computing power.
*/
return localCommand
}
/**
* A preset optimised for a stillimage audio video
*/
function audio (_ffmpeg) {
_ffmpeg
.preset(veryfast)
.outputOption('-tune stillimage')
async function presetStillImageWithAudio (
command: ffmpeg.FfmpegCommand,
resolution: VideoResolution,
fps: number
): Promise<ffmpeg.FfmpegCommand> {
let localCommand = await presetH264VeryFast(command, resolution, fps)
localCommand = localCommand.outputOption('-tune stillimage')
return localCommand
}
/**
* A toolbox to play with audio
*/
namespace audio {
export const get = (_ffmpeg, pos: number | string = 0) => {
export const get = (option: ffmpeg.FfmpegCommand | string) => {
// without position, ffprobe considers the last input only
// we make it consider the first input only
// if you pass a file path to pos, then ffprobe acts on that file directly
return new Promise<{ absolutePath: string, audioStream?: any }>((res, rej) => {
_ffmpeg.ffprobe(pos, (err,data) => {
function parseFfprobe (err: any, data: ffmpeg.FfprobeData) {
if (err) return rej(err)
if ('streams' in data) {
const audioStream = data['streams'].find(stream => stream['codec_type'] === 'audio')
const audioStream = data.streams.find(stream => stream['codec_type'] === 'audio')
if (audioStream) {
return res({
absolutePath: data.format.filename,
@@ -230,8 +279,15 @@ namespace audio {
})
}
}
return res({ absolutePath: data.format.filename })
})
}
if (typeof option === 'string') {
return ffmpeg.ffprobe(option, parseFfprobe)
}
return option.ffprobe(parseFfprobe)
})
}
@@ -273,39 +329,48 @@ namespace audio {
* As for the audio, quality '5' is the highest and ensures 96-112kbps/channel
* See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr
*/
async function standard (_ffmpeg) {
let localFfmpeg = _ffmpeg
async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResolution, fps: number): Promise<ffmpeg.FfmpegCommand> {
let localCommand = command
.format('mp4')
.videoCodec('libx264')
.outputOption('-level 3.1') // 3.1 is the minimal ressource allocation for our highest supported resolution
.outputOption('-b_strategy 1') // NOTE: b-strategy 1 - heuristic algorythm, 16 is optimal B-frames for it
.outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16
.outputOption('-pix_fmt yuv420p') // allows import of source material with incompatible pixel formats (e.g. MJPEG video)
.outputOption('-map_metadata -1') // strip all metadata
.outputOption('-movflags faststart')
const _audio = await audio.get(localFfmpeg)
if (!_audio.audioStream) {
return localFfmpeg.noAudio()
}
const parsedAudio = await audio.get(localCommand)
// we favor VBR, if a good AAC encoder is available
if ((await checkFFmpegEncoders()).get('libfdk_aac')) {
return localFfmpeg
if (!parsedAudio.audioStream) {
localCommand = localCommand.noAudio()
} else if ((await checkFFmpegEncoders()).get('libfdk_aac')) { // we favor VBR, if a good AAC encoder is available
localCommand = localCommand
.audioCodec('libfdk_aac')
.audioQuality(5)
} else {
// we try to reduce the ceiling bitrate by making rough correspondances of bitrates
// of course this is far from perfect, but it might save some space in the end
const audioCodecName = parsedAudio.audioStream[ 'codec_name' ]
let bitrate: number
if (audio.bitrate[ audioCodecName ]) {
localCommand = localCommand.audioCodec('aac')
bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ])
if (bitrate !== undefined && bitrate !== -1) localCommand = localCommand.audioBitrate(bitrate)
}
}
// we try to reduce the ceiling bitrate by making rough correspondances of bitrates
// of course this is far from perfect, but it might save some space in the end
const audioCodecName = _audio.audioStream['codec_name']
let bitrate: number
if (audio.bitrate[audioCodecName]) {
bitrate = audio.bitrate[audioCodecName](_audio.audioStream['bit_rate'])
// Constrained Encoding (VBV)
// https://slhck.info/video/2017/03/01/rate-control.html
// https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)
localCommand = localCommand.outputOptions([`-maxrate ${ targetBitrate }`, `-bufsize ${ targetBitrate * 2 }`])
if (bitrate === -1) return localFfmpeg.audioCodec('copy')
}
// Keyframe interval of 2 seconds for faster seeking and resolution switching.
// https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html
// https://superuser.com/a/908325
localCommand = localCommand.outputOption(`-g ${ fps * 2 }`)
if (bitrate !== undefined) return localFfmpeg.audioBitrate(bitrate)
return localFfmpeg
return localCommand
}

View File

@@ -1,13 +1,26 @@
import 'multer'
import * as sharp from 'sharp'
import { remove } from 'fs-extra'
import { readFile, remove } from 'fs-extra'
import { logger } from './logger'
async function processImage (
physicalFile: { path: string },
destination: string,
newSize: { width: number, height: number }
) {
await sharp(physicalFile.path)
if (physicalFile.path === destination) {
throw new Error('Sharp needs an input path different that the output path.')
}
logger.debug('Processing image %s to %s.', physicalFile.path, destination)
// Avoid sharp cache
const buf = await readFile(physicalFile.path)
const sharpInstance = sharp(buf)
await remove(destination)
await sharpInstance
.resize(newSize.width, newSize.height)
.toFile(destination)

View File

@@ -1,8 +1,14 @@
import { BCRYPT_SALT_SIZE, PRIVATE_RSA_KEY_SIZE } from '../initializers'
import { Request } from 'express'
import { BCRYPT_SALT_SIZE, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers'
import { ActorModel } from '../models/activitypub/actor'
import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey } from './core-utils'
import { jsig } from './custom-jsonld-signature'
import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey, sha256 } from './core-utils'
import { jsig, jsonld } from './custom-jsonld-signature'
import { logger } from './logger'
import { cloneDeep } from 'lodash'
import { createVerify } from 'crypto'
import { buildDigest } from '../lib/job-queue/handlers/utils/activitypub-http-utils'
const httpSignature = require('http-signature')
async function createPrivateAndPublicKeys () {
logger.info('Generating a RSA key...')
@@ -13,42 +19,7 @@ async function createPrivateAndPublicKeys () {
return { privateKey: key, publicKey }
}
function isSignatureVerified (fromActor: ActorModel, signedDocument: object) {
const publicKeyObject = {
'@context': jsig.SECURITY_CONTEXT_URL,
'@id': fromActor.url,
'@type': 'CryptographicKey',
owner: fromActor.url,
publicKeyPem: fromActor.publicKey
}
const publicKeyOwnerObject = {
'@context': jsig.SECURITY_CONTEXT_URL,
'@id': fromActor.url,
publicKey: [ publicKeyObject ]
}
const options = {
publicKey: publicKeyObject,
publicKeyOwner: publicKeyOwnerObject
}
return jsig.promises.verify(signedDocument, options)
.catch(err => {
logger.error('Cannot check signature.', { err })
return false
})
}
function signObject (byActor: ActorModel, data: any) {
const options = {
privateKeyPem: byActor.privateKey,
creator: byActor.url,
algorithm: 'RsaSignature2017'
}
return jsig.promises.sign(data, options)
}
// User password checks
function comparePassword (plainPassword: string, hashPassword: string) {
return bcryptComparePromise(plainPassword, hashPassword)
@@ -60,12 +31,119 @@ async function cryptPassword (password: string) {
return bcryptHashPromise(password, salt)
}
// HTTP Signature
function isHTTPSignatureDigestValid (rawBody: Buffer, req: Request): boolean {
if (req.headers[HTTP_SIGNATURE.HEADER_NAME] && req.headers['digest']) {
return buildDigest(rawBody.toString()) === req.headers['digest']
}
return true
}
function isHTTPSignatureVerified (httpSignatureParsed: any, actor: ActorModel): boolean {
return httpSignature.verifySignature(httpSignatureParsed, actor.publicKey) === true
}
function parseHTTPSignature (req: Request, clockSkew?: number) {
return httpSignature.parse(req, { authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, clockSkew })
}
// JSONLD
async function isJsonLDSignatureVerified (fromActor: ActorModel, signedDocument: any): Promise<boolean> {
if (signedDocument.signature.type === 'RsaSignature2017') {
// Mastodon algorithm
const res = await isJsonLDRSA2017Verified(fromActor, signedDocument)
// Success? If no, try with our library
if (res === true) return true
}
const publicKeyObject = {
'@context': jsig.SECURITY_CONTEXT_URL,
id: fromActor.url,
type: 'CryptographicKey',
owner: fromActor.url,
publicKeyPem: fromActor.publicKey
}
const publicKeyOwnerObject = {
'@context': jsig.SECURITY_CONTEXT_URL,
id: fromActor.url,
publicKey: [ publicKeyObject ]
}
const options = {
publicKey: publicKeyObject,
publicKeyOwner: publicKeyOwnerObject
}
return jsig.promises
.verify(signedDocument, options)
.then((result: { verified: boolean }) => result.verified)
.catch(err => {
logger.error('Cannot check signature.', { err })
return false
})
}
// Backward compatibility with "other" implementations
async function isJsonLDRSA2017Verified (fromActor: ActorModel, signedDocument: any) {
function hash (obj: any): Promise<any> {
return jsonld.promises
.normalize(obj, {
algorithm: 'URDNA2015',
format: 'application/n-quads'
})
.then(res => sha256(res))
}
const signatureCopy = cloneDeep(signedDocument.signature)
Object.assign(signatureCopy, {
'@context': [
'https://w3id.org/security/v1',
{ RsaSignature2017: 'https://w3id.org/security#RsaSignature2017' }
]
})
delete signatureCopy.type
delete signatureCopy.id
delete signatureCopy.signatureValue
const docWithoutSignature = cloneDeep(signedDocument)
delete docWithoutSignature.signature
const [ documentHash, optionsHash ] = await Promise.all([
hash(docWithoutSignature),
hash(signatureCopy)
])
const toVerify = optionsHash + documentHash
const verify = createVerify('RSA-SHA256')
verify.update(toVerify, 'utf8')
return verify.verify(fromActor.publicKey, signedDocument.signature.signatureValue, 'base64')
}
function signJsonLDObject (byActor: ActorModel, data: any) {
const options = {
privateKeyPem: byActor.privateKey,
creator: byActor.url,
algorithm: 'RsaSignature2017'
}
return jsig.promises.sign(data, options)
}
// ---------------------------------------------------------------------------
export {
isSignatureVerified,
isHTTPSignatureDigestValid,
parseHTTPSignature,
isHTTPSignatureVerified,
isJsonLDSignatureVerified,
comparePassword,
createPrivateAndPublicKeys,
cryptPassword,
signObject
signJsonLDObject
}

23
server/helpers/regexp.ts Normal file
View File

@@ -0,0 +1,23 @@
// Thanks to https://regex101.com
function regexpCapture (str: string, regex: RegExp, maxIterations = 100) {
let m: RegExpExecArray
let i = 0
let result: RegExpExecArray[] = []
// tslint:disable:no-conditional-assignment
while ((m = regex.exec(str)) !== null && i < maxIterations) {
// This is necessary to avoid infinite loops with zero-width matches
if (m.index === regex.lastIndex) {
regex.lastIndex++
}
result.push(m)
i++
}
return result
}
export {
regexpCapture
}

View File

@@ -1,17 +1,19 @@
import * as Bluebird from 'bluebird'
import { createWriteStream } from 'fs-extra'
import * as request from 'request'
import { ACTIVITY_PUB } from '../initializers'
import { ACTIVITY_PUB, CONFIG } from '../initializers'
import { processImage } from './image-utils'
import { join } from 'path'
function doRequest (
function doRequest <T> (
requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean }
): Bluebird<{ response: request.RequestResponse, body: any }> {
): Bluebird<{ response: request.RequestResponse, body: T }> {
if (requestOptions.activityPub === true) {
if (!Array.isArray(requestOptions.headers)) requestOptions.headers = {}
requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER
}
return new Bluebird<{ response: request.RequestResponse, body: any }>((res, rej) => {
return new Bluebird<{ response: request.RequestResponse, body: T }>((res, rej) => {
request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body }))
})
}
@@ -27,9 +29,18 @@ function doRequestAndSaveToFile (requestOptions: request.CoreOptions & request.U
})
}
async function downloadImage (url: string, destDir: string, destName: string, size: { width: number, height: number }) {
const tmpPath = join(CONFIG.STORAGE.TMP_DIR, 'pending-' + destName)
await doRequestAndSaveToFile({ method: 'GET', uri: url }, tmpPath)
const destPath = join(destDir, destName)
await processImage({ path: tmpPath }, destPath, size)
}
// ---------------------------------------------------------------------------
export {
doRequest,
doRequestAndSaveToFile
doRequestAndSaveToFile,
downloadImage
}

View File

@@ -19,10 +19,7 @@ async function generateRandomString (size: number) {
return raw.toString('hex')
}
interface FormattableToJSON {
toFormattedJSON (args?: any)
}
interface FormattableToJSON { toFormattedJSON (args?: any) }
function getFormattedObjects<U, T extends FormattableToJSON> (objects: T[], objectsTotal: number, formattedArg?: any) {
const formattedObjects: U[] = []
@@ -40,21 +37,24 @@ const getServerActor = memoizee(async function () {
const application = await ApplicationModel.load()
if (!application) throw Error('Could not load Application from database.')
return application.Account.Actor
const actor = application.Account.Actor
actor.Account = application.Account
return actor
})
function generateVideoTmpPath (target: string | ParseTorrent) {
function generateVideoImportTmpPath (target: string | ParseTorrent) {
const id = typeof target === 'string' ? target : target.infoHash
const hash = sha256(id)
return join(CONFIG.STORAGE.VIDEOS_DIR, hash + '-import.mp4')
return join(CONFIG.STORAGE.TMP_DIR, hash + '-import.mp4')
}
function getSecureTorrentName (originalName: string) {
return sha256(originalName) + '.torrent'
}
async function getVersion () {
async function getServerCommit () {
try {
const tag = await execPromise2(
'[ ! -d .git ] || git name-rev --name-only --tags --no-undefined HEAD 2>/dev/null || true',
@@ -74,7 +74,21 @@ async function getVersion () {
logger.debug('Cannot get version from git HEAD.', { err })
}
return require('../../../package.json').version
return ''
}
/**
* From a filename like "ede4cba5-742b-46fa-a388-9a6eb3a3aeb3.mp4", returns
* only the "ede4cba5-742b-46fa-a388-9a6eb3a3aeb3" part. If the filename does
* not contain a UUID, returns null.
*/
function getUUIDFromFilename (filename: string) {
const regex = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/
const result = filename.match(regex)
if (!result || Array.isArray(result) === false) return null
return result[0]
}
// ---------------------------------------------------------------------------
@@ -85,6 +99,7 @@ export {
getFormattedObjects,
getSecureTorrentName,
getServerActor,
getVersion,
generateVideoTmpPath
getServerCommit,
generateVideoImportTmpPath,
getUUIDFromFilename
}

View File

@@ -1,10 +1,12 @@
import { VideoModel } from '../models/video/video'
type VideoFetchType = 'all' | 'only-video' | 'id' | 'none'
type VideoFetchType = 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none'
function fetchVideo (id: number | string, fetchType: VideoFetchType, userId?: number) {
if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId)
if (fetchType === 'only-video-with-rights') return VideoModel.loadWithRights(id)
if (fetchType === 'only-video') return VideoModel.load(id)
if (fetchType === 'id' || fetchType === 'none') return VideoModel.loadOnlyId(id)

View File

@@ -1,5 +1,5 @@
import { logger } from './logger'
import { generateVideoTmpPath } from './utils'
import { generateVideoImportTmpPath } from './utils'
import * as WebTorrent from 'webtorrent'
import { createWriteStream, ensureDir, remove } from 'fs-extra'
import { CONFIG } from '../initializers'
@@ -9,10 +9,10 @@ async function downloadWebTorrentVideo (target: { magnetUri: string, torrentName
const id = target.magnetUri || target.torrentName
let timer
const path = generateVideoTmpPath(id)
const path = generateVideoImportTmpPath(id)
logger.info('Importing torrent video %s', id)
const directoryPath = join(CONFIG.STORAGE.VIDEOS_DIR, 'import')
const directoryPath = join(CONFIG.STORAGE.TMP_DIR, 'webtorrent')
await ensureDir(directoryPath)
return new Promise<string>((res, rej) => {

View File

@@ -1,7 +1,7 @@
import { truncate } from 'lodash'
import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES } from '../initializers'
import { logger } from './logger'
import { generateVideoTmpPath } from './utils'
import { generateVideoImportTmpPath } from './utils'
import { join } from 'path'
import { root } from './core-utils'
import { ensureDir, writeFile, remove } from 'fs-extra'
@@ -24,10 +24,10 @@ const processOptions = {
function getYoutubeDLInfo (url: string, opts?: string[]): Promise<YoutubeDLInfo> {
return new Promise<YoutubeDLInfo>(async (res, rej) => {
const options = opts || [ '-j', '--flat-playlist' ]
const args = opts || [ '-j', '--flat-playlist' ]
const youtubeDL = await safeGetYoutubeDL()
youtubeDL.getInfo(url, options, (err, info) => {
youtubeDL.getInfo(url, args, processOptions, (err, info) => {
if (err) return rej(err)
if (info.is_live === true) return rej(new Error('Cannot download a live streaming.'))
@@ -40,7 +40,7 @@ function getYoutubeDLInfo (url: string, opts?: string[]): Promise<YoutubeDLInfo>
}
function downloadYoutubeDLVideo (url: string, timeout: number) {
const path = generateVideoTmpPath(url)
const path = generateVideoImportTmpPath(url)
let timer
logger.info('Importing youtubeDL video %s', url)