webcomponent: enhance message handling and security in WebComponent communication
This commit is contained in:
parent
7836da8858
commit
459a37bee8
@ -56,6 +56,17 @@ export class CommandsManager {
|
|||||||
this.sendMessage(message);
|
this.sendMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the target origin used when sending messages to the iframe.
|
||||||
|
* This should be called once the iframe URL is known to improve security.
|
||||||
|
*
|
||||||
|
* @param newOrigin - The origin of the content loaded in the iframe
|
||||||
|
* (e.g. 'https://meet.example.com')
|
||||||
|
*/
|
||||||
|
public setTargetOrigin(newOrigin: string): void {
|
||||||
|
this.targetIframeOrigin = newOrigin;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribe to an event
|
* Subscribe to an event
|
||||||
* @param eventName Name of the event to listen for
|
* @param eventName Name of the event to listen for
|
||||||
@ -85,9 +96,7 @@ export class CommandsManager {
|
|||||||
handlers?.add(callback);
|
handlers?.add(callback);
|
||||||
|
|
||||||
// Register with standard DOM API
|
// Register with standard DOM API
|
||||||
|
|
||||||
element.addEventListener(eventName, listener);
|
element.addEventListener(eventName, listener);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,7 +121,6 @@ export class CommandsManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.on(element, eventName, wrapperCallback);
|
this.on(element, eventName, wrapperCallback);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,30 +164,16 @@ export class CommandsManager {
|
|||||||
this.sendMessage(message);
|
this.sendMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// public toggleChat() {
|
|
||||||
// const message: ParentMessage = { action: WebComponentActionType.TOGGLE_CHAT };
|
|
||||||
// this.commandsManager.sendMessage(message);
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the target origin used when sending messages to the iframe.
|
|
||||||
* This should be called once the iframe URL is known to improve security.
|
|
||||||
*
|
|
||||||
* @param newOrigin - The origin of the content loaded in the iframe
|
|
||||||
* (e.g. 'https://meet.example.com')
|
|
||||||
*/
|
|
||||||
public setTargetOrigin(newOrigin: string): void {
|
|
||||||
this.targetIframeOrigin = newOrigin;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a message to the iframe using window.postMessage
|
* Sends a message to the iframe using window.postMessage
|
||||||
*
|
*
|
||||||
* @param message - The message to send to the iframe
|
* @param message - The message to send to the iframe
|
||||||
* @param explicitTargetOrigin - Optional override for the target origin
|
* @param targetOrigin - Optional override for the target origin
|
||||||
*/
|
*/
|
||||||
private sendMessage(message: WebComponentInboundCommandMessage, explicitTargetOrigin?: string): void {
|
private sendMessage(
|
||||||
explicitTargetOrigin = explicitTargetOrigin || this.targetIframeOrigin;
|
message: WebComponentInboundCommandMessage,
|
||||||
this.iframe.contentWindow?.postMessage(message, explicitTargetOrigin);
|
targetOrigin: string = this.targetIframeOrigin
|
||||||
|
): void {
|
||||||
|
this.iframe.contentWindow?.postMessage(message, targetOrigin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,9 +2,22 @@ import { WebComponentOutboundEventMessage } from '../typings/ce/message.type';
|
|||||||
|
|
||||||
export class EventsManager {
|
export class EventsManager {
|
||||||
private element: HTMLElement;
|
private element: HTMLElement;
|
||||||
|
private targetIframeOrigin: string;
|
||||||
|
|
||||||
constructor(element: HTMLElement) {
|
constructor(element: HTMLElement, initialTargetOrigin: string) {
|
||||||
this.element = element;
|
this.element = element;
|
||||||
|
this.targetIframeOrigin = initialTargetOrigin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the target origin used when sending messages to the iframe.
|
||||||
|
* This should be called once the iframe URL is known to improve security.
|
||||||
|
*
|
||||||
|
* @param newOrigin - The origin of the content loaded in the iframe
|
||||||
|
* (e.g. 'https://meet.example.com')
|
||||||
|
*/
|
||||||
|
public setTargetOrigin(newOrigin: string): void {
|
||||||
|
this.targetIframeOrigin = newOrigin;
|
||||||
}
|
}
|
||||||
|
|
||||||
public listen() {
|
public listen() {
|
||||||
@ -18,8 +31,12 @@ export class EventsManager {
|
|||||||
private handleMessage(event: MessageEvent) {
|
private handleMessage(event: MessageEvent) {
|
||||||
const message: WebComponentOutboundEventMessage = event.data;
|
const message: WebComponentOutboundEventMessage = event.data;
|
||||||
// Validate message origin (security measure)
|
// Validate message origin (security measure)
|
||||||
|
if (event.origin !== this.targetIframeOrigin) {
|
||||||
|
console.warn('Message from unknown origin:', event.origin);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!message || !message.event) {
|
if (!message || !message.event) {
|
||||||
// console.warn('Invalid message:', message);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,14 +5,16 @@ import styles from '../assets/css/styles.css';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The `OpenViduMeet` web component provides an interface for embedding an OpenVidu Meet room within a web page.
|
* The `OpenViduMeet` web component provides an interface for embedding an OpenVidu Meet room within a web page.
|
||||||
* It allows for dynamic configuration through attributes and provides methods to interact with the OpenVidu Meet.
|
* It can also be used to view a recording of a meeting.
|
||||||
|
* It allows for dynamic configuration through attributes and provides methods to interact with OpenVidu Meet.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```html
|
* ```html
|
||||||
* <openvidu-meet roomUrl="https://your-openvidu-server.com/room"></openvidu-meet>
|
* <openvidu-meet room-url="https://your-openvidu-server.com/room"></openvidu-meet>
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @attribute roomUrl - The base URL of the OpenVidu Meet room. This attribute is required.
|
* @attribute room-url - The base URL of the OpenVidu Meet room. This attribute is required unless `recording-url` is provided.
|
||||||
|
* @attribute recording-url - The URL of a recording to view. If this is provided, the `room-url` is not required.
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@ -43,7 +45,7 @@ export class OpenViduMeet extends HTMLElement {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.commandsManager = new CommandsManager(this.iframe, this.targetIframeOrigin);
|
this.commandsManager = new CommandsManager(this.iframe, this.targetIframeOrigin);
|
||||||
this.eventsManager = new EventsManager(this);
|
this.eventsManager = new EventsManager(this, this.targetIframeOrigin);
|
||||||
|
|
||||||
// Listen for changes in attributes to update the iframe src
|
// Listen for changes in attributes to update the iframe src
|
||||||
const observer = new MutationObserver(() => this.updateIframeSrc());
|
const observer = new MutationObserver(() => this.updateIframeSrc());
|
||||||
@ -117,7 +119,8 @@ export class OpenViduMeet extends HTMLElement {
|
|||||||
this.loadTimeout = null;
|
this.loadTimeout = null;
|
||||||
this.iframe.onload = null;
|
this.iframe.onload = null;
|
||||||
};
|
};
|
||||||
// this.iframe.onload = this.handleIframeLoaded.bind(this);
|
|
||||||
|
// Handle iframe errors
|
||||||
this.iframe.onerror = (event: Event | string) => {
|
this.iframe.onerror = (event: Event | string) => {
|
||||||
console.error('Iframe error:', event);
|
console.error('Iframe error:', event);
|
||||||
clearTimeout(this.loadTimeout);
|
clearTimeout(this.loadTimeout);
|
||||||
@ -140,6 +143,7 @@ export class OpenViduMeet extends HTMLElement {
|
|||||||
const url = new URL(baseUrl);
|
const url = new URL(baseUrl);
|
||||||
this.targetIframeOrigin = url.origin;
|
this.targetIframeOrigin = url.origin;
|
||||||
this.commandsManager.setTargetOrigin(this.targetIframeOrigin);
|
this.commandsManager.setTargetOrigin(this.targetIframeOrigin);
|
||||||
|
this.eventsManager.setTargetOrigin(this.targetIframeOrigin);
|
||||||
|
|
||||||
// Update query params
|
// Update query params
|
||||||
Array.from(this.attributes).forEach((attr) => {
|
Array.from(this.attributes).forEach((attr) => {
|
||||||
|
|||||||
@ -2,23 +2,21 @@
|
|||||||
* All available commands that can be sent to the WebComponent.
|
* All available commands that can be sent to the WebComponent.
|
||||||
*/
|
*/
|
||||||
export enum WebComponentCommand {
|
export enum WebComponentCommand {
|
||||||
/**
|
/**
|
||||||
* Initializes the WebComponent with the given configuration.
|
* Initializes the WebComponent with the given configuration.
|
||||||
* This command is sent from the webcomponent to the iframe for intialice the domain.
|
* This command is sent from the webcomponent to the iframe for intialice the domain.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
INITIALIZE = 'INITIALIZE',
|
INITIALIZE = 'INITIALIZE',
|
||||||
|
/**
|
||||||
/**
|
* Ends the current meeting for all participants.
|
||||||
* Ends the current meeting for all participants.
|
* This command is only available for the moderator.
|
||||||
* This command is only available for the moderator.
|
*/
|
||||||
*/
|
END_MEETING = 'END_MEETING',
|
||||||
END_MEETING = 'END_MEETING',
|
/**
|
||||||
/**
|
* Disconnects the local participant from the current room.
|
||||||
* Disconnects the local participant from the current room.
|
*/
|
||||||
*/
|
LEAVE_ROOM = 'LEAVE_ROOM'
|
||||||
LEAVE_ROOM = 'LEAVE_ROOM'
|
|
||||||
// TOGGLE_CHAT = 'TOGGLE_CHAT'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -27,16 +25,16 @@ export enum WebComponentCommand {
|
|||||||
* @category Communication
|
* @category Communication
|
||||||
*/
|
*/
|
||||||
export interface WebComponentCommandPayloads {
|
export interface WebComponentCommandPayloads {
|
||||||
/**
|
/**
|
||||||
* Payload for the INITIALIZE command.
|
* Payload for the INITIALIZE command.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
[WebComponentCommand.INITIALIZE]: {
|
[WebComponentCommand.INITIALIZE]: {
|
||||||
domain: string;
|
domain: string;
|
||||||
};
|
};
|
||||||
[WebComponentCommand.END_MEETING]: void;
|
[WebComponentCommand.END_MEETING]: void;
|
||||||
[WebComponentCommand.LEAVE_ROOM]: void;
|
[WebComponentCommand.LEAVE_ROOM]: void;
|
||||||
// [WebComponentCommand.TOGGLE_CHAT]: void;
|
// [WebComponentCommand.TOGGLE_CHAT]: void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -46,5 +44,5 @@ export interface WebComponentCommandPayloads {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
export type WenComponentCommandPayloadFor<T extends WebComponentCommand> = T extends keyof WebComponentCommandPayloads
|
export type WenComponentCommandPayloadFor<T extends WebComponentCommand> = T extends keyof WebComponentCommandPayloads
|
||||||
? WebComponentCommandPayloads[T]
|
? WebComponentCommandPayloads[T]
|
||||||
: never;
|
: never;
|
||||||
|
|||||||
@ -3,23 +3,23 @@
|
|||||||
* @category Communication
|
* @category Communication
|
||||||
*/
|
*/
|
||||||
export enum WebComponentEvent {
|
export enum WebComponentEvent {
|
||||||
/**
|
/**
|
||||||
* Event emitted when application is ready to receive commands.
|
* Event emitted when application is ready to receive commands.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
READY = 'READY',
|
READY = 'READY',
|
||||||
/**
|
/**
|
||||||
* Event emitted when the local participant joins the room.
|
* Event emitted when the local participant joins the room.
|
||||||
*/
|
*/
|
||||||
JOIN = 'JOIN',
|
JOIN = 'JOIN',
|
||||||
/**
|
/**
|
||||||
* Event emitted when the local participant leaves the room.
|
* Event emitted when the local participant leaves the room.
|
||||||
*/
|
*/
|
||||||
LEFT = 'LEFT',
|
LEFT = 'LEFT',
|
||||||
/**
|
/**
|
||||||
* Event emitted when a moderator ends the meeting.
|
* Event emitted when a moderator ends the meeting.
|
||||||
*/
|
*/
|
||||||
MEETING_ENDED = 'MEETING_ENDED'
|
MEETING_ENDED = 'MEETING_ENDED'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,23 +28,23 @@ export enum WebComponentEvent {
|
|||||||
* @category Communication
|
* @category Communication
|
||||||
*/
|
*/
|
||||||
export interface WebComponentEventPayloads {
|
export interface WebComponentEventPayloads {
|
||||||
/**
|
/**
|
||||||
* Payload for the {@link WebComponentEvent.READY} event.
|
* Payload for the {@link WebComponentEvent.READY} event.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
[WebComponentEvent.READY]: {};
|
[WebComponentEvent.READY]: {};
|
||||||
[WebComponentEvent.JOIN]: {
|
[WebComponentEvent.JOIN]: {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
participantName: string;
|
participantName: string;
|
||||||
};
|
};
|
||||||
[WebComponentEvent.LEFT]: {
|
[WebComponentEvent.LEFT]: {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
participantName: string;
|
participantName: string;
|
||||||
reason: string;
|
reason: string;
|
||||||
};
|
};
|
||||||
[WebComponentEvent.MEETING_ENDED]: {
|
[WebComponentEvent.MEETING_ENDED]: {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -53,4 +53,6 @@ export interface WebComponentEventPayloads {
|
|||||||
* @category Type Helpers
|
* @category Type Helpers
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
export type WebComponentEventPayloadFor<T extends WebComponentEvent> = T extends keyof WebComponentEventPayloads ? WebComponentEventPayloads[T] : never;
|
export type WebComponentEventPayloadFor<T extends WebComponentEvent> = T extends keyof WebComponentEventPayloads
|
||||||
|
? WebComponentEventPayloads[T]
|
||||||
|
: never;
|
||||||
|
|||||||
@ -5,7 +5,9 @@ import { WebComponentEventPayloadFor, WebComponentEvent } from './event.model.js
|
|||||||
* Represents all possible messages exchanged between the host application and WebComponent.
|
* Represents all possible messages exchanged between the host application and WebComponent.
|
||||||
* @category Communication
|
* @category Communication
|
||||||
*/
|
*/
|
||||||
export type WebComponentMessage = WebComponentInboundCommandMessage<WebComponentCommand> | WebComponentOutboundEventMessage<WebComponentEvent>;
|
export type WebComponentMessage =
|
||||||
|
| WebComponentInboundCommandMessage<WebComponentCommand>
|
||||||
|
| WebComponentOutboundEventMessage<WebComponentEvent>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Message sent from the host application to the WebComponent.
|
* Message sent from the host application to the WebComponent.
|
||||||
@ -13,10 +15,10 @@ export type WebComponentMessage = WebComponentInboundCommandMessage<WebComponent
|
|||||||
* @category Communication
|
* @category Communication
|
||||||
*/
|
*/
|
||||||
export interface WebComponentInboundCommandMessage<T extends WebComponentCommand = WebComponentCommand> {
|
export interface WebComponentInboundCommandMessage<T extends WebComponentCommand = WebComponentCommand> {
|
||||||
/** The command to execute in the WebComponent */
|
/** The command to execute in the WebComponent */
|
||||||
command: T;
|
command: T;
|
||||||
/** Optional payload with additional data for the command */
|
/** Optional payload with additional data for the command */
|
||||||
payload?: WenComponentCommandPayloadFor<T>;
|
payload?: WenComponentCommandPayloadFor<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -25,10 +27,10 @@ export interface WebComponentInboundCommandMessage<T extends WebComponentCommand
|
|||||||
* @category Communication
|
* @category Communication
|
||||||
*/
|
*/
|
||||||
export interface WebComponentOutboundEventMessage<T extends WebComponentEvent = WebComponentEvent> {
|
export interface WebComponentOutboundEventMessage<T extends WebComponentEvent = WebComponentEvent> {
|
||||||
/** The type of event being emitted */
|
/** The type of event being emitted */
|
||||||
event: T;
|
event: T;
|
||||||
/** Optional payload with additional data about the event */
|
/** Optional payload with additional data about the event */
|
||||||
payload?: WebComponentEventPayloadFor<T>;
|
payload?: WebComponentEventPayloadFor<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -40,10 +42,10 @@ export interface WebComponentOutboundEventMessage<T extends WebComponentEvent =
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
export function createWebComponentCommandMessage<T extends WebComponentCommand>(
|
export function createWebComponentCommandMessage<T extends WebComponentCommand>(
|
||||||
command: T,
|
command: T,
|
||||||
payload?: WenComponentCommandPayloadFor<T>
|
payload?: WenComponentCommandPayloadFor<T>
|
||||||
): WebComponentInboundCommandMessage<T> {
|
): WebComponentInboundCommandMessage<T> {
|
||||||
return { command, payload };
|
return { command, payload };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -55,8 +57,8 @@ export function createWebComponentCommandMessage<T extends WebComponentCommand>(
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
export function createWebComponentEventMessage<T extends WebComponentEvent>(
|
export function createWebComponentEventMessage<T extends WebComponentEvent>(
|
||||||
event: T,
|
event: T,
|
||||||
payload?: WebComponentEventPayloadFor<T>
|
payload?: WebComponentEventPayloadFor<T>
|
||||||
): WebComponentOutboundEventMessage<T> {
|
): WebComponentOutboundEventMessage<T> {
|
||||||
return { event, payload };
|
return { event, payload };
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user