From ac8278a00647684190e04fdfaf2b683160b0e13a Mon Sep 17 00:00:00 2001 From: giakas Date: Mon, 10 Jan 2022 13:02:03 -0800 Subject: [PATCH] switch to query param based Auth (#103) * switch to query param based Auth * remove some changes commited by mistake. * fixed tslint issues * change the dash to authorization header * remove credentials headers --- .../common/services/auth/ava-api.class.ts | 6 +- .../common/services/media/media-api.class.ts | 55 +++++---- .../services/media/media.definitions.ts | 5 + .../src/player-component/player-component.ts | 108 ++++++------------ .../src/player-component/player.class.ts | 42 +++---- packages/widgets/src/player/player-widget.ts | 13 +-- 6 files changed, 96 insertions(+), 133 deletions(-) diff --git a/packages/common/services/auth/ava-api.class.ts b/packages/common/services/auth/ava-api.class.ts index bb3c73c..e9aa580 100644 --- a/packages/common/services/auth/ava-api.class.ts +++ b/packages/common/services/auth/ava-api.class.ts @@ -1,4 +1,6 @@ import { WidgetGeneralError } from '../../../widgets/src'; +import { MediaApi } from '../media/media-api.class'; +import { VideoContentToken } from '../media/media.definitions'; import { TokenHandler } from './token-handler.class'; export class AvaAPi { @@ -29,14 +31,14 @@ export class AvaAPi { headers['Authorization'] = `Bearer ${TokenHandler.avaAPIToken}`; const response = await fetch(url, { - credentials: 'include', method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${TokenHandler.avaAPIToken}` } }); - const data = await response.json(); + const data: VideoContentToken = await response.json(); + MediaApi.contentToken = data.Token; if (!data.ExpirationDate) { throw new WidgetGeneralError('Invalid cookie expiration'); } diff --git a/packages/common/services/media/media-api.class.ts b/packages/common/services/media/media-api.class.ts index cb25c1d..f1be9ac 100644 --- a/packages/common/services/media/media-api.class.ts +++ b/packages/common/services/media/media-api.class.ts @@ -5,6 +5,7 @@ export class MediaApi { private static _rtspStream: string | undefined; private static _format = MediaApi.supportsMediaSource() ? VideoFormat.DASH : VideoFormat.HLS; private static _videoEntity: VideoEntity; + private static _contentToken: string; public static supportsMediaSource(): boolean { return !!window.MediaSource; @@ -16,6 +17,14 @@ export class MediaApi { this._rtspStream = this._videoEntity?.properties?.contentUrls?.rtspTunnelUrl; } + public static set contentToken(value: string) { + this._contentToken = value; + } + + public static get contentToken() { + return this._contentToken; + } + public static isApple(): boolean { return !!navigator.vendor && navigator.vendor.includes('Apple'); } @@ -37,18 +46,21 @@ export class MediaApi { } public static get rtspStream() { - return this._rtspStream; + return this.addTokenQueryParam(this._rtspStream, 'authorization'); } public static get liveStream(): string { // if RTSP is present use RTSP URL. + let liveUrl = ''; if (this._rtspStream && this.supportsMediaSource()) { - return this._rtspStream; - } - const format = MediaApi._format === VideoFormat.HLS ? 'm3u8-cmaf' : 'mpd-time-cmaf'; - const extension = MediaApi._format === VideoFormat.HLS ? '.m3u8' : '.mpd'; + liveUrl = this.rtspStream; + } else { + const format = MediaApi._format === VideoFormat.HLS ? 'm3u8-cmaf' : 'mpd-time-cmaf'; + const extension = MediaApi._format === VideoFormat.HLS ? '.m3u8' : '.mpd'; - return `${this.baseStream}/manifest(format=${format})${extension}`; + liveUrl = `${this.baseStream}/manifest(format=${format})${extension}`; + } + return MediaApi._format === VideoFormat.HLS ? this.addTokenQueryParam(liveUrl) : liveUrl; } public static getVODStream(range: IExpandedTimeRange = null): string { @@ -66,7 +78,8 @@ export class MediaApi { } } - return `${this.baseStream}/manifest(format=${format}${range_query})${extension}`; + const url = `${this.baseStream}/manifest(format=${format}${range_query})${extension}`; + return MediaApi._format === VideoFormat.HLS ? this.addTokenQueryParam(url) : url; } public static getVODStreamForCLip(startTime: Date, endTime: Date): string { @@ -80,15 +93,11 @@ export class MediaApi { range_query = `,starttime=${startTimeISOFormat},endtime=${endTimeISOFormat}`; } - return `${this.baseStream}/manifest(format=${format}${range_query})${extension}`; + const url = `${this.baseStream}/manifest(format=${format}${range_query})${extension}`; + return MediaApi._format === VideoFormat.HLS ? this.addTokenQueryParam(url) : url; } - public static getAvailableMedia( - precision: Precision, - range: IExpandedTimeRange = null, - allowCrossSiteCredentials = true, - token?: string - ): Promise { + public static getAvailableMedia(precision: Precision, range: IExpandedTimeRange = null): Promise { // time ranges are required for month, day and full if ((precision === Precision.MONTH || precision === Precision.DAY || precision === Precision.FULL) && !range) { throw Error('wrong parameters'); @@ -100,17 +109,7 @@ export class MediaApi { const url = `${this.baseStream}/availableMedia${range_query}`; // eslint-disable-next-line no-undef - const requestInit: RequestInit = {}; - - if (allowCrossSiteCredentials) { - requestInit.credentials = 'include'; - } - - if (token) { - requestInit.headers = { - Authorization: `Bearer ${token}` - }; - } + const requestInit: RequestInit = { headers: { Authorization: `Bearer ${this.contentToken}` } }; return fetch(url, requestInit); } @@ -127,4 +126,10 @@ export class MediaApi { return ''; } } + + private static addTokenQueryParam(urlString: string, paramName = 'token') { + const url = new URL(urlString); + url.searchParams.set(paramName, encodeURIComponent(this.contentToken)); + return url.toString(); + } } diff --git a/packages/common/services/media/media.definitions.ts b/packages/common/services/media/media.definitions.ts index f8f109f..28eda19 100644 --- a/packages/common/services/media/media.definitions.ts +++ b/packages/common/services/media/media.definitions.ts @@ -84,3 +84,8 @@ export interface SystemData { lastModifiedByType?: string; lastModifiedAt?: Date; } + +export interface VideoContentToken { + readonly ExpirationDate?: Date; + readonly Token?: string; +} diff --git a/packages/web-components/src/player-component/player-component.ts b/packages/web-components/src/player-component/player-component.ts index c4e28e6..647df1a 100644 --- a/packages/web-components/src/player-component/player-component.ts +++ b/packages/web-components/src/player-component/player-component.ts @@ -83,13 +83,7 @@ export class PlayerComponent extends FASTElement { PlayerWrapper.setDebugMode(debug); } - public async init( - allowCrossSiteCredentials = true, - accessToken?: string, - allowedControllers?: ControlPanelElements[], - clipTimeRange?: IClipTimeRange, - isMuted?: boolean - ) { + public async init(allowedControllers?: ControlPanelElements[], clipTimeRange?: IClipTimeRange, isMuted?: boolean) { if (!this.connected) { return; } @@ -140,10 +134,10 @@ export class PlayerComponent extends FASTElement { this.isMuted = isMuted ?? true; - this.initializePlayer(this.allowedControllers, allowCrossSiteCredentials, accessToken); + this.initializePlayer(this.allowedControllers); } - public async initializePlayer(allowedControllers: ControlPanelElements[], allowCrossSiteCredentials = true, accessToken?: string) { + public async initializePlayer(allowedControllers: ControlPanelElements[]) { if (this.player) { // If there was an existing error -clear state this.clearError(); @@ -166,11 +160,6 @@ export class PlayerComponent extends FASTElement { this.player.addLoading(); - if (accessToken) { - this.player.accessToken = accessToken; - } - this.player.allowCrossCred = allowCrossSiteCredentials; - if (!MediaApi.videoFlags.canStream || (!MediaApi.baseStream && (!MediaApi.videoFlags.isInUse || !MediaApi.liveStream))) { this.hasError = true; this.player.removeLoading(); @@ -199,12 +188,6 @@ export class PlayerComponent extends FASTElement { this.classList.remove('loading'); } - public setPlaybackAuthorization(accessToken: string) { - if (accessToken) { - this.player.accessToken = accessToken; - } - } - public async initializeAvailableMedia(checkForLive: boolean) { await this.fetchAvailableYears(); @@ -399,7 +382,7 @@ export class PlayerComponent extends FASTElement { public switchToDash() { MediaApi.rtspStream = ''; - this.initializePlayer(this.allowedControllers, this.player.allowCrossCred, this.player.accessToken); + this.initializePlayer(this.allowedControllers); } public retryStreaming() { @@ -464,23 +447,18 @@ export class PlayerComponent extends FASTElement { private async fetchAvailableSegments(startDate: IExpandedDate, end: IExpandedDate): Promise { try { - const availableHours = await MediaApi.getAvailableMedia( - Precision.FULL, - { - start: { - year: startDate.year, - month: startDate.month, - day: startDate.day - }, - end: { - year: end.year, - month: end.month, - day: end.day - } + const availableHours = await MediaApi.getAvailableMedia(Precision.FULL, { + start: { + year: startDate.year, + month: startDate.month, + day: startDate.day }, - this.player.allowCrossCred, - this.player.accessToken - ); + end: { + year: end.year, + month: end.month, + day: end.day + } + }); return await availableHours.json(); } catch (error) { @@ -695,7 +673,7 @@ export class PlayerComponent extends FASTElement { } private async fetchAvailableYears() { - const availableYears = await MediaApi.getAvailableMedia(Precision.YEAR, null, this.player.allowCrossCred, this.player.accessToken); + const availableYears = await MediaApi.getAvailableMedia(Precision.YEAR, null); try { const yearRanges: IAvailableMediaResponse = await availableYears.json(); @@ -726,23 +704,18 @@ export class PlayerComponent extends FASTElement { private async fetchAvailableMonths(year: number) { // Take available months according to year try { - const availableMonths = await MediaApi.getAvailableMedia( - Precision.MONTH, - { - start: { - year: year, - month: 1, - day: 1 - }, - end: { - year: year, - month: 12, - day: 1 - } + const availableMonths = await MediaApi.getAvailableMedia(Precision.MONTH, { + start: { + year: year, + month: 1, + day: 1 }, - this.player.allowCrossCred, - this.player.accessToken - ); + end: { + year: year, + month: 12, + day: 1 + } + }); const monthRanges: IAvailableMediaResponse = await availableMonths.json(); // Get last available month @@ -770,23 +743,18 @@ export class PlayerComponent extends FASTElement { const lastDayOfMonth = new Date(year, month, 1, 0, 0, 0); lastDayOfMonth.setDate(lastDayOfMonth.getDate() - 1); // fetch available days - const availableDays = await MediaApi.getAvailableMedia( - Precision.DAY, - { - start: { - year: year, - month: month, - day: 1 - }, - end: { - year: year, - month: month, - day: lastDayOfMonth.getDate() - } + const availableDays = await MediaApi.getAvailableMedia(Precision.DAY, { + start: { + year: year, + month: month, + day: 1 }, - this.player.allowCrossCred, - this.player.accessToken - ); + end: { + year: year, + month: month, + day: lastDayOfMonth.getDate() + } + }); const dayRanges: IAvailableMediaResponse = await availableDays.json(); diff --git a/packages/web-components/src/player-component/player.class.ts b/packages/web-components/src/player-component/player.class.ts index 05324b3..7a6b661 100644 --- a/packages/web-components/src/player-component/player.class.ts +++ b/packages/web-components/src/player-component/player.class.ts @@ -33,11 +33,9 @@ export class PlayerWrapper { private isClip = false; private isLoaded = false; private duringSegmentJump = false; - private _accessToken = ''; private _mimeType: MimeType; // eslint-disable-next-line @typescript-eslint/no-explicit-any private controls: any; - private _allowCrossCred = true; private timestampOffset: number; private firstSegmentStartSeconds: number; private date: Date; @@ -110,9 +108,9 @@ export class PlayerWrapper { if (this._liveStream === MediaApi.rtspStream) { // getVideo API may one day return the RTSP URL, until then, hard-code it. url.searchParams.set('rtsp', encodeURIComponent('rtsp://localhost')); - if (this.accessToken) { - url.searchParams.set('authorization', this.accessToken); - } + // if (MediaApi.contentToken) { + // url.searchParams.set('authorization', MediaApi.contentToken); + // } } return url.toString(); } @@ -123,14 +121,6 @@ export class PlayerWrapper { this._vodStream = value; } - public set allowCrossCred(value: boolean) { - this._allowCrossCred = value; - } - - public get allowCrossCred() { - return this._allowCrossCred; - } - public set mimeType(value: MimeType) { this._mimeType = value; } @@ -154,14 +144,6 @@ export class PlayerWrapper { this.video.play(); } - public set accessToken(accessToken: string) { - this._accessToken = accessToken; - } - - public get accessToken() { - return this._accessToken; - } - public set currentDate(startDate: Date) { this._currentDate = startDate; } @@ -362,10 +344,7 @@ export class PlayerWrapper { this._availableSegments?.timeRanges[this._availableSegments?.timeRanges.length - 1]?.start, this._currentDate ); - const firstSegmentStartSeconds = extractRealTimeFromISO( - this._availableSegments?.timeRanges[0]?.start, - this._currentDate - ); + const firstSegmentStartSeconds = extractRealTimeFromISO(this._availableSegments?.timeRanges[0]?.start, this._currentDate); this.disableNextSegmentButton(this.currentSegment.startSeconds === lastSegmentStartSeconds); this.disablePrevSegmentButton(this.currentSegment.startSeconds === firstSegmentStartSeconds); } @@ -685,13 +664,16 @@ export class PlayerWrapper { } private authenticationHandler(type: shaka_player.net.NetworkingEngine.RequestType, request: shaka_player.extern.Request) { - request['allowCrossSiteCredentials'] = this._allowCrossCred; - if (!this._accessToken) { + if (!MediaApi.contentToken) { return; } - // Add authorization header - request.headers['Authorization'] = `Bearer ${this._accessToken}`; + const url: string = request['uris'][0]; + if (url.indexOf('http') != -1) { + const urlRequest = new URL(url); + urlRequest.searchParams.set('token', MediaApi.contentToken); + request['uris'][0] = urlRequest.toString(); + } } private onShakaMetadata(event: shaka_player.PlayerEvents.EmsgEvent) { @@ -733,6 +715,8 @@ export class PlayerWrapper { private async onTrackChange() { // Get player manifest if (!MediaApi.supportsMediaSource()) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await new Promise((resolve) => setTimeout(resolve, 1000)); // eslint-disable-next-line @typescript-eslint/no-explicit-any const date = (this.video as any).getStartDate(); Logger.log(`video start date is ${date} ${date.getUTCDate()}`); diff --git a/packages/widgets/src/player/player-widget.ts b/packages/widgets/src/player/player-widget.ts index bdda029..7da3883 100644 --- a/packages/widgets/src/player/player-widget.ts +++ b/packages/widgets/src/player/player-widget.ts @@ -88,19 +88,18 @@ export class Player extends BaseWidget { public setSource(source: ISource) { this.source = source; MediaApi.videoEntity = this.source.videoEntity; + MediaApi.contentToken = this.source.authenticationToken; this.setLocalization(this.config?.locale, ['common', 'player']); if (this.loaded) { const playerComponent: PlayerComponent = this.shadowRoot.querySelector('media-player'); playerComponent.cameraName = AvaAPi.videoName; - playerComponent.init(this.source.allowCrossSiteCredentials, this.source.authenticationToken, this.allowedControllers); + playerComponent.init(this.allowedControllers); } } public setPlaybackAuthorization(token: string) { - const playerComponent: PlayerComponent = this.shadowRoot.querySelector('media-player'); - - playerComponent.setPlaybackAuthorization(token); + MediaApi.contentToken = token; } public set apiBase(apiBase: string) { @@ -120,7 +119,7 @@ export class Player extends BaseWidget { // If set source state if (this.source) { playerComponent.cameraName = AvaAPi.videoName; - playerComponent.init(this.source.allowCrossSiteCredentials, this.source.authenticationToken, this.allowedControllers); + playerComponent.init(this.allowedControllers); return; } // Configuration state - work with AVA API @@ -136,7 +135,7 @@ export class Player extends BaseWidget { // Authorize video await AvaAPi.authorize(); playerComponent.cameraName = AvaAPi.videoName; - playerComponent.init(true, '', this.allowedControllers, this.clipTimeRange, this.isMuted); + playerComponent.init(this.allowedControllers, this.clipTimeRange, this.isMuted); } }) .catch((error) => { @@ -172,7 +171,7 @@ export class Player extends BaseWidget { private handelFallback(error: HttpError) { const player: PlayerComponent = this.shadowRoot.querySelector('media-player'); player.cameraName = AvaAPi.videoName; - player.init(true, '', this.allowedControllers); + player.init(this.allowedControllers); player.handleError(error); }