1
0
Fork 0
mirror of https://github.com/Chocobozzz/PeerTube.git synced 2025-10-03 09:49:20 +02:00

Merge branch 'release/7.1.0' into develop

This commit is contained in:
Chocobozzz 2025-04-09 16:45:05 +02:00
commit ccb3fd4ab7
No known key found for this signature in database
GPG key ID: 583A612D890159BE
30 changed files with 384 additions and 157 deletions

View file

@ -5,7 +5,9 @@ import {
ensureCanAccessPrivateVideoHLSFiles,
ensureCanAccessVideoPrivateWebVideoFiles,
handleStaticError,
optionalAuthenticate
optionalAuthenticate,
privateHLSFileValidator,
privateM3U8PlaylistValidator
} from '@server/middlewares/index.js'
import cors from 'cors'
import express from 'express'
@ -55,17 +57,20 @@ const privateHLSStaticMiddlewares = CONFIG.STATIC_FILES.PRIVATE_FILES_REQUIRE_AU
: []
staticRouter.use(
STATIC_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS + ':videoUUID/:playlistName.m3u8',
STATIC_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS + ':videoUUID/:playlistNameWithoutExtension([a-z0-9-]+).m3u8',
privateM3U8PlaylistValidator,
...privateHLSStaticMiddlewares,
asyncMiddleware(servePrivateM3U8)
)
staticRouter.use(
STATIC_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS,
STATIC_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS + ':videoUUID/:filename',
privateHLSFileValidator,
...privateHLSStaticMiddlewares,
express.static(DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE, { fallthrough: false }),
handleStaticError
servePrivateHLSFile
)
// ---------------------------------------------------------------------------
staticRouter.use(
STATIC_PATHS.STREAMING_PLAYLISTS.HLS,
express.static(DIRECTORIES.HLS_STREAMING_PLAYLIST.PUBLIC, { fallthrough: false }),
@ -80,9 +85,15 @@ export {
// ---------------------------------------------------------------------------
function servePrivateHLSFile (req: express.Request, res: express.Response) {
const path = join(DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE, req.params.videoUUID, req.params.filename)
return res.sendFile(path)
}
async function servePrivateM3U8 (req: express.Request, res: express.Response) {
const path = join(DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE, req.params.videoUUID, req.params.playlistName + '.m3u8')
const filename = req.params.playlistName + '.m3u8'
const path = join(DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE, req.params.videoUUID, req.params.playlistNameWithoutExtension + '.m3u8')
const filename = req.params.playlistNameWithoutExtension + '.m3u8'
let playlistContent: string

View file

@ -7,7 +7,14 @@ import { logger, loggerTagsFactory } from './logger.js'
const lTags = loggerTagsFactory('unzip')
export async function unzip (source: string, destination: string) {
export async function unzip (options: {
source: string
destination: string
maxSize: number // in bytes
maxFiles: number
}) {
const { source, destination } = options
await ensureDir(destination)
logger.info(`Unzip ${source} to ${destination}`, lTags())
@ -16,9 +23,27 @@ export async function unzip (source: string, destination: string) {
yauzl.open(source, { lazyEntries: true }, (err, zipFile) => {
if (err) return rej(err)
zipFile.on('error', err => rej(err))
let decompressedSize = 0
let entries = 0
zipFile.readEntry()
zipFile.on('entry', async entry => {
decompressedSize += entry.uncompressedSize
entries++
if (decompressedSize > options.maxSize) {
zipFile.close()
return rej(new Error(`Unzipped size exceeds ${options.maxSize} bytes`))
}
if (entries > options.maxFiles) {
zipFile.close()
return rej(new Error(`Unzipped files count exceeds ${options.maxFiles}`))
}
const entryPath = join(destination, entry.fileName)
try {

View file

@ -0,0 +1,8 @@
import { MActorHostOnly } from '@server/types/models/index.js'
export function haveActorsSameRemoteHost (base: MActorHostOnly, other: MActorHostOnly) {
if (!base.serverId || !other.serverId) return false
if (base.serverId !== other.serverId) return false
return true
}

View file

@ -143,7 +143,7 @@ async function scheduleOutboxFetchIfNeeded (actor: MActor, created: boolean, ref
async function schedulePlaylistFetchIfNeeded (actor: MActorAccountId, created: boolean, accountPlaylistsUrl: string) {
// We created a new account: fetch the playlists
if (created === true && actor.Account && accountPlaylistsUrl) {
const payload = { uri: accountPlaylistsUrl, type: 'account-playlists' as 'account-playlists' }
const payload = { uri: accountPlaylistsUrl, type: 'account-playlists' as 'account-playlists', accountId: actor.Account.id }
await JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
}
}

View file

@ -1,3 +1,4 @@
export * from './check-actor.js'
export * from './get.js'
export * from './image.js'
export * from './keys.js'

View file

@ -6,10 +6,10 @@ import { logger } from '../../helpers/logger.js'
import { ACTIVITY_PUB, WEBSERVER } from '../../initializers/constants.js'
import { fetchAP } from './activity.js'
type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>)
type HandlerFunction<T> = (items: T[]) => Promise<any> | Bluebird<any>
type CleanerFunction = (startedDate: Date) => Promise<any>
async function crawlCollectionPage <T> (argUrl: string, handler: HandlerFunction<T>, cleaner?: CleanerFunction) {
export async function crawlCollectionPage<T> (argUrl: string, handler: HandlerFunction<T>, cleaner?: CleanerFunction) {
let url = argUrl
logger.info('Crawling ActivityPub data on %s.', url)
@ -23,6 +23,8 @@ async function crawlCollectionPage <T> (argUrl: string, handler: HandlerFunction
let i = 0
let nextLink = firstBody.first
while (nextLink && i < limit) {
i++
let body: any
if (typeof nextLink === 'string') {
@ -40,7 +42,6 @@ async function crawlCollectionPage <T> (argUrl: string, handler: HandlerFunction
}
nextLink = body.next
i++
if (Array.isArray(body.orderedItems)) {
const items = body.orderedItems
@ -52,7 +53,3 @@ async function crawlCollectionPage <T> (argUrl: string, handler: HandlerFunction
if (cleaner) await retryTransactionWrapper(cleaner, startDate)
}
export {
crawlCollectionPage
}

View file

@ -1,5 +1,4 @@
import { HttpStatusCode, PlaylistObject } from '@peertube/peertube-models'
import { AttributesOnly } from '@peertube/peertube-typescript-utils'
import { isArray } from '@server/helpers/custom-validators/misc.js'
import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
@ -10,11 +9,18 @@ import { updateRemotePlaylistMiniatureFromUrl } from '@server/lib/thumbnail.js'
import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element.js'
import { VideoPlaylistModel } from '@server/models/video/video-playlist.js'
import { FilteredModelAttributes } from '@server/types/index.js'
import { MThumbnail, MVideoPlaylist, MVideoPlaylistFull, MVideoPlaylistVideosLength } from '@server/types/models/index.js'
import {
MAccountHost,
MThumbnail,
MVideoPlaylist,
MVideoPlaylistFull,
MVideoPlaylistVideosLength
} from '@server/types/models/index.js'
import Bluebird from 'bluebird'
import { getAPId } from '../activity.js'
import { getOrCreateAPActor } from '../actors/index.js'
import { crawlCollectionPage } from '../crawl.js'
import { checkUrlsSameHost } from '../url.js'
import { getOrCreateAPVideo } from '../videos/index.js'
import {
fetchRemotePlaylistElement,
@ -22,11 +28,17 @@ import {
playlistElementObjectToDBAttributes,
playlistObjectToDBAttributes
} from './shared/index.js'
import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activitypub/misc.js'
const lTags = loggerTagsFactory('ap', 'video-playlist')
async function createAccountPlaylists (playlistUrls: string[]) {
export async function createAccountPlaylists (playlistUrls: string[], account: MAccountHost) {
await Bluebird.map(playlistUrls, async playlistUrl => {
if (!checkUrlsSameHost(playlistUrl, account.Actor.url)) {
logger.warn(`Playlist ${playlistUrl} is not on the same host as owner account ${account.Actor.url}`, lTags(playlistUrl))
return
}
try {
const exists = await VideoPlaylistModel.doesPlaylistExist(playlistUrl)
if (exists === true) return
@ -37,17 +49,31 @@ async function createAccountPlaylists (playlistUrls: string[]) {
throw new Error(`Cannot refresh remote playlist ${playlistUrl}: invalid body.`)
}
return createOrUpdateVideoPlaylist(playlistObject)
return createOrUpdateVideoPlaylist({ playlistObject, contextUrl: playlistUrl })
} catch (err) {
logger.warn(`Cannot create or update playlist ${playlistUrl}`, { err, ...lTags(playlistUrl) })
}
}, { concurrency: CRAWL_REQUEST_CONCURRENCY })
}
async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, to?: string[]) {
export async function createOrUpdateVideoPlaylist (options: {
playlistObject: PlaylistObject
// Which is the context where we retrieved the playlist
// Can be the actor that signed the activity URL or the playlist URL we fetched
contextUrl: string
to?: string[]
}) {
const { playlistObject, contextUrl, to } = options
if (!checkUrlsSameHost(playlistObject.id, contextUrl)) {
throw new Error(`Playlist ${playlistObject.id} is not on the same host as context URL ${contextUrl}`)
}
const playlistAttributes = playlistObjectToDBAttributes(playlistObject, to || playlistObject.to)
await setVideoChannel(playlistObject, playlistAttributes)
const channel = await getRemotePlaylistChannel(playlistObject)
playlistAttributes.videoChannelId = channel.id
playlistAttributes.ownerAccountId = channel.accountId
const [ upsertPlaylist ] = await VideoPlaylistModel.upsert<MVideoPlaylistVideosLength>(playlistAttributes, { returning: true })
@ -65,28 +91,26 @@ async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, to?:
}
// ---------------------------------------------------------------------------
export {
createAccountPlaylists,
createOrUpdateVideoPlaylist
}
// Private
// ---------------------------------------------------------------------------
async function setVideoChannel (playlistObject: PlaylistObject, playlistAttributes: AttributesOnly<VideoPlaylistModel>) {
async function getRemotePlaylistChannel (playlistObject: PlaylistObject) {
if (!isArray(playlistObject.attributedTo) || playlistObject.attributedTo.length !== 1) {
throw new Error('Not attributed to for playlist object ' + getAPId(playlistObject))
}
const actor = await getOrCreateAPActor(getAPId(playlistObject.attributedTo[0]), 'all')
if (!actor.VideoChannel) {
logger.warn('Playlist "attributedTo" %s is not a video channel.', playlistObject.id, { playlistObject, ...lTags(playlistObject.id) })
return
const channelUrl = getAPId(playlistObject.attributedTo[0])
if (!checkUrlsSameHost(channelUrl, playlistObject.id)) {
throw new Error(`Playlist ${playlistObject.id} and "attributedTo" channel ${channelUrl} are not on the same host`)
}
playlistAttributes.videoChannelId = actor.VideoChannel.id
playlistAttributes.ownerAccountId = actor.VideoChannel.Account.id
const actor = await getOrCreateAPActor(channelUrl, 'all')
if (!actor.VideoChannel) {
throw new Error(`Playlist ${playlistObject.id} "attributedTo" is not a video channel.`)
}
return actor.VideoChannel
}
async function fetchElementUrls (playlistObject: PlaylistObject) {
@ -97,7 +121,7 @@ async function fetchElementUrls (playlistObject: PlaylistObject) {
return Promise.resolve()
})
return accItems
return accItems.filter(i => isActivityPubUrlValid(i))
}
async function updatePlaylistThumbnail (playlistObject: PlaylistObject, playlist: MVideoPlaylistFull) {

View file

@ -1,14 +1,11 @@
import { VideoPlaylistModel } from '@server/models/video/video-playlist.js'
import { MVideoPlaylistFullSummary } from '@server/types/models/index.js'
import { APObjectId } from '@peertube/peertube-models'
import { getAPId } from '../activity.js'
import { createOrUpdateVideoPlaylist } from './create-update.js'
import { scheduleRefreshIfNeeded } from './refresh.js'
import { fetchRemoteVideoPlaylist } from './shared/index.js'
async function getOrCreateAPVideoPlaylist (playlistObjectArg: APObjectId): Promise<MVideoPlaylistFullSummary> {
const playlistUrl = getAPId(playlistObjectArg)
export async function getOrCreateAPVideoPlaylist (playlistUrl: string): Promise<MVideoPlaylistFullSummary> {
const playlistFromDatabase = await VideoPlaylistModel.loadByUrlWithAccountAndChannelSummary(playlistUrl)
if (playlistFromDatabase) {
@ -21,15 +18,9 @@ async function getOrCreateAPVideoPlaylist (playlistObjectArg: APObjectId): Promi
if (!playlistObject) throw new Error('Cannot fetch remote playlist with url: ' + playlistUrl)
// playlistUrl is just an alias/redirection, so process object id instead
if (playlistObject.id !== playlistUrl) return getOrCreateAPVideoPlaylist(playlistObject)
if (playlistObject.id !== playlistUrl) return getOrCreateAPVideoPlaylist(getAPId(playlistObject))
const playlistCreated = await createOrUpdateVideoPlaylist(playlistObject)
const playlistCreated = await createOrUpdateVideoPlaylist({ playlistObject, contextUrl: playlistUrl })
return playlistCreated
}
// ---------------------------------------------------------------------------
export {
getOrCreateAPVideoPlaylist
}

View file

@ -1,8 +1,8 @@
import { HttpStatusCode } from '@peertube/peertube-models'
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
import { PeerTubeRequestError } from '@server/helpers/requests.js'
import { JobQueue } from '@server/lib/job-queue/index.js'
import { MVideoPlaylist, MVideoPlaylistOwner } from '@server/types/models/index.js'
import { HttpStatusCode } from '@peertube/peertube-models'
import { MVideoPlaylist, MVideoPlaylistOwnerDefault } from '@server/types/models/index.js'
import { createOrUpdateVideoPlaylist } from './create-update.js'
import { fetchRemoteVideoPlaylist } from './shared/index.js'
@ -12,7 +12,7 @@ function scheduleRefreshIfNeeded (playlist: MVideoPlaylist) {
JobQueue.Instance.createJobAsync({ type: 'activitypub-refresher', payload: { type: 'video-playlist', url: playlist.url } })
}
async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner): Promise<MVideoPlaylistOwner> {
async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwnerDefault): Promise<MVideoPlaylistOwnerDefault> {
if (!videoPlaylist.isOutdated()) return videoPlaylist
const lTags = loggerTagsFactory('ap', 'video-playlist', 'refresh', videoPlaylist.uuid, videoPlaylist.url)
@ -29,7 +29,7 @@ async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner)
return videoPlaylist
}
await createOrUpdateVideoPlaylist(playlistObject)
await createOrUpdateVideoPlaylist({ playlistObject, contextUrl: videoPlaylist.url })
return videoPlaylist
} catch (err) {
@ -50,6 +50,6 @@ async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner)
}
export {
scheduleRefreshIfNeeded,
refreshVideoPlaylistIfNeeded
refreshVideoPlaylistIfNeeded,
scheduleRefreshIfNeeded
}

View file

@ -16,8 +16,8 @@ export function playlistObjectToDBAttributes (playlistObject: PlaylistObject, to
privacy,
url: playlistObject.id,
uuid: playlistObject.uuid,
ownerAccountId: null,
videoChannelId: null,
ownerAccountId: null,
createdAt: new Date(playlistObject.published),
updatedAt: new Date(playlistObject.updated)
} as AttributesOnly<VideoPlaylistModel>

View file

@ -184,8 +184,7 @@ async function processCreatePlaylist (
byActor: MActorSignature
) {
const byAccount = byActor.Account
if (!byAccount) throw new Error('Cannot create video playlist with the non account actor ' + byActor.url)
await createOrUpdateVideoPlaylist(playlistObject, activity.to)
await createOrUpdateVideoPlaylist({ playlistObject, contextUrl: byActor.url, to: activity.to })
}

View file

@ -127,5 +127,5 @@ async function processUpdatePlaylist (
const byAccount = byActor.Account
if (!byAccount) throw new Error('Cannot update video playlist with the non account actor ' + byActor.url)
await createOrUpdateVideoPlaylist(playlistObject, activity.to)
await createOrUpdateVideoPlaylist({ playlistObject, contextUrl: byActor.url, to: activity.to })
}

View file

@ -1,4 +1,3 @@
import { Transaction } from 'sequelize'
import { VideoObject, VideoPrivacy } from '@peertube/peertube-models'
import { resetSequelizeInstance, runInReadCommittedTransaction } from '@server/helpers/database-utils.js'
import { logger, loggerTagsFactory, LoggerTagsFn } from '@server/helpers/logger.js'
@ -8,12 +7,14 @@ import { Hooks } from '@server/lib/plugins/hooks.js'
import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist.js'
import { VideoLiveModel } from '@server/models/video/video-live.js'
import {
MActor,
MActorHost,
MChannelAccountLight,
MChannelId,
MVideoAccountLightBlacklistAllFiles,
MVideoFullLight
} from '@server/types/models/index.js'
import { Transaction } from 'sequelize'
import { haveActorsSameRemoteHost } from '../actors/check-actor.js'
import { APVideoAbstractBuilder, getVideoAttributesFromObject, updateVideoRates } from './shared/index.js'
export class APVideoUpdater extends APVideoAbstractBuilder {
@ -40,7 +41,8 @@ export class APVideoUpdater extends APVideoAbstractBuilder {
async update (overrideTo?: string[]) {
logger.debug(
'Updating remote video "%s".', this.videoObject.uuid,
'Updating remote video "%s".',
this.videoObject.uuid,
{ videoObject: this.videoObject, ...this.lTags() }
)
@ -111,13 +113,9 @@ export class APVideoUpdater extends APVideoAbstractBuilder {
}
// Check we can update the channel: we trust the remote server
private checkChannelUpdateOrThrow (newChannelActor: MActor) {
if (!this.oldVideoChannel.Actor.serverId || !newChannelActor.serverId) {
throw new Error('Cannot check old channel/new channel validity because `serverId` is null')
}
if (this.oldVideoChannel.Actor.serverId !== newChannelActor.serverId) {
throw new Error(`New channel ${newChannelActor.url} is not on the same server than new channel ${this.oldVideoChannel.Actor.url}`)
private checkChannelUpdateOrThrow (newChannelActor: MActorHost) {
if (haveActorsSameRemoteHost(this.oldVideoChannel.Actor, newChannelActor) !== true) {
throw new Error(`Actor ${this.oldVideoChannel.Actor.url} is not on the same host as ${newChannelActor.url}`)
}
}

View file

@ -4,12 +4,13 @@ import { logger } from '../../../helpers/logger.js'
import { VideoModel } from '../../../models/video/video.js'
import { VideoCommentModel } from '../../../models/video/video-comment.js'
import { VideoShareModel } from '../../../models/video/video-share.js'
import { MVideoFullLight } from '../../../types/models/index.js'
import { MAccountDefault, MVideoFullLight } from '../../../types/models/index.js'
import { crawlCollectionPage } from '../../activitypub/crawl.js'
import { createAccountPlaylists } from '../../activitypub/playlists/index.js'
import { processActivities } from '../../activitypub/process/index.js'
import { addVideoShares } from '../../activitypub/share.js'
import { addVideoComments } from '../../activitypub/video-comments.js'
import { AccountModel } from '@server/models/account/account.js'
async function processActivityPubHttpFetcher (job: Job) {
logger.info('Processing ActivityPub fetcher in job %s.', job.id)
@ -19,14 +20,17 @@ async function processActivityPubHttpFetcher (job: Job) {
let video: MVideoFullLight
if (payload.videoId) video = await VideoModel.loadFull(payload.videoId)
const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = {
let account: MAccountDefault
if (payload.accountId) account = await AccountModel.load(payload.accountId)
const fetcherType: { [id in FetchType]: (items: any[]) => Promise<any> } = {
'activity': items => processActivities(items, { outboxUrl: payload.uri, fromFetch: true }),
'video-shares': items => addVideoShares(items, video),
'video-comments': items => addVideoComments(items),
'account-playlists': items => createAccountPlaylists(items)
'account-playlists': items => createAccountPlaylists(items, account)
}
const cleanerType: { [ id in FetchType ]?: (crawlStartDate: Date) => Promise<any> } = {
const cleanerType: { [id in FetchType]?: (crawlStartDate: Date) => Promise<any> } = {
'video-shares': crawlStartDate => VideoShareModel.cleanOldSharesOf(video.id, crawlStartDate),
'video-comments': crawlStartDate => VideoCommentModel.cleanOldCommentsOf(video.id, crawlStartDate)
}

View file

@ -1,5 +1,5 @@
import { UserImportResultSummary, UserImportState } from '@peertube/peertube-models'
import { getFilenameWithoutExt } from '@peertube/peertube-node-utils'
import { getFilenameWithoutExt, getFileSize } from '@peertube/peertube-node-utils'
import { saveInTransactionWithRetries } from '@server/helpers/database-utils.js'
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
import { unzip } from '@server/helpers/unzip.js'
@ -20,6 +20,7 @@ import { UserVideoHistoryImporter } from './importers/user-video-history-importe
import { VideoPlaylistsImporter } from './importers/video-playlists-importer.js'
import { VideosImporter } from './importers/videos-importer.js'
import { WatchedWordsListsImporter } from './importers/watched-words-lists-importer.js'
import { parseBytes } from '@server/helpers/core-utils.js'
const lTags = loggerTagsFactory('user-import')
@ -51,7 +52,14 @@ export class UserImporter {
const inputZip = getFSUserImportFilePath(importModel)
this.extractedDirectory = join(dirname(inputZip), getFilenameWithoutExt(inputZip))
await unzip(inputZip, this.extractedDirectory)
await unzip({
source: inputZip,
destination: this.extractedDirectory,
// Videos that take a lot of space don't have a good compression ratio
// Keep a minimum of 1GB if the archive doesn't contain video files
maxSize: Math.max(await getFileSize(inputZip) * 2, parseBytes('1GB')),
maxFiles: 10000
})
const user = await UserModel.loadByIdFull(importModel.userId)

View file

@ -83,7 +83,7 @@ export async function doesVideoChannelOfAccountExist (channelId: number, user: M
const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId)
if (videoChannel === null) {
res.fail({ message: 'Unknown video "video channel" for this instance.' })
res.fail({ message: `Unknown ${channelId} on this instance.` })
return false
}
@ -94,9 +94,7 @@ export async function doesVideoChannelOfAccountExist (channelId: number, user: M
}
if (videoChannel.Account.id !== user.Account.id) {
res.fail({
message: 'Unknown video "video channel" for this account.'
})
res.fail({ message: `Unknown channel ${channelId} for this account.` })
return false
}

View file

@ -1,28 +1,35 @@
import { HttpStatusCode } from '@peertube/peertube-models'
import { exists, isSafePeerTubeFilenameWithoutExtension, isUUIDValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc.js'
import {
exists,
isSafeFilename,
isSafePeerTubeFilenameWithoutExtension,
isUUIDValid,
toBooleanOrNull
} from '@server/helpers/custom-validators/misc.js'
import { logger } from '@server/helpers/logger.js'
import { LRU_CACHE } from '@server/initializers/constants.js'
import { VideoFileModel } from '@server/models/video/video-file.js'
import { VideoModel } from '@server/models/video/video.js'
import { MStreamingPlaylist, MVideoFile, MVideoThumbnailBlacklist } from '@server/types/models/index.js'
import express from 'express'
import { query } from 'express-validator'
import { param, query } from 'express-validator'
import { LRUCache } from 'lru-cache'
import { basename, dirname } from 'path'
import { basename } from 'path'
import { areValidationErrors, checkCanAccessVideoStaticFiles, isValidVideoPasswordHeader } from './shared/index.js'
type LRUValue = {
allowed: boolean
video?: MVideoThumbnailBlacklist
file?: MVideoFile
playlist?: MStreamingPlaylist }
playlist?: MStreamingPlaylist
}
const staticFileTokenBypass = new LRUCache<string, LRUValue>({
max: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.MAX_SIZE,
ttl: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.TTL
})
const ensureCanAccessVideoPrivateWebVideoFiles = [
export const ensureCanAccessVideoPrivateWebVideoFiles = [
query('videoFileToken').optional().custom(exists),
isValidVideoPasswordHeader(),
@ -61,32 +68,50 @@ const ensureCanAccessVideoPrivateWebVideoFiles = [
}
]
const ensureCanAccessPrivateVideoHLSFiles = [
query('videoFileToken')
.optional()
.custom(exists),
export const privateM3U8PlaylistValidator = [
param('videoUUID')
.custom(isUUIDValid),
param('playlistNameWithoutExtension')
.custom(v => isSafePeerTubeFilenameWithoutExtension(v)),
query('reinjectVideoFileToken')
.optional()
.customSanitizer(toBooleanOrNull)
.isBoolean().withMessage('Should be a valid reinjectVideoFileToken boolean'),
query('playlistName')
(req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return
return next()
}
]
export const privateHLSFileValidator = [
param('videoUUID')
.custom(isUUIDValid),
param('filename')
.custom(v => isSafeFilename(v)),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return
return next()
}
]
export const ensureCanAccessPrivateVideoHLSFiles = [
query('videoFileToken')
.optional()
.customSanitizer(isSafePeerTubeFilenameWithoutExtension),
.custom(exists),
isValidVideoPasswordHeader(),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return
const videoUUID = basename(dirname(req.originalUrl))
if (!isUUIDValid(videoUUID)) {
logger.debug('Path does not contain valid video UUID to serve static file %s', req.originalUrl)
return res.sendStatus(HttpStatusCode.FORBIDDEN_403)
}
const videoUUID = req.params.videoUUID
const token = extractTokenOrDie(req, res)
if (!token) return
@ -121,10 +146,6 @@ const ensureCanAccessPrivateVideoHLSFiles = [
}
]
export {
ensureCanAccessPrivateVideoHLSFiles, ensureCanAccessVideoPrivateWebVideoFiles
}
// ---------------------------------------------------------------------------
async function isWebVideoAllowed (req: express.Request, res: express.Response) {

View file

@ -1,5 +1,3 @@
import express from 'express'
import { body, param, query, ValidationChain } from 'express-validator'
import { forceNumber } from '@peertube/peertube-core-utils'
import {
HttpStatusCode,
@ -12,6 +10,8 @@ import {
} from '@peertube/peertube-models'
import { ExpressPromiseHandler } from '@server/types/express-handler.js'
import { MUserAccountId } from '@server/types/models/index.js'
import express from 'express'
import { body, param, query, ValidationChain } from 'express-validator'
import {
isArrayOf,
isIdOrUUIDValid,
@ -37,7 +37,7 @@ import { MVideoPlaylist } from '../../../types/models/video/video-playlist.js'
import { authenticatePromise } from '../../auth.js'
import {
areValidationErrors,
doesVideoChannelIdExist,
doesVideoChannelOfAccountExist,
doesVideoExist,
doesVideoPlaylistExist,
isValidPlaylistIdParam,
@ -52,7 +52,9 @@ const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
const body: VideoPlaylistCreate = req.body
if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req)
if (body.videoChannelId && !await doesVideoChannelOfAccountExist(body.videoChannelId, res.locals.oauth.token.User, res)) {
return cleanUpReqFiles(req)
}
if (
!body.videoChannelId &&
@ -88,7 +90,8 @@ const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([
const body: VideoPlaylistUpdate = req.body
const newPrivacy = body.privacy || videoPlaylist.privacy
if (newPrivacy === VideoPlaylistPrivacy.PUBLIC &&
if (
newPrivacy === VideoPlaylistPrivacy.PUBLIC &&
(
(!videoPlaylist.videoChannelId && !body.videoChannelId) ||
body.videoChannelId === null
@ -105,7 +108,9 @@ const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([
return res.fail({ message: 'Cannot update a watch later playlist.' })
}
if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req)
if (body.videoChannelId && !await doesVideoChannelOfAccountExist(body.videoChannelId, res.locals.oauth.token.User, res)) {
return cleanUpReqFiles(req)
}
return next()
}
@ -350,21 +355,17 @@ const doVideosInPlaylistExistValidator = [
// ---------------------------------------------------------------------------
export {
commonVideoPlaylistFiltersValidator,
doVideosInPlaylistExistValidator,
videoPlaylistElementAPGetValidator,
videoPlaylistsAddValidator,
videoPlaylistsUpdateValidator,
videoPlaylistsAddVideoValidator,
videoPlaylistsDeleteValidator,
videoPlaylistsGetValidator,
videoPlaylistsSearchValidator,
videoPlaylistsAddVideoValidator,
videoPlaylistsUpdateOrRemoveVideoValidator,
videoPlaylistsReorderVideosValidator,
videoPlaylistElementAPGetValidator,
commonVideoPlaylistFiltersValidator,
doVideosInPlaylistExistValidator
videoPlaylistsSearchValidator,
videoPlaylistsUpdateOrRemoveVideoValidator,
videoPlaylistsUpdateValidator
}
// ---------------------------------------------------------------------------
@ -375,7 +376,7 @@ function getCommonPlaylistEditAttributes () {
.custom((value, { req }) => isVideoImageValid(req.files, 'thumbnailfile'))
.withMessage(
'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.IMAGE.EXTNAME.join(', ')
CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.IMAGE.EXTNAME.join(', ')
),
body('description')