From c7621ac0838e42cc0f571144f768b908060ae9e3 Mon Sep 17 00:00:00 2001 From: Tim Maes Date: Wed, 27 Aug 2025 17:57:25 +0200 Subject: [PATCH] Implement feature --- README.md | 21 +- docs/configuration/communication-options.md | 219 ++++++-------------- docs/core-features/parameters.md | 33 +++ src/BlazorFrame/BlazorFrame.csproj | 6 +- src/BlazorFrame/BlazorFrame.razor.cs | 126 ++++++++++- src/BlazorFrame/wwwroot/BlazorFrame.js | 23 ++ 6 files changed, 265 insertions(+), 163 deletions(-) diff --git a/README.md b/README.md index 25a78ee..fdb2e59 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ A security-first Blazor iframe component with automatic resizing, cross-frame me - **Security-First Design** - Built-in origin validation, message filtering, and sandbox isolation - **Content Security Policy** - Comprehensive CSP integration with fluent configuration API -- **Cross-Frame Messaging** - Secure postMessage communication with validation +- **Bidirectional Communication** - Secure postMessage communication with validation for both directions - **Sandbox Support** - Multiple security levels from permissive to paranoid isolation - **Environment-Aware** - Different configurations for development vs production - **Automatic Resizing** - Smart height adjustment based on iframe content @@ -57,13 +57,18 @@ dotnet add package BlazorFrame - - + + + @code { + private BlazorFrame? iframeRef; + private readonly MessageSecurityOptions securityOptions = new MessageSecurityOptions() .ForProduction() // Strict security settings .WithBasicSandbox() // Enable iframe sandboxing @@ -79,7 +84,15 @@ dotnet add package BlazorFrame { Console.WriteLine($"Security violation: {violation.ValidationError}"); return Task.CompletedTask; - }; + } + + private async Task SendDataToIframe() + { + if (iframeRef != null) + { + await iframeRef.SendTypedMessageAsync("user-data", new { userId = 123, name = "John" }); + } + } } ``` diff --git a/docs/configuration/communication-options.md b/docs/configuration/communication-options.md index 0707fcb..c68b3e4 100644 --- a/docs/configuration/communication-options.md +++ b/docs/configuration/communication-options.md @@ -2,45 +2,87 @@ **Cross-frame messaging and event handling for BlazorFrame** -This guide covers all aspects of configuring communication between your Blazor application and iframe content, including message validation, origin control, and event handling. +This guide covers all aspects of configuring communication between your Blazor application and iframe content, including message validation, origin control, event handling, and **bidirectional communication**. ## Message Handling Overview -BlazorFrame provides two main approaches to handling messages: +BlazorFrame provides comprehensive communication capabilities: -- **Validated Messages** (`OnValidatedMessage`) - Recommended for new implementations +- **Iframe -> Host** (`OnValidatedMessage`) - Receive messages from iframe with validation +- **Host -> Iframe** (`SendMessageAsync`) - Send messages to iframe with security validation - **Raw Messages** (`OnMessage`) - Legacy support for simple scenarios ## Basic Message Configuration -### Essential Message Handling +### Bidirectional Communication ```razor - + + + @code { - private readonly MessageSecurityOptions messageOptions = new MessageSecurityOptions() - .ForProduction() - .WithBasicSandbox(); + private BlazorFrame? iframeRef; - private async Task HandleValidatedMessage(IframeMessage message) + // Send structured data to iframe + private async Task SendDataToIframe() { - Logger.LogInformation("Received message from {Origin}: {Data}", - message.Origin, message.Data); - - // Process the validated message - await ProcessMessage(message); + if (iframeRef == null) return; + + var success = await iframeRef.SendMessageAsync(new + { + type = "data-update", + timestamp = DateTime.UtcNow, + data = new + { + userId = currentUser.Id, + preferences = currentUser.Preferences, + theme = currentTheme + } + }); + + if (success) + { + Logger.LogInformation("Data sent successfully to iframe"); + } } - private async Task HandleSecurityViolation(IframeMessage violation) + // Send typed messages with automatic structure + private async Task SendNotification() { - Logger.LogWarning("Security violation: {Error}", violation.ValidationError); - - // Handle security issues - await HandleSecurityIssue(violation); + await iframeRef.SendTypedMessageAsync("notification", new + { + message = "Hello from host!", + level = "info", + timestamp = DateTimeOffset.UtcNow + }); + } + + private async Task HandleMessage(IframeMessage message) + { + if (message.MessageType == "request-user-data") + { + // Respond to iframe's request for user data + await SendUserDataToIframe(); + } + } + + private async Task SendUserDataToIframe() + { + await iframeRef.SendTypedMessageAsync("user-data-response", new + { + user = new + { + id = currentUser.Id, + name = currentUser.Name, + email = currentUser.Email, + permissions = currentUser.Permissions + } + }); } } ``` @@ -336,139 +378,6 @@ public class MessageProcessor } ``` -## Bidirectional Communication - -### Sending Messages to Iframe - -```razor - - - - -@code { - private BlazorFrame? iframeRef; - - private async Task SendDataToIframe() - { - if (iframeRef == null) return; - - var messageData = new - { - type = "data-update", - timestamp = DateTime.UtcNow, - data = new - { - userId = currentUser.Id, - preferences = currentUser.Preferences, - theme = currentTheme - } - }; - - await iframeRef.SendMessageAsync(messageData); - } - - private async Task HandleMessage(IframeMessage message) - { - if (message.MessageType == "request-user-data") - { - // Respond to iframe's request for user data - await SendUserDataToIframe(); - } - } - - private async Task SendUserDataToIframe() - { - var userData = new - { - type = "user-data-response", - user = new - { - id = currentUser.Id, - name = currentUser.Name, - email = currentUser.Email, - permissions = currentUser.Permissions - } - }; - - await iframeRef.SendMessageAsync(userData); - } -} -``` - -### Request-Response Pattern - -```razor -@code { - private readonly Dictionary> pendingRequests = new(); - - private async Task SendRequestToIframe(string requestType, object data) - { - var requestId = Guid.NewGuid().ToString(); - var tcs = new TaskCompletionSource(); - - pendingRequests[requestId] = tcs; - - try - { - // Send request to iframe - await iframeRef.SendMessageAsync(new - { - type = requestType, - requestId = requestId, - data = data - }); - - // Wait for response with timeout - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); - cts.Token.Register(() => tcs.TrySetCanceled()); - - var response = await tcs.Task; - return JsonSerializer.Deserialize(response.ToString()); - } - finally - { - pendingRequests.Remove(requestId); - } - } - - private async Task HandleMessage(IframeMessage message) - { - // Handle responses to our requests - if (message.MessageType?.EndsWith("-response") == true) - { - var responseData = JsonSerializer.Deserialize(message.Data); - - if (pendingRequests.TryGetValue(responseData.RequestId, out var tcs)) - { - tcs.SetResult(responseData.Data); - } - } - } - - // Usage example - private async Task GetIframeData() - { - try - { - var iframeData = await SendRequestToIframe( - "get-data", - new { category = "user-preferences" } - ); - - Logger.LogInformation("Received iframe data: {Data}", iframeData); - } - catch (OperationCanceledException) - { - Logger.LogWarning("Request to iframe timed out"); - } - } -} -``` - ## Event Handling ### Comprehensive Event Configuration diff --git a/docs/core-features/parameters.md b/docs/core-features/parameters.md index bc26e9c..ca932c9 100644 --- a/docs/core-features/parameters.md +++ b/docs/core-features/parameters.md @@ -339,6 +339,39 @@ Complete reference for all BlazorFrame component parameters, their types, defaul } ``` +### OnMessageSent +**Type:** `EventCallback` +**Description:** Fired when a message is successfully sent to the iframe. + +```razor + + +@code { + private Task HandleMessageSent(string messageJson) + { + Logger.LogDebug("Message sent successfully: {Message}", messageJson); + return Task.CompletedTask; + } +} +``` + +### OnMessageSendFailed +**Type:** `EventCallback` +**Description:** Fired when sending a message to the iframe fails. + +```razor + + +@code { + private Task HandleSendFailure(Exception ex) + { + Logger.LogError(ex, "Failed to send message to iframe"); + // Handle the failure appropriately + ShowErrorToUser("Communication with widget failed"); + return Task.CompletedTask; + } +} +``` ## Styling Parameters ### AdditionalAttributes diff --git a/src/BlazorFrame/BlazorFrame.csproj b/src/BlazorFrame/BlazorFrame.csproj index 1311bb4..8970290 100644 --- a/src/BlazorFrame/BlazorFrame.csproj +++ b/src/BlazorFrame/BlazorFrame.csproj @@ -6,12 +6,12 @@ enable true BlazorFrame - 2.1.2 - A enhanced secure Blazor iFrame component with built-in origin validation and message security. + 2.2.0 + A enhanced secure Blazor iFrame component with built-in origin validation, bidirectional messaging, and comprehensive security features. https://www.github.com/Tim-Maes/BlazorFrame README.md https://www.github.com/Tim-Maes/BlazorFrame - blazor; iframe; wasm; security; postmessage; origin-validation; + blazor; iframe; wasm; security; postmessage; origin-validation; bidirectional; LICENSE.txt BlazorFrameIcon.ico diff --git a/src/BlazorFrame/BlazorFrame.razor.cs b/src/BlazorFrame/BlazorFrame.razor.cs index fcd9d8e..790d393 100644 --- a/src/BlazorFrame/BlazorFrame.razor.cs +++ b/src/BlazorFrame/BlazorFrame.razor.cs @@ -2,6 +2,7 @@ using Microsoft.JSInterop; using Microsoft.Extensions.Logging; using BlazorFrame.Services; +using System.Text.Json; namespace BlazorFrame; @@ -65,6 +66,16 @@ public partial class BlazorFrame : IAsyncDisposable /// [Parameter] public EventCallback OnCspHeaderGenerated { get; set; } + /// + /// Event fired when a message is successfully sent to the iframe + /// + [Parameter] public EventCallback OnMessageSent { get; set; } + + /// + /// Event fired when sending a message to the iframe fails + /// + [Parameter] public EventCallback OnMessageSendFailed { get; set; } + [Parameter(CaptureUnmatchedValues = true)] public Dictionary AdditionalAttributes { get; set; } = new(); @@ -104,6 +115,119 @@ private Dictionary IframeAttributes } } + /// + /// Sends a message to the iframe content + /// + /// Message data to send + /// Target origin for security (defaults to iframe origin) + /// True if message was sent successfully + public async Task SendMessageAsync(object data, string? targetOrigin = null) + { + if (module == null || !isInitialized) + { + var ex = new InvalidOperationException("BlazorFrame not initialized. Cannot send message."); + Logger?.LogError(ex, "Attempted to send message before initialization"); + await OnMessageSendFailed.InvokeAsync(ex); + return false; + } + + if (string.IsNullOrEmpty(Src)) + { + var ex = new InvalidOperationException("BlazorFrame Src is not set. Cannot determine target origin."); + Logger?.LogError(ex, "Attempted to send message without valid Src"); + await OnMessageSendFailed.InvokeAsync(ex); + return false; + } + + try + { + // Use specified target origin or derive from Src + var effectiveOrigin = targetOrigin ?? validationService.ExtractOrigin(Src); + if (string.IsNullOrEmpty(effectiveOrigin)) + { + var ex = new ArgumentException($"Cannot determine valid origin from Src: {Src}"); + Logger?.LogError(ex, "Invalid target origin for message"); + await OnMessageSendFailed.InvokeAsync(ex); + return false; + } + + // Validate target origin is allowed + if (!computedAllowedOrigins.Contains(effectiveOrigin, StringComparer.OrdinalIgnoreCase)) + { + var ex = new UnauthorizedAccessException($"Target origin '{effectiveOrigin}' is not in allowed origins list"); + Logger?.LogWarning(ex, "Attempted to send message to unauthorized origin"); + await OnMessageSendFailed.InvokeAsync(ex); + return false; + } + + // Serialize message data + var messageJson = JsonSerializer.Serialize(data, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + + // Validate outbound message if strict validation is enabled + if (SecurityOptions.EnableStrictValidation) + { + var validationResult = validationService.ValidateMessage( + effectiveOrigin, + messageJson, + computedAllowedOrigins, + SecurityOptions); + + if (!validationResult.IsValid) + { + var ex = new ArgumentException($"Outbound message validation failed: {validationResult.ValidationError}"); + Logger?.LogWarning(ex, "Outbound message failed validation"); + await OnMessageSendFailed.InvokeAsync(ex); + return false; + } + } + + // Send message via JavaScript + var success = await module.InvokeAsync("sendMessage", iframeElement, messageJson, effectiveOrigin); + + if (success) + { + Logger?.LogDebug("BlazorFrame: Message sent successfully to {Origin}", effectiveOrigin); + await OnMessageSent.InvokeAsync(messageJson); + } + else + { + var ex = new InvalidOperationException("JavaScript sendMessage returned false"); + Logger?.LogWarning(ex, "Failed to send message to iframe"); + await OnMessageSendFailed.InvokeAsync(ex); + } + + return success; + } + catch (Exception ex) + { + Logger?.LogError(ex, "Error sending message to iframe"); + await OnMessageSendFailed.InvokeAsync(ex); + return false; + } + } + + /// + /// Sends a message to the iframe content with automatic type detection + /// + /// Type identifier for the message + /// Message payload + /// Target origin for security (defaults to iframe origin) + /// True if message was sent successfully + public async Task SendTypedMessageAsync(string messageType, object? data = null, string? targetOrigin = null) + { + var message = new + { + type = messageType, + data = data, + timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + }; + + return await SendMessageAsync(message, targetOrigin); + } + /// /// Gets the recommended CSP header for the current configuration /// @@ -423,4 +547,4 @@ public async ValueTask DisposeAsync() isInitialized = false; } } -} \ No newline at end of file +}} \ No newline at end of file diff --git a/src/BlazorFrame/wwwroot/BlazorFrame.js b/src/BlazorFrame/wwwroot/BlazorFrame.js index ceab778..7a731f5 100644 --- a/src/BlazorFrame/wwwroot/BlazorFrame.js +++ b/src/BlazorFrame/wwwroot/BlazorFrame.js @@ -128,4 +128,27 @@ export function initialize(iframe, dotNetHelper, enableResize, allowedOrigins = cleanupFunctions.forEach(cleanup => cleanup()); console.log('BlazorFrame: Cleanup completed'); }; +} + +// New function for bidirectional communication - sending messages to iframe +export function sendMessage(iframe, messageJson, targetOrigin) { + if (!iframe || !iframe.contentWindow) { + console.warn('BlazorFrame: Cannot send message - iframe not ready'); + return false; + } + + if (!targetOrigin) { + console.warn('BlazorFrame: Cannot send message - target origin not specified'); + return false; + } + + try { + const messageData = JSON.parse(messageJson); + iframe.contentWindow.postMessage(messageData, targetOrigin); + console.log('BlazorFrame: Message sent to iframe:', targetOrigin); + return true; + } catch (error) { + console.error('BlazorFrame: Failed to send message to iframe:', error); + return false; + } } \ No newline at end of file