From 3545e72c686ff1725bbdfd8d16d693e2f4aa75a3 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 12 Oct 2022 16:09:02 +0200 Subject: Put private videos under a specific subdirectory --- .../+videos/+video-watch/video-watch.component.ts | 53 +++++++++++++++------- client/src/app/core/auth/auth-user.model.ts | 18 ++++---- client/src/app/core/auth/auth.service.ts | 4 +- .../app/core/users/user-local-storage.service.ts | 14 +++--- client/src/app/helpers/utils/url.ts | 5 +- .../app/shared/shared-main/shared-main.module.ts | 11 ++++- client/src/app/shared/shared-main/video/index.ts | 1 + .../shared-main/video/video-file-token.service.ts | 33 ++++++++++++++ .../video-download.component.html | 5 +- .../video-download.component.ts | 19 ++++++-- client/src/assets/player/shared/common/utils.ts | 8 +++- .../shared/manager-options/hls-options-builder.ts | 18 +++++++- .../manager-options/webtorrent-options-builder.ts | 15 +++++- .../shared/p2p-media-loader/segment-validator.ts | 30 ++++++++++-- .../player/shared/peertube/peertube-plugin.ts | 4 +- .../player/shared/webtorrent/webtorrent-plugin.ts | 22 +++++++-- client/src/assets/player/types/manager-options.ts | 4 +- .../player/types/peertube-videojs-typings.ts | 7 ++- client/src/root-helpers/logger.ts | 4 +- client/src/root-helpers/users/index.ts | 2 +- client/src/root-helpers/users/oauth-user-tokens.ts | 46 +++++++++++++++++++ client/src/root-helpers/users/user-tokens.ts | 46 ------------------- client/src/root-helpers/video.ts | 9 +++- client/src/standalone/videos/embed.ts | 22 +++++---- client/src/standalone/videos/shared/auth-http.ts | 28 ++++++------ .../videos/shared/player-manager-options.ts | 12 ++++- .../src/standalone/videos/shared/video-fetcher.ts | 17 +++++-- 27 files changed, 316 insertions(+), 141 deletions(-) create mode 100644 client/src/app/shared/shared-main/video/video-file-token.service.ts create mode 100644 client/src/root-helpers/users/oauth-user-tokens.ts delete mode 100644 client/src/root-helpers/users/user-tokens.ts (limited to 'client') diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts index 9ae6f9f12..b3818c8de 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.ts +++ b/client/src/app/+videos/+video-watch/video-watch.component.ts @@ -20,12 +20,12 @@ import { } from '@app/core' import { HooksService } from '@app/core/plugins/hooks.service' import { isXPercentInViewport, scrollToTop } from '@app/helpers' -import { Video, VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main' +import { Video, VideoCaptionService, VideoDetails, VideoFileTokenService, VideoService } from '@app/shared/shared-main' import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' import { LiveVideoService } from '@app/shared/shared-video-live' import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' import { logger } from '@root-helpers/logger' -import { isP2PEnabled } from '@root-helpers/video' +import { isP2PEnabled, videoRequiresAuth } from '@root-helpers/video' import { timeToInt } from '@shared/core-utils' import { HTMLServerConfig, @@ -78,6 +78,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy { private nextVideoUUID = '' private nextVideoTitle = '' + private videoFileToken: string + private currentTime: number private paramsSub: Subscription @@ -110,6 +112,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { private pluginService: PluginService, private peertubeSocket: PeerTubeSocket, private screenService: ScreenService, + private videoFileTokenService: VideoFileTokenService, private location: PlatformLocation, @Inject(LOCALE_ID) private localeId: string ) { } @@ -252,12 +255,19 @@ export class VideoWatchComponent implements OnInit, OnDestroy { 'filter:api.video-watch.video.get.result' ) - const videoAndLiveObs: Observable<{ video: VideoDetails, live?: LiveVideo }> = videoObs.pipe( + const videoAndLiveObs: Observable<{ video: VideoDetails, live?: LiveVideo, videoFileToken?: string }> = videoObs.pipe( switchMap(video => { - if (!video.isLive) return of({ video }) + if (!video.isLive) return of({ video, live: undefined }) return this.liveVideoService.getVideoLive(video.uuid) .pipe(map(live => ({ live, video }))) + }), + + switchMap(({ video, live }) => { + if (!videoRequiresAuth(video)) return of({ video, live, videoFileToken: undefined }) + + return this.videoFileTokenService.getVideoFileToken(video.uuid) + .pipe(map(({ token }) => ({ video, live, videoFileToken: token }))) }) ) @@ -266,7 +276,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { this.videoCaptionService.listCaptions(videoId), this.userService.getAnonymousOrLoggedUser() ]).subscribe({ - next: ([ { video, live }, captionsResult, loggedInOrAnonymousUser ]) => { + next: ([ { video, live, videoFileToken }, captionsResult, loggedInOrAnonymousUser ]) => { const queryParams = this.route.snapshot.queryParams const urlOptions = { @@ -283,7 +293,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { peertubeLink: false } - this.onVideoFetched({ video, live, videoCaptions: captionsResult.data, loggedInOrAnonymousUser, urlOptions }) + this.onVideoFetched({ video, live, videoCaptions: captionsResult.data, videoFileToken, loggedInOrAnonymousUser, urlOptions }) .catch(err => this.handleGlobalError(err)) }, @@ -356,16 +366,19 @@ export class VideoWatchComponent implements OnInit, OnDestroy { video: VideoDetails live: LiveVideo videoCaptions: VideoCaption[] + videoFileToken: string + urlOptions: URLOptions loggedInOrAnonymousUser: User }) { - const { video, live, videoCaptions, urlOptions, loggedInOrAnonymousUser } = options + const { video, live, videoCaptions, urlOptions, videoFileToken, loggedInOrAnonymousUser } = options this.subscribeToLiveEventsIfNeeded(this.video, video) this.video = video this.videoCaptions = videoCaptions this.liveVideo = live + this.videoFileToken = videoFileToken // Re init attributes this.playerPlaceholderImgSrc = undefined @@ -414,6 +427,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { video: this.video, videoCaptions: this.videoCaptions, liveVideo: this.liveVideo, + videoFileToken: this.videoFileToken, urlOptions, loggedInOrAnonymousUser, user: this.user @@ -561,11 +575,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy { video: VideoDetails liveVideo: LiveVideo videoCaptions: VideoCaption[] + + videoFileToken: string + urlOptions: CustomizationOptions & { playerMode: PlayerMode } + loggedInOrAnonymousUser: User user?: AuthUser // Keep for plugins }) { - const { video, liveVideo, videoCaptions, urlOptions, loggedInOrAnonymousUser } = params + const { video, liveVideo, videoCaptions, videoFileToken, urlOptions, loggedInOrAnonymousUser } = params const getStartTime = () => { const byUrl = urlOptions.startTime !== undefined @@ -623,13 +641,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { theaterButton: true, captions: videoCaptions.length !== 0, - videoViewUrl: video.privacy.id !== VideoPrivacy.PRIVATE - ? this.videoService.getVideoViewUrl(video.uuid) - : null, - authorizationHeader: this.authService.getRequestHeaderValue(), - - metricsUrl: environment.apiUrl + '/api/v1/metrics/playback', - embedUrl: video.embedUrl, embedTitle: video.name, instanceName: this.serverConfig.instance.name, @@ -639,7 +650,17 @@ export class VideoWatchComponent implements OnInit, OnDestroy { language: this.localeId, - serverUrl: environment.apiUrl, + metricsUrl: environment.apiUrl + '/api/v1/metrics/playback', + + videoViewUrl: video.privacy.id !== VideoPrivacy.PRIVATE + ? this.videoService.getVideoViewUrl(video.uuid) + : null, + authorizationHeader: () => this.authService.getRequestHeaderValue(), + + serverUrl: environment.originServerUrl, + + videoFileToken: () => videoFileToken, + requiresAuth: videoRequiresAuth(video), videoCaptions: playerCaptions, diff --git a/client/src/app/core/auth/auth-user.model.ts b/client/src/app/core/auth/auth-user.model.ts index cd9665e37..a12325421 100644 --- a/client/src/app/core/auth/auth-user.model.ts +++ b/client/src/app/core/auth/auth-user.model.ts @@ -1,7 +1,7 @@ import { Observable, of } from 'rxjs' import { map } from 'rxjs/operators' import { User } from '@app/core/users/user.model' -import { UserTokens } from '@root-helpers/users' +import { OAuthUserTokens } from '@root-helpers/users' import { hasUserRight } from '@shared/core-utils/users' import { MyUser as ServerMyUserModel, @@ -13,33 +13,33 @@ import { } from '@shared/models' export class AuthUser extends User implements ServerMyUserModel { - tokens: UserTokens + oauthTokens: OAuthUserTokens specialPlaylists: MyUserSpecialPlaylist[] canSeeVideosLink = true - constructor (userHash: Partial, hashTokens: Partial) { + constructor (userHash: Partial, hashTokens: Partial) { super(userHash) - this.tokens = new UserTokens(hashTokens) + this.oauthTokens = new OAuthUserTokens(hashTokens) this.specialPlaylists = userHash.specialPlaylists } getAccessToken () { - return this.tokens.accessToken + return this.oauthTokens.accessToken } getRefreshToken () { - return this.tokens.refreshToken + return this.oauthTokens.refreshToken } getTokenType () { - return this.tokens.tokenType + return this.oauthTokens.tokenType } refreshTokens (accessToken: string, refreshToken: string) { - this.tokens.accessToken = accessToken - this.tokens.refreshToken = refreshToken + this.oauthTokens.accessToken = accessToken + this.oauthTokens.refreshToken = refreshToken } hasRight (right: UserRight) { diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts index 7f4fae4aa..4de28e51e 100644 --- a/client/src/app/core/auth/auth.service.ts +++ b/client/src/app/core/auth/auth.service.ts @@ -5,7 +5,7 @@ import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular import { Injectable } from '@angular/core' import { Router } from '@angular/router' import { Notifier } from '@app/core/notification/notifier.service' -import { logger, objectToUrlEncoded, peertubeLocalStorage, UserTokens } from '@root-helpers/index' +import { logger, OAuthUserTokens, objectToUrlEncoded, peertubeLocalStorage } from '@root-helpers/index' import { HttpStatusCode, MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models' import { environment } from '../../../environments/environment' import { RestExtractor } from '../rest/rest-extractor.service' @@ -74,7 +74,7 @@ export class AuthService { ] } - buildAuthUser (userInfo: Partial, tokens: UserTokens) { + buildAuthUser (userInfo: Partial, tokens: OAuthUserTokens) { this.user = new AuthUser(userInfo, tokens) } diff --git a/client/src/app/core/users/user-local-storage.service.ts b/client/src/app/core/users/user-local-storage.service.ts index fff649eef..f1588bdd2 100644 --- a/client/src/app/core/users/user-local-storage.service.ts +++ b/client/src/app/core/users/user-local-storage.service.ts @@ -4,7 +4,7 @@ import { Injectable } from '@angular/core' import { AuthService, AuthStatus } from '@app/core/auth' import { getBoolOrDefault } from '@root-helpers/local-storage-utils' import { logger } from '@root-helpers/logger' -import { UserLocalStorageKeys, UserTokens } from '@root-helpers/users' +import { UserLocalStorageKeys, OAuthUserTokens } from '@root-helpers/users' import { UserRole, UserUpdateMe } from '@shared/models' import { NSFWPolicyType } from '@shared/models/videos' import { ServerService } from '../server' @@ -24,7 +24,7 @@ export class UserLocalStorageService { this.setLoggedInUser(user) this.setUserInfo(user) - this.setTokens(user.tokens) + this.setTokens(user.oauthTokens) } }) @@ -43,7 +43,7 @@ export class UserLocalStorageService { next: () => { const user = this.authService.getUser() - this.setTokens(user.tokens) + this.setTokens(user.oauthTokens) } }) } @@ -174,14 +174,14 @@ export class UserLocalStorageService { // --------------------------------------------------------------------------- getTokens () { - return UserTokens.getUserTokens(this.localStorageService) + return OAuthUserTokens.getUserTokens(this.localStorageService) } - setTokens (tokens: UserTokens) { - UserTokens.saveToLocalStorage(this.localStorageService, tokens) + setTokens (tokens: OAuthUserTokens) { + OAuthUserTokens.saveToLocalStorage(this.localStorageService, tokens) } flushTokens () { - UserTokens.flushLocalStorage(this.localStorageService) + OAuthUserTokens.flushLocalStorage(this.localStorageService) } } diff --git a/client/src/app/helpers/utils/url.ts b/client/src/app/helpers/utils/url.ts index 08c27e3c1..9e7dc3e6f 100644 --- a/client/src/app/helpers/utils/url.ts +++ b/client/src/app/helpers/utils/url.ts @@ -54,8 +54,9 @@ function objectToFormData (obj: any, form?: FormData, namespace?: string) { } export { - objectToFormData, getAbsoluteAPIUrl, getAPIHost, - getAbsoluteEmbedUrl + getAbsoluteEmbedUrl, + + objectToFormData } diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts index 04b223cc5..c1523bc50 100644 --- a/client/src/app/shared/shared-main/shared-main.module.ts +++ b/client/src/app/shared/shared-main/shared-main.module.ts @@ -44,7 +44,15 @@ import { import { PluginPlaceholderComponent, PluginSelectorDirective } from './plugins' import { ActorRedirectGuard } from './router' import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' -import { EmbedComponent, RedundancyService, VideoImportService, VideoOwnershipService, VideoResolver, VideoService } from './video' +import { + EmbedComponent, + RedundancyService, + VideoFileTokenService, + VideoImportService, + VideoOwnershipService, + VideoResolver, + VideoService +} from './video' import { VideoCaptionService } from './video-caption' import { VideoChannelService } from './video-channel' @@ -185,6 +193,7 @@ import { VideoChannelService } from './video-channel' VideoImportService, VideoOwnershipService, VideoService, + VideoFileTokenService, VideoResolver, VideoCaptionService, diff --git a/client/src/app/shared/shared-main/video/index.ts b/client/src/app/shared/shared-main/video/index.ts index 361601456..a2e47883e 100644 --- a/client/src/app/shared/shared-main/video/index.ts +++ b/client/src/app/shared/shared-main/video/index.ts @@ -2,6 +2,7 @@ export * from './embed.component' export * from './redundancy.service' export * from './video-details.model' export * from './video-edit.model' +export * from './video-file-token.service' export * from './video-import.service' export * from './video-ownership.service' export * from './video.model' diff --git a/client/src/app/shared/shared-main/video/video-file-token.service.ts b/client/src/app/shared/shared-main/video/video-file-token.service.ts new file mode 100644 index 000000000..791607249 --- /dev/null +++ b/client/src/app/shared/shared-main/video/video-file-token.service.ts @@ -0,0 +1,33 @@ +import { catchError, map, of, tap } from 'rxjs' +import { HttpClient } from '@angular/common/http' +import { Injectable } from '@angular/core' +import { RestExtractor } from '@app/core' +import { VideoToken } from '@shared/models' +import { VideoService } from './video.service' + +@Injectable() +export class VideoFileTokenService { + + private readonly store = new Map() + + constructor ( + private authHttp: HttpClient, + private restExtractor: RestExtractor + ) {} + + getVideoFileToken (videoUUID: string) { + const existing = this.store.get(videoUUID) + if (existing) return of(existing) + + return this.createVideoFileToken(videoUUID) + .pipe(tap(result => this.store.set(videoUUID, { token: result.token, expires: new Date(result.expires) }))) + } + + private createVideoFileToken (videoUUID: string) { + return this.authHttp.post(`${VideoService.BASE_VIDEO_URL}/${videoUUID}/token`, {}) + .pipe( + map(({ files }) => files), + catchError(err => this.restExtractor.handleError(err)) + ) + } +} diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.html b/client/src/app/shared/shared-video-miniature/video-download.component.html index 1c7458b4b..1f622933d 100644 --- a/client/src/app/shared/shared-video-miniature/video-download.component.html +++ b/client/src/app/shared/shared-video-miniature/video-download.component.html @@ -48,10 +48,7 @@ diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.ts b/client/src/app/shared/shared-video-miniature/video-download.component.ts index 47482caaa..667cb107f 100644 --- a/client/src/app/shared/shared-video-miniature/video-download.component.ts +++ b/client/src/app/shared/shared-video-miniature/video-download.component.ts @@ -2,11 +2,12 @@ import { mapValues, pick } from 'lodash-es' import { firstValueFrom } from 'rxjs' import { tap } from 'rxjs/operators' import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core' -import { AuthService, HooksService, Notifier } from '@app/core' +import { HooksService } from '@app/core' import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' import { logger } from '@root-helpers/logger' +import { videoRequiresAuth } from '@root-helpers/video' import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models' -import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoService } from '../shared-main' +import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoFileTokenService, VideoService } from '../shared-main' type DownloadType = 'video' | 'subtitles' type FileMetadata = { [key: string]: { label: string, value: string }} @@ -32,6 +33,8 @@ export class VideoDownloadComponent { type: DownloadType = 'video' + videoFileToken: string + private activeModal: NgbModalRef private bytesPipe: BytesPipe @@ -42,10 +45,9 @@ export class VideoDownloadComponent { constructor ( @Inject(LOCALE_ID) private localeId: string, - private notifier: Notifier, private modalService: NgbModal, private videoService: VideoService, - private auth: AuthService, + private videoFileTokenService: VideoFileTokenService, private hooks: HooksService ) { this.bytesPipe = new BytesPipe() @@ -71,6 +73,8 @@ export class VideoDownloadComponent { } show (video: VideoDetails, videoCaptions?: VideoCaption[]) { + this.videoFileToken = undefined + this.video = video this.videoCaptions = videoCaptions @@ -84,6 +88,11 @@ export class VideoDownloadComponent { this.subtitleLanguageId = this.videoCaptions[0].language.id } + if (videoRequiresAuth(this.video)) { + this.videoFileTokenService.getVideoFileToken(this.video.uuid) + .subscribe(({ token }) => this.videoFileToken = token) + } + this.activeModal.shown.subscribe(() => { this.hooks.runAction('action:modal.video-download.shown', 'common') }) @@ -155,7 +164,7 @@ export class VideoDownloadComponent { if (!file) return '' const suffix = this.isConfidentialVideo() - ? '?access_token=' + this.auth.getAccessToken() + ? '?videoFileToken=' + this.videoFileToken : '' switch (this.downloadType) { diff --git a/client/src/assets/player/shared/common/utils.ts b/client/src/assets/player/shared/common/utils.ts index a010d9184..609240626 100644 --- a/client/src/assets/player/shared/common/utils.ts +++ b/client/src/assets/player/shared/common/utils.ts @@ -52,6 +52,10 @@ function getRtcConfig () { } } +function isSameOrigin (current: string, target: string) { + return new URL(current).origin === new URL(target).origin +} + // --------------------------------------------------------------------------- export { @@ -60,5 +64,7 @@ export { videoFileMaxByResolution, videoFileMinByResolution, - bytes + bytes, + + isSameOrigin } diff --git a/client/src/assets/player/shared/manager-options/hls-options-builder.ts b/client/src/assets/player/shared/manager-options/hls-options-builder.ts index 361c76f4b..933c0d595 100644 --- a/client/src/assets/player/shared/manager-options/hls-options-builder.ts +++ b/client/src/assets/player/shared/manager-options/hls-options-builder.ts @@ -5,7 +5,7 @@ import { LiveVideoLatencyMode } from '@shared/models' import { getAverageBandwidthInStore } from '../../peertube-player-local-storage' import { P2PMediaLoader, P2PMediaLoaderPluginOptions } from '../../types' import { PeertubePlayerManagerOptions } from '../../types/manager-options' -import { getRtcConfig } from '../common' +import { getRtcConfig, isSameOrigin } from '../common' import { RedundancyUrlManager } from '../p2p-media-loader/redundancy-url-manager' import { segmentUrlBuilderFactory } from '../p2p-media-loader/segment-url-builder' import { segmentValidatorFactory } from '../p2p-media-loader/segment-validator' @@ -84,7 +84,21 @@ export class HLSOptionsBuilder { simultaneousHttpDownloads: 1, httpFailedSegmentTimeout: 1000, - segmentValidator: segmentValidatorFactory(this.options.p2pMediaLoader.segmentsSha256Url, this.options.common.isLive), + xhrSetup: (xhr, url) => { + if (!this.options.common.requiresAuth) return + if (!isSameOrigin(this.options.common.serverUrl, url)) return + + xhr.setRequestHeader('Authorization', this.options.common.authorizationHeader()) + }, + + segmentValidator: segmentValidatorFactory({ + segmentsSha256Url: this.options.p2pMediaLoader.segmentsSha256Url, + isLive: this.options.common.isLive, + authorizationHeader: this.options.common.authorizationHeader, + requiresAuth: this.options.common.requiresAuth, + serverUrl: this.options.common.serverUrl + }), + segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager), useP2P: this.options.common.p2pEnabled, diff --git a/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts b/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts index 257cf1e05..b5bdcd4e6 100644 --- a/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts +++ b/client/src/assets/player/shared/manager-options/webtorrent-options-builder.ts @@ -1,4 +1,5 @@ -import { PeertubePlayerManagerOptions } from '../../types' +import { addQueryParams } from '../../../../../../shared/core-utils' +import { PeertubePlayerManagerOptions, WebtorrentPluginOptions } from '../../types' export class WebTorrentOptionsBuilder { @@ -16,13 +17,23 @@ export class WebTorrentOptionsBuilder { const autoplay = this.autoPlayValue === 'play' - const webtorrent = { + const webtorrent: WebtorrentPluginOptions = { autoplay, playerRefusedP2P: commonOptions.p2pEnabled === false, videoDuration: commonOptions.videoDuration, playerElement: commonOptions.playerElement, + videoFileToken: commonOptions.videoFileToken, + + requiresAuth: commonOptions.requiresAuth, + + buildWebSeedUrls: file => { + if (!commonOptions.requiresAuth) return [] + + return [ addQueryParams(file.fileUrl, { videoFileToken: commonOptions.videoFileToken() }) ] + }, + videoFiles: webtorrentOptions.videoFiles.length !== 0 ? webtorrentOptions.videoFiles // The WebTorrent plugin won't be able to play these files, but it will fallback to HTTP mode diff --git a/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts b/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts index 18cb6750f..a7ee91950 100644 --- a/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts +++ b/client/src/assets/player/shared/p2p-media-loader/segment-validator.ts @@ -2,13 +2,22 @@ import { basename } from 'path' import { Segment } from '@peertube/p2p-media-loader-core' import { logger } from '@root-helpers/logger' import { wait } from '@root-helpers/utils' +import { isSameOrigin } from '../common' type SegmentsJSON = { [filename: string]: string | { [byterange: string]: string } } const maxRetries = 3 -function segmentValidatorFactory (segmentsSha256Url: string, isLive: boolean) { - let segmentsJSON = fetchSha256Segments(segmentsSha256Url) +function segmentValidatorFactory (options: { + serverUrl: string + segmentsSha256Url: string + isLive: boolean + authorizationHeader: () => string + requiresAuth: boolean +}) { + const { serverUrl, segmentsSha256Url, isLive, authorizationHeader, requiresAuth } = options + + let segmentsJSON = fetchSha256Segments({ serverUrl, segmentsSha256Url, authorizationHeader, requiresAuth }) const regex = /bytes=(\d+)-(\d+)/ return async function segmentValidator (segment: Segment, _method: string, _peerId: string, retry = 1) { @@ -28,7 +37,7 @@ function segmentValidatorFactory (segmentsSha256Url: string, isLive: boolean) { await wait(1000) - segmentsJSON = fetchSha256Segments(segmentsSha256Url) + segmentsJSON = fetchSha256Segments({ serverUrl, segmentsSha256Url, authorizationHeader, requiresAuth }) await segmentValidator(segment, _method, _peerId, retry + 1) return @@ -68,8 +77,19 @@ export { // --------------------------------------------------------------------------- -function fetchSha256Segments (url: string) { - return fetch(url) +function fetchSha256Segments (options: { + serverUrl: string + segmentsSha256Url: string + authorizationHeader: () => string + requiresAuth: boolean +}) { + const { serverUrl, segmentsSha256Url, requiresAuth, authorizationHeader } = options + + const headers = requiresAuth && isSameOrigin(serverUrl, segmentsSha256Url) + ? { Authorization: authorizationHeader() } + : {} + + return fetch(segmentsSha256Url, { headers }) .then(res => res.json() as Promise) .catch(err => { logger.error('Cannot get sha256 segments', err) diff --git a/client/src/assets/player/shared/peertube/peertube-plugin.ts b/client/src/assets/player/shared/peertube/peertube-plugin.ts index a5d712d70..4bd038bb1 100644 --- a/client/src/assets/player/shared/peertube/peertube-plugin.ts +++ b/client/src/assets/player/shared/peertube/peertube-plugin.ts @@ -22,7 +22,7 @@ const Plugin = videojs.getPlugin('plugin') class PeerTubePlugin extends Plugin { private readonly videoViewUrl: string - private readonly authorizationHeader: string + private readonly authorizationHeader: () => string private readonly videoUUID: string private readonly startTime: number @@ -228,7 +228,7 @@ class PeerTubePlugin extends Plugin { 'Content-type': 'application/json; charset=UTF-8' }) - if (this.authorizationHeader) headers.set('Authorization', this.authorizationHeader) + if (this.authorizationHeader) headers.set('Authorization', this.authorizationHeader()) return fetch(this.videoViewUrl, { method: 'POST', body: JSON.stringify(body), headers }) } diff --git a/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts b/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts index fa3f48a9a..658b7c867 100644 --- a/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts +++ b/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts @@ -2,7 +2,7 @@ import videojs from 'video.js' import * as WebTorrent from 'webtorrent' import { logger } from '@root-helpers/logger' import { isIOS } from '@root-helpers/web-browser' -import { timeToInt } from '@shared/core-utils' +import { addQueryParams, timeToInt } from '@shared/core-utils' import { VideoFile } from '@shared/models' import { getAverageBandwidthInStore, getStoredMute, getStoredVolume, saveAverageBandwidth } from '../../peertube-player-local-storage' import { PeerTubeResolution, PlayerNetworkInfo, WebtorrentPluginOptions } from '../../types' @@ -38,6 +38,8 @@ class WebTorrentPlugin extends Plugin { BANDWIDTH_AVERAGE_NUMBER_OF_VALUES: 5 // Last 5 seconds to build average bandwidth } + private readonly buildWebSeedUrls: (file: VideoFile) => string[] + private readonly webtorrent = new WebTorrent({ tracker: { rtcConfig: getRtcConfig() @@ -57,6 +59,9 @@ class WebTorrentPlugin extends Plugin { private isAutoResolutionObservation = false private playerRefusedP2P = false + private requiresAuth: boolean + private videoFileToken: () => string + private torrentInfoInterval: any private autoQualityInterval: any private addTorrentDelay: any @@ -81,6 +86,11 @@ class WebTorrentPlugin extends Plugin { this.savePlayerSrcFunction = this.player.src this.playerElement = options.playerElement + this.requiresAuth = options.requiresAuth + this.videoFileToken = options.videoFileToken + + this.buildWebSeedUrls = options.buildWebSeedUrls + this.player.ready(() => { const playerOptions = this.player.options_ @@ -268,7 +278,8 @@ class WebTorrentPlugin extends Plugin { return new CacheChunkStore(new PeertubeChunkStore(chunkLength, storeOpts), { max: 100 }) - } + }, + urlList: this.buildWebSeedUrls(this.currentVideoFile) } this.torrent = this.webtorrent.add(magnetOrTorrentUrl, torrentOptions, torrent => { @@ -533,7 +544,12 @@ class WebTorrentPlugin extends Plugin { // Enable error display now this is our last fallback this.player.one('error', () => this.player.peertube().displayFatalError()) - const httpUrl = this.currentVideoFile.fileUrl + let httpUrl = this.currentVideoFile.fileUrl + + if (this.requiresAuth && this.videoFileToken) { + httpUrl = addQueryParams(httpUrl, { videoFileToken: this.videoFileToken() }) + } + this.player.src = this.savePlayerSrcFunction this.player.src(httpUrl) diff --git a/client/src/assets/player/types/manager-options.ts b/client/src/assets/player/types/manager-options.ts index b4d9374c3..9da8fedf8 100644 --- a/client/src/assets/player/types/manager-options.ts +++ b/client/src/assets/player/types/manager-options.ts @@ -57,7 +57,7 @@ export interface CommonOptions extends CustomizationOptions { captions: boolean videoViewUrl: string - authorizationHeader?: string + authorizationHeader?: () => string metricsUrl: string @@ -77,6 +77,8 @@ export interface CommonOptions extends CustomizationOptions { videoShortUUID: string serverUrl: string + requiresAuth: boolean + videoFileToken: () => string errorNotifier: (message: string) => void } diff --git a/client/src/assets/player/types/peertube-videojs-typings.ts b/client/src/assets/player/types/peertube-videojs-typings.ts index 6df94992c..037c4b74b 100644 --- a/client/src/assets/player/types/peertube-videojs-typings.ts +++ b/client/src/assets/player/types/peertube-videojs-typings.ts @@ -95,7 +95,7 @@ type PeerTubePluginOptions = { videoDuration: number videoViewUrl: string - authorizationHeader?: string + authorizationHeader?: () => string subtitle?: string @@ -151,6 +151,11 @@ type WebtorrentPluginOptions = { startTime: number | string playerRefusedP2P: boolean + + requiresAuth: boolean + videoFileToken: () => string + + buildWebSeedUrls: (file: VideoFile) => string[] } type P2PMediaLoaderPluginOptions = { diff --git a/client/src/root-helpers/logger.ts b/client/src/root-helpers/logger.ts index 0d486c433..d1fdf73aa 100644 --- a/client/src/root-helpers/logger.ts +++ b/client/src/root-helpers/logger.ts @@ -1,6 +1,6 @@ import { ClientLogCreate } from '@shared/models/server' import { peertubeLocalStorage } from './peertube-web-storage' -import { UserTokens } from './users' +import { OAuthUserTokens } from './users' export type LoggerHook = (message: LoggerMessage, meta?: LoggerMeta) => void export type LoggerLevel = 'info' | 'warn' | 'error' @@ -56,7 +56,7 @@ class Logger { }) try { - const tokens = UserTokens.getUserTokens(peertubeLocalStorage) + const tokens = OAuthUserTokens.getUserTokens(peertubeLocalStorage) if (tokens) headers.set('Authorization', `${tokens.tokenType} ${tokens.accessToken}`) } catch (err) { diff --git a/client/src/root-helpers/users/index.ts b/client/src/root-helpers/users/index.ts index 2b11d0b7e..c03e67325 100644 --- a/client/src/root-helpers/users/index.ts +++ b/client/src/root-helpers/users/index.ts @@ -1,2 +1,2 @@ export * from './user-local-storage-keys' -export * from './user-tokens' +export * from './oauth-user-tokens' diff --git a/client/src/root-helpers/users/oauth-user-tokens.ts b/client/src/root-helpers/users/oauth-user-tokens.ts new file mode 100644 index 000000000..a24e76b91 --- /dev/null +++ b/client/src/root-helpers/users/oauth-user-tokens.ts @@ -0,0 +1,46 @@ +import { UserTokenLocalStorageKeys } from './user-local-storage-keys' + +export class OAuthUserTokens { + accessToken: string + refreshToken: string + tokenType: string + + constructor (hash?: Partial) { + if (hash) { + this.accessToken = hash.accessToken + this.refreshToken = hash.refreshToken + + if (hash.tokenType === 'bearer') { + this.tokenType = 'Bearer' + } else { + this.tokenType = hash.tokenType + } + } + } + + static getUserTokens (localStorage: Pick) { + const accessTokenLocalStorage = localStorage.getItem(UserTokenLocalStorageKeys.ACCESS_TOKEN) + const refreshTokenLocalStorage = localStorage.getItem(UserTokenLocalStorageKeys.REFRESH_TOKEN) + const tokenTypeLocalStorage = localStorage.getItem(UserTokenLocalStorageKeys.TOKEN_TYPE) + + if (!accessTokenLocalStorage || !refreshTokenLocalStorage || !tokenTypeLocalStorage) return null + + return new OAuthUserTokens({ + accessToken: accessTokenLocalStorage, + refreshToken: refreshTokenLocalStorage, + tokenType: tokenTypeLocalStorage + }) + } + + static saveToLocalStorage (localStorage: Pick, tokens: OAuthUserTokens) { + localStorage.setItem(UserTokenLocalStorageKeys.ACCESS_TOKEN, tokens.accessToken) + localStorage.setItem(UserTokenLocalStorageKeys.REFRESH_TOKEN, tokens.refreshToken) + localStorage.setItem(UserTokenLocalStorageKeys.TOKEN_TYPE, tokens.tokenType) + } + + static flushLocalStorage (localStorage: Pick) { + localStorage.removeItem(UserTokenLocalStorageKeys.ACCESS_TOKEN) + localStorage.removeItem(UserTokenLocalStorageKeys.REFRESH_TOKEN) + localStorage.removeItem(UserTokenLocalStorageKeys.TOKEN_TYPE) + } +} diff --git a/client/src/root-helpers/users/user-tokens.ts b/client/src/root-helpers/users/user-tokens.ts deleted file mode 100644 index a6d614cb7..000000000 --- a/client/src/root-helpers/users/user-tokens.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { UserTokenLocalStorageKeys } from './user-local-storage-keys' - -export class UserTokens { - accessToken: string - refreshToken: string - tokenType: string - - constructor (hash?: Partial) { - if (hash) { - this.accessToken = hash.accessToken - this.refreshToken = hash.refreshToken - - if (hash.tokenType === 'bearer') { - this.tokenType = 'Bearer' - } else { - this.tokenType = hash.tokenType - } - } - } - - static getUserTokens (localStorage: Pick) { - const accessTokenLocalStorage = localStorage.getItem(UserTokenLocalStorageKeys.ACCESS_TOKEN) - const refreshTokenLocalStorage = localStorage.getItem(UserTokenLocalStorageKeys.REFRESH_TOKEN) - const tokenTypeLocalStorage = localStorage.getItem(UserTokenLocalStorageKeys.TOKEN_TYPE) - - if (!accessTokenLocalStorage || !refreshTokenLocalStorage || !tokenTypeLocalStorage) return null - - return new UserTokens({ - accessToken: accessTokenLocalStorage, - refreshToken: refreshTokenLocalStorage, - tokenType: tokenTypeLocalStorage - }) - } - - static saveToLocalStorage (localStorage: Pick, tokens: UserTokens) { - localStorage.setItem(UserTokenLocalStorageKeys.ACCESS_TOKEN, tokens.accessToken) - localStorage.setItem(UserTokenLocalStorageKeys.REFRESH_TOKEN, tokens.refreshToken) - localStorage.setItem(UserTokenLocalStorageKeys.TOKEN_TYPE, tokens.tokenType) - } - - static flushLocalStorage (localStorage: Pick) { - localStorage.removeItem(UserTokenLocalStorageKeys.ACCESS_TOKEN) - localStorage.removeItem(UserTokenLocalStorageKeys.REFRESH_TOKEN) - localStorage.removeItem(UserTokenLocalStorageKeys.TOKEN_TYPE) - } -} diff --git a/client/src/root-helpers/video.ts b/client/src/root-helpers/video.ts index ba84e49ea..107ba1eba 100644 --- a/client/src/root-helpers/video.ts +++ b/client/src/root-helpers/video.ts @@ -1,4 +1,4 @@ -import { HTMLServerConfig, Video } from '@shared/models' +import { HTMLServerConfig, Video, VideoPrivacy } from '@shared/models' function buildVideoOrPlaylistEmbed (options: { embedUrl: string @@ -26,9 +26,14 @@ function isP2PEnabled (video: Video, config: HTMLServerConfig, userP2PEnabled: b return userP2PEnabled } +function videoRequiresAuth (video: Video) { + return new Set([ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL ]).has(video.privacy.id) +} + export { buildVideoOrPlaylistEmbed, - isP2PEnabled + isP2PEnabled, + videoRequiresAuth } // --------------------------------------------------------------------------- diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts index 451e54840..c6160151a 100644 --- a/client/src/standalone/videos/embed.ts +++ b/client/src/standalone/videos/embed.ts @@ -6,7 +6,7 @@ import { peertubeTranslate } from '../../../../shared/core-utils/i18n' import { HTMLServerConfig, LiveVideo, ResultList, VideoDetails, VideoPlaylist, VideoPlaylistElement } from '../../../../shared/models' import { PeertubePlayerManager } from '../../assets/player' import { TranslationsManager } from '../../assets/player/translations-manager' -import { getParamString, logger } from '../../root-helpers' +import { getParamString, logger, videoRequiresAuth } from '../../root-helpers' import { PeerTubeEmbedApi } from './embed-api' import { AuthHTTP, LiveManager, PeerTubePlugin, PlayerManagerOptions, PlaylistFetcher, PlaylistTracker, VideoFetcher } from './shared' import { PlayerHTML } from './shared/player-html' @@ -167,22 +167,25 @@ export class PeerTubeEmbed { private async buildVideoPlayer (videoResponse: Response, captionsPromise: Promise) { const alreadyHadPlayer = this.resetPlayerElement() - const videoInfoPromise: Promise<{ video: VideoDetails, live?: LiveVideo }> = videoResponse.json() - .then((videoInfo: VideoDetails) => { + const videoInfoPromise = videoResponse.json() + .then(async (videoInfo: VideoDetails) => { this.playerManagerOptions.loadParams(this.config, videoInfo) if (!alreadyHadPlayer && !this.playerManagerOptions.hasAutoplay()) { this.playerHTML.buildPlaceholder(videoInfo) } + const live = videoInfo.isLive + ? await this.videoFetcher.loadLive(videoInfo) + : undefined - if (!videoInfo.isLive) { - return { video: videoInfo } - } + const videoFileToken = videoRequiresAuth(videoInfo) + ? await this.videoFetcher.loadVideoToken(videoInfo) + : undefined - return this.videoFetcher.loadVideoWithLive(videoInfo) + return { live, video: videoInfo, videoFileToken } }) - const [ { video, live }, translations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([ + const [ { video, live, videoFileToken }, translations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([ videoInfoPromise, this.translationsPromise, captionsPromise, @@ -200,6 +203,9 @@ export class PeerTubeEmbed { translations, serverConfig: this.config, + authorizationHeader: () => this.http.getHeaderTokenValue(), + videoFileToken: () => videoFileToken, + onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer(uuid), playlistTracker: this.playlistTracker, diff --git a/client/src/standalone/videos/shared/auth-http.ts b/client/src/standalone/videos/shared/auth-http.ts index 0356ab8a6..43af5dff4 100644 --- a/client/src/standalone/videos/shared/auth-http.ts +++ b/client/src/standalone/videos/shared/auth-http.ts @@ -1,5 +1,5 @@ import { HttpStatusCode, OAuth2ErrorCode, UserRefreshToken } from '../../../../../shared/models' -import { objectToUrlEncoded, UserTokens } from '../../../root-helpers' +import { OAuthUserTokens, objectToUrlEncoded } from '../../../root-helpers' import { peertubeLocalStorage } from '../../../root-helpers/peertube-web-storage' export class AuthHTTP { @@ -8,30 +8,30 @@ export class AuthHTTP { CLIENT_SECRET: 'client_secret' } - private userTokens: UserTokens + private userOAuthTokens: OAuthUserTokens private headers = new Headers() constructor () { - this.userTokens = UserTokens.getUserTokens(peertubeLocalStorage) + this.userOAuthTokens = OAuthUserTokens.getUserTokens(peertubeLocalStorage) - if (this.userTokens) this.setHeadersFromTokens() + if (this.userOAuthTokens) this.setHeadersFromTokens() } - fetch (url: string, { optionalAuth }: { optionalAuth: boolean }) { + fetch (url: string, { optionalAuth, method }: { optionalAuth: boolean, method?: string }) { const refreshFetchOptions = optionalAuth ? { headers: this.headers } : {} - return this.refreshFetch(url.toString(), refreshFetchOptions) + return this.refreshFetch(url.toString(), { ...refreshFetchOptions, method }) } getHeaderTokenValue () { - return `${this.userTokens.tokenType} ${this.userTokens.accessToken}` + return `${this.userOAuthTokens.tokenType} ${this.userOAuthTokens.accessToken}` } isLoggedIn () { - return !!this.userTokens + return !!this.userOAuthTokens } private refreshFetch (url: string, options?: RequestInit) { @@ -47,7 +47,7 @@ export class AuthHTTP { headers.set('Content-Type', 'application/x-www-form-urlencoded') const data = { - refresh_token: this.userTokens.refreshToken, + refresh_token: this.userOAuthTokens.refreshToken, client_id: clientId, client_secret: clientSecret, response_type: 'code', @@ -64,15 +64,15 @@ export class AuthHTTP { return res.json() }).then((obj: UserRefreshToken & { code?: OAuth2ErrorCode }) => { if (!obj || obj.code === OAuth2ErrorCode.INVALID_GRANT) { - UserTokens.flushLocalStorage(peertubeLocalStorage) + OAuthUserTokens.flushLocalStorage(peertubeLocalStorage) this.removeTokensFromHeaders() return resolve() } - this.userTokens.accessToken = obj.access_token - this.userTokens.refreshToken = obj.refresh_token - UserTokens.saveToLocalStorage(peertubeLocalStorage, this.userTokens) + this.userOAuthTokens.accessToken = obj.access_token + this.userOAuthTokens.refreshToken = obj.refresh_token + OAuthUserTokens.saveToLocalStorage(peertubeLocalStorage, this.userOAuthTokens) this.setHeadersFromTokens() @@ -84,7 +84,7 @@ export class AuthHTTP { return refreshingTokenPromise .catch(() => { - UserTokens.flushLocalStorage(peertubeLocalStorage) + OAuthUserTokens.flushLocalStorage(peertubeLocalStorage) this.removeTokensFromHeaders() }).then(() => fetch(url, { diff --git a/client/src/standalone/videos/shared/player-manager-options.ts b/client/src/standalone/videos/shared/player-manager-options.ts index eed821994..87a84975b 100644 --- a/client/src/standalone/videos/shared/player-manager-options.ts +++ b/client/src/standalone/videos/shared/player-manager-options.ts @@ -17,7 +17,8 @@ import { isP2PEnabled, logger, peertubeLocalStorage, - UserLocalStorageKeys + UserLocalStorageKeys, + videoRequiresAuth } from '../../../root-helpers' import { PeerTubePlugin } from './peertube-plugin' import { PlayerHTML } from './player-html' @@ -154,6 +155,9 @@ export class PlayerManagerOptions { captionsResponse: Response live?: LiveVideo + authorizationHeader: () => string + videoFileToken: () => string + serverConfig: HTMLServerConfig alreadyHadPlayer: boolean @@ -169,9 +173,11 @@ export class PlayerManagerOptions { video, captionsResponse, alreadyHadPlayer, + videoFileToken, translations, playlistTracker, live, + authorizationHeader, serverConfig } = options @@ -227,6 +233,10 @@ export class PlayerManagerOptions { embedUrl: window.location.origin + video.embedPath, embedTitle: video.name, + requiresAuth: videoRequiresAuth(video), + authorizationHeader, + videoFileToken, + errorNotifier: () => { // Empty, we don't have a notifier in the embed }, diff --git a/client/src/standalone/videos/shared/video-fetcher.ts b/client/src/standalone/videos/shared/video-fetcher.ts index b42d622f9..cf6d12831 100644 --- a/client/src/standalone/videos/shared/video-fetcher.ts +++ b/client/src/standalone/videos/shared/video-fetcher.ts @@ -1,4 +1,4 @@ -import { HttpStatusCode, LiveVideo, VideoDetails } from '../../../../../shared/models' +import { HttpStatusCode, LiveVideo, VideoDetails, VideoToken } from '../../../../../shared/models' import { logger } from '../../../root-helpers' import { AuthHTTP } from './auth-http' @@ -36,10 +36,15 @@ export class VideoFetcher { return { captionsPromise, videoResponse } } - loadVideoWithLive (video: VideoDetails) { + loadLive (video: VideoDetails) { return this.http.fetch(this.getLiveUrl(video.uuid), { optionalAuth: true }) - .then(res => res.json()) - .then((live: LiveVideo) => ({ video, live })) + .then(res => res.json() as Promise) + } + + loadVideoToken (video: VideoDetails) { + return this.http.fetch(this.getVideoTokenUrl(video.uuid), { optionalAuth: true, method: 'POST' }) + .then(res => res.json() as Promise) + .then(token => token.files.token) } getVideoViewsUrl (videoUUID: string) { @@ -61,4 +66,8 @@ export class VideoFetcher { private getLiveUrl (videoId: string) { return window.location.origin + '/api/v1/videos/live/' + videoId } + + private getVideoTokenUrl (id: string) { + return this.getVideoUrl(id) + '/token' + } } -- cgit v1.2.3