mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-02-25 18:55:32 -06:00
Merge branch 'develop' into pr/1217
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
14
server/helpers/custom-validators/activitypub/flag.ts
Normal file
14
server/helpers/custom-validators/activitypub/flag.ts
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
23
server/helpers/custom-validators/user-notifications.ts
Normal file
23
server/helpers/custom-validators/user-notifications.ts
Normal 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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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('|')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
23
server/helpers/regexp.ts
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user