webcomponent: enhance message handling and security in WebComponent communication

This commit is contained in:
juancarmore 2025-07-07 20:03:37 +02:00
parent 7836da8858
commit 459a37bee8
6 changed files with 126 additions and 109 deletions

View File

@ -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);
} }
} }

View File

@ -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;
} }

View File

@ -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) => {

View File

@ -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;

View File

@ -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;

View File

@ -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 };
} }