Skip to content

Commit b2a71fa

Browse files
committed
Add web request interception into BlazorWebView
This also refactors the code a bit and disconnects it from the HybridWebView since the concept is a base idea.
1 parent a1e337c commit b2a71fa

18 files changed

+301
-146
lines changed

src/BlazorWebView/src/Maui/Android/WebKitWebViewClient.cs

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using Android.Runtime;
55
using Android.Webkit;
66
using Java.Net;
7+
using Microsoft.Extensions.Logging;
8+
using Microsoft.Maui.Platform;
79
using AWebView = Android.Webkit.WebView;
810

911
namespace Microsoft.AspNetCore.Components.WebView.Maui
@@ -83,10 +85,40 @@ private bool ShouldOverrideUrlLoadingCore(IWebResourceRequest? request)
8385
}
8486

8587
var requestUri = request?.Url?.ToString();
88+
89+
var logger = _webViewHandler?.Logger;
90+
91+
logger?.LogDebug("Intercepting request for {Url}.", requestUri);
92+
93+
if (view is not null && request is not null && !string.IsNullOrEmpty(requestUri))
94+
{
95+
// 1. Check if the app wants to modify or override the request
96+
var response = WebRequestInterceptingWebView.TryInterceptResponseStream(_webViewHandler, view, request, requestUri, logger);
97+
if (response is not null)
98+
{
99+
return response;
100+
}
101+
102+
// 2. Check if the request is for a Blazor resource
103+
response = GetResponse(requestUri, _webViewHandler?.Logger);
104+
if (response is not null)
105+
{
106+
return response;
107+
}
108+
}
109+
110+
// 3. Otherwise, we let the request go through as is
111+
logger?.LogDebug("Request for {Url} was not handled.", requestUri);
112+
113+
return base.ShouldInterceptRequest(view, request);
114+
}
115+
116+
private WebResourceResponse? GetResponse(string requestUri, ILogger? logger)
117+
{
86118
var allowFallbackOnHostPage = AppOriginUri.IsBaseOfPage(requestUri);
87119
requestUri = QueryStringHelper.RemovePossibleQueryString(requestUri);
88120

89-
_webViewHandler?.Logger.HandlingWebRequest(requestUri);
121+
logger?.HandlingWebRequest(requestUri);
90122

91123
if (requestUri != null &&
92124
_webViewHandler != null &&
@@ -95,16 +127,16 @@ private bool ShouldOverrideUrlLoadingCore(IWebResourceRequest? request)
95127
{
96128
var contentType = headers["Content-Type"];
97129

98-
_webViewHandler?.Logger.ResponseContentBeingSent(requestUri, statusCode);
130+
logger?.ResponseContentBeingSent(requestUri, statusCode);
99131

100132
return new WebResourceResponse(contentType, "UTF-8", statusCode, statusMessage, headers, content);
101133
}
102134
else
103135
{
104-
_webViewHandler?.Logger.ReponseContentNotFound(requestUri ?? string.Empty);
136+
logger?.ReponseContentNotFound(requestUri ?? string.Empty);
105137
}
106138

107-
return base.ShouldInterceptRequest(view, request);
139+
return null;
108140
}
109141

110142
public override void OnPageFinished(AWebView? view, string? url)

src/BlazorWebView/src/Maui/BlazorWebView.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Microsoft.AspNetCore.Components.Web;
44
using Microsoft.Extensions.FileProviders;
55
using Microsoft.Maui.Controls;
6+
using Microsoft.Maui;
67

78
namespace Microsoft.AspNetCore.Components.WebView.Maui
89
{
@@ -65,6 +66,18 @@ public string StartPath
6566
/// </summary>
6667
public event EventHandler<BlazorWebViewInitializedEventArgs>? BlazorWebViewInitialized;
6768

69+
/// <summary>
70+
/// Raised when a web resource is requested. This event allows the application to intercept the request and provide a
71+
/// custom response.
72+
/// The event handler can set the <see cref="WebViewWebResourceRequestedEventArgs.Handled"/> property to true
73+
/// to indicate that the request has been handled and no further processing is needed. If the event handler does set this
74+
/// property to true, it must also call the
75+
/// <see cref="WebViewWebResourceRequestedEventArgs.SetResponse(int, string, System.Collections.Generic.IReadOnlyDictionary{string, string}?, System.IO.Stream?)"/>
76+
/// or <see cref="WebViewWebResourceRequestedEventArgs.SetResponse(int, string, System.Collections.Generic.IReadOnlyDictionary{string, string}?, System.Threading.Tasks.Task{System.IO.Stream?})"/>
77+
/// method to provide a response to the request.
78+
/// </summary>
79+
public event EventHandler<WebViewWebResourceRequestedEventArgs>? WebResourceRequested;
80+
6881
/// <inheritdoc />
6982
#if ANDROID
7083
[System.Runtime.Versioning.SupportedOSPlatform("android23.0")]
@@ -108,5 +121,14 @@ void IBlazorWebView.BlazorWebViewInitializing(BlazorWebViewInitializingEventArgs
108121
/// <inheritdoc />
109122
void IBlazorWebView.BlazorWebViewInitialized(BlazorWebViewInitializedEventArgs args) =>
110123
BlazorWebViewInitialized?.Invoke(this, args);
124+
125+
/// <inheritdoc />
126+
bool IWebRequestInterceptingWebView.WebResourceRequested(WebResourceRequestedEventArgs args)
127+
{
128+
var platformArgs = new PlatformWebViewWebResourceRequestedEventArgs(args);
129+
var e = new WebViewWebResourceRequestedEventArgs(platformArgs);
130+
WebResourceRequested?.Invoke(this, e);
131+
return e.Handled;
132+
}
111133
}
112134
}

src/BlazorWebView/src/Maui/IBlazorWebView.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Components.WebView.Maui
88
/// <summary>
99
/// Defines a contract for a view that renders Blazor content.
1010
/// </summary>
11-
public interface IBlazorWebView : IView
11+
public interface IBlazorWebView : IView, IWebRequestInterceptingWebView
1212
{
1313
/// <summary>
1414
/// Gets the path to the HTML file to render.

src/BlazorWebView/src/Maui/Windows/WinUIWebViewManager.cs

Lines changed: 60 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ namespace Microsoft.AspNetCore.Components.WebView.Maui
2020
/// </summary>
2121
internal class WinUIWebViewManager : WebView2WebViewManager
2222
{
23+
private readonly BlazorWebViewHandler _handler;
2324
private readonly WebView2Control _webview;
2425
private readonly string _hostPageRelativePath;
2526
private readonly string _contentRootRelativeToAppRoot;
@@ -62,6 +63,7 @@ public WinUIWebViewManager(
6263
ILogger logger)
6364
: base(webview, services, dispatcher, fileProvider, jsComponents, contentRootRelativeToAppRoot, hostPagePathWithinFileProvider, webViewHandler, logger)
6465
{
66+
_handler = webViewHandler;
6567
_logger = logger;
6668
_webview = webview;
6769
_hostPageRelativePath = hostPagePathWithinFileProvider;
@@ -71,52 +73,71 @@ public WinUIWebViewManager(
7173
/// <inheritdoc />
7274
protected override async Task HandleWebResourceRequest(CoreWebView2WebResourceRequestedEventArgs eventArgs)
7375
{
74-
// Unlike server-side code, we get told exactly why the browser is making the request,
75-
// so we can be smarter about fallback. We can ensure that 'fetch' requests never result
76-
// in fallback, for example.
77-
var allowFallbackOnHostPage =
78-
eventArgs.ResourceContext == CoreWebView2WebResourceContext.Document ||
79-
eventArgs.ResourceContext == CoreWebView2WebResourceContext.Other; // e.g., dev tools requesting page source
76+
var url = eventArgs.Request.Uri;
8077

81-
// Get a deferral object so that WebView2 knows there's some async stuff going on. We call Complete() at the end of this method.
82-
using var deferral = eventArgs.GetDeferral();
78+
_logger.LogDebug("Intercepting request for {Url}.", url);
8379

84-
var requestUri = QueryStringHelper.RemovePossibleQueryString(eventArgs.Request.Uri);
85-
86-
_logger.HandlingWebRequest(requestUri);
87-
88-
var uri = new Uri(requestUri);
89-
var relativePath = AppOriginUri.IsBaseOf(uri) ? AppOriginUri.MakeRelativeUri(uri).ToString() : null;
90-
91-
// Check if the uri is _framework/blazor.modules.json is a special case as the built-in file provider
92-
// brings in a default implementation.
93-
if (relativePath != null &&
94-
string.Equals(relativePath, "_framework/blazor.modules.json", StringComparison.Ordinal) &&
95-
await TryServeFromFolderAsync(eventArgs, allowFallbackOnHostPage: false, requestUri, relativePath))
96-
{
97-
_logger.ResponseContentBeingSent(requestUri, 200);
98-
}
99-
else if (TryGetResponseContent(requestUri, allowFallbackOnHostPage, out var statusCode, out var statusMessage, out var content, out var headers)
100-
&& statusCode != 404)
80+
// 1. First check if the app wants to modify or override the request.
81+
if (WebRequestInterceptingWebView.TryInterceptResponseStream(_handler, _webview.CoreWebView2, eventArgs, url, _logger))
10182
{
102-
// First, call into WebViewManager to see if it has a framework file for this request. It will
103-
// fall back to an IFileProvider, but on WinUI it's always a NullFileProvider, so that will never
104-
// return a file.
105-
var headerString = GetHeaderString(headers);
106-
_logger.ResponseContentBeingSent(requestUri, statusCode);
107-
eventArgs.Response = _coreWebView2Environment!.CreateWebResourceResponse(content.AsRandomAccessStream(), statusCode, statusMessage, headerString);
83+
return;
10884
}
109-
else if (relativePath != null)
85+
86+
// 2. If this is an app request, then assume the request is for a Blazor resource.
87+
var requestUri = QueryStringHelper.RemovePossibleQueryString(url);
88+
if (new Uri(requestUri) is Uri uri)
11089
{
111-
await TryServeFromFolderAsync(
112-
eventArgs,
113-
allowFallbackOnHostPage,
114-
requestUri,
115-
relativePath);
90+
// Unlike server-side code, we get told exactly why the browser is making the request,
91+
// so we can be smarter about fallback. We can ensure that 'fetch' requests never result
92+
// in fallback, for example.
93+
var allowFallbackOnHostPage =
94+
eventArgs.ResourceContext == CoreWebView2WebResourceContext.Document ||
95+
eventArgs.ResourceContext == CoreWebView2WebResourceContext.Other; // e.g., dev tools requesting page source
96+
97+
// Get a deferral object so that WebView2 knows there's some async stuff going on. We call Complete() at the end of this method.
98+
using var deferral = eventArgs.GetDeferral();
99+
100+
_logger.HandlingWebRequest(requestUri);
101+
102+
var relativePath = AppOriginUri.IsBaseOf(uri) ? AppOriginUri.MakeRelativeUri(uri).ToString() : null;
103+
104+
// Check if the uri is _framework/blazor.modules.json is a special case as the built-in file provider
105+
// brings in a default implementation.
106+
if (relativePath != null &&
107+
string.Equals(relativePath, "_framework/blazor.modules.json", StringComparison.Ordinal) &&
108+
await TryServeFromFolderAsync(eventArgs, allowFallbackOnHostPage: false, requestUri, relativePath))
109+
{
110+
_logger.ResponseContentBeingSent(requestUri, 200);
111+
}
112+
else if (TryGetResponseContent(requestUri, allowFallbackOnHostPage, out var statusCode, out var statusMessage, out var content, out var headers)
113+
&& statusCode != 404)
114+
{
115+
// First, call into WebViewManager to see if it has a framework file for this request. It will
116+
// fall back to an IFileProvider, but on WinUI it's always a NullFileProvider, so that will never
117+
// return a file.
118+
var headerString = GetHeaderString(headers);
119+
_logger.ResponseContentBeingSent(requestUri, statusCode);
120+
eventArgs.Response = _coreWebView2Environment!.CreateWebResourceResponse(content.AsRandomAccessStream(), statusCode, statusMessage, headerString);
121+
}
122+
else if (relativePath != null)
123+
{
124+
await TryServeFromFolderAsync(
125+
eventArgs,
126+
allowFallbackOnHostPage,
127+
requestUri,
128+
relativePath);
129+
}
130+
131+
// Notify WebView2 that the deferred (async) operation is complete and we set a response.
132+
deferral.Complete();
133+
return;
116134
}
117135

118-
// Notify WebView2 that the deferred (async) operation is complete and we set a response.
119-
deferral.Complete();
136+
// 3. If the request is not handled by the app nor is it a local source, then we let the WebView2
137+
// handle the request as it would normally do. This means that it will try to load the resource
138+
// from the internet or from the local cache.
139+
140+
_logger.LogDebug("Request for {Url} was not handled.", url);
120141
}
121142

122143
private async Task<bool> TryServeFromFolderAsync(

src/BlazorWebView/src/Maui/iOS/BlazorWebViewHandler.iOS.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Microsoft.Maui;
1212
using Microsoft.Maui.Dispatching;
1313
using Microsoft.Maui.Handlers;
14+
using Microsoft.Maui.Platform;
1415
using UIKit;
1516
using WebKit;
1617
using RectangleF = CoreGraphics.CGRect;
@@ -259,12 +260,29 @@ public SchemeHandler(BlazorWebViewHandler webViewHandler)
259260
[SupportedOSPlatform("ios11.0")]
260261
public void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSchemeTask)
261262
{
262-
var responseBytes = GetResponseBytes(urlSchemeTask.Request.Url?.AbsoluteString ?? "", out var contentType, statusCode: out var statusCode);
263+
var url = urlSchemeTask.Request.Url.AbsoluteString;
264+
if (string.IsNullOrEmpty(url))
265+
{
266+
return;
267+
}
268+
269+
var logger = _webViewHandler.Logger;
270+
271+
logger?.LogDebug("Intercepting request for {Url}.", url);
272+
273+
// 1. First check if the app wants to modify or override the request.
274+
if (WebRequestInterceptingWebView.TryInterceptResponseStream(_webViewHandler, webView, urlSchemeTask, url, logger))
275+
{
276+
return;
277+
}
278+
279+
// 2. If this is an app request, then assume the request is for a Blazor resource.
280+
var responseBytes = GetResponseBytes(url, out var contentType, statusCode: out var statusCode);
263281
if (statusCode == 200)
264282
{
265283
using (var dic = new NSMutableDictionary<NSString, NSString>())
266284
{
267-
dic.Add((NSString)"Content-Length", (NSString)(responseBytes.Length.ToString(CultureInfo.InvariantCulture)));
285+
dic.Add((NSString)"Content-Length", (NSString)responseBytes.Length.ToString(CultureInfo.InvariantCulture));
268286
dic.Add((NSString)"Content-Type", (NSString)contentType);
269287
// Disable local caching. This will prevent user scripts from executing correctly.
270288
dic.Add((NSString)"Cache-Control", (NSString)"no-cache, max-age=0, must-revalidate, no-store");
@@ -278,6 +296,12 @@ public void StartUrlSchemeTask(WKWebView webView, IWKUrlSchemeTask urlSchemeTask
278296
urlSchemeTask.DidReceiveData(NSData.FromArray(responseBytes));
279297
urlSchemeTask.DidFinish();
280298
}
299+
300+
// 3. If the request is not handled by the app nor is it a local source, then we let the WKWebView
301+
// handle the request as it would normally do. This means that it will try to load the resource
302+
// from the internet or from the local cache.
303+
304+
logger?.LogDebug("Request for {Url} was not handled.", url);
281305
}
282306

283307
private byte[] GetResponseBytes(string? url, out string contentType, out int statusCode)

src/Controls/src/Core/HybridWebView/HybridWebView.cs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,25 +66,26 @@ void IHybridWebView.RawMessageReceived(string rawMessage)
6666
/// </summary>
6767
public event EventHandler<HybridWebViewRawMessageReceivedEventArgs>? RawMessageReceived;
6868

69-
bool IHybridWebView.WebResourceRequested(WebResourceRequestedEventArgs args)
69+
/// <inheritdoc/>
70+
bool IWebRequestInterceptingWebView.WebResourceRequested(WebResourceRequestedEventArgs args)
7071
{
71-
var platformArgs = new PlatformHybridWebViewWebResourceRequestedEventArgs(args);
72-
var e = new HybridWebViewWebResourceRequestedEventArgs(platformArgs);
72+
var platformArgs = new PlatformWebViewWebResourceRequestedEventArgs(args);
73+
var e = new WebViewWebResourceRequestedEventArgs(platformArgs);
7374
WebResourceRequested?.Invoke(this, e);
7475
return e.Handled;
7576
}
7677

7778
/// <summary>
7879
/// Raised when a web resource is requested. This event allows the application to intercept the request and provide a
7980
/// custom response.
80-
/// The event handler can set the <see cref="HybridWebViewWebResourceRequestedEventArgs.Handled"/> property to true
81+
/// The event handler can set the <see cref="WebViewWebResourceRequestedEventArgs.Handled"/> property to true
8182
/// to indicate that the request has been handled and no further processing is needed. If the event handler does set this
8283
/// property to true, it must also call the
83-
/// <see cref="HybridWebViewWebResourceRequestedEventArgs.SetResponse(int, string, System.Collections.Generic.IReadOnlyDictionary{string, string}?, System.IO.Stream?)"/>
84-
/// or <see cref="HybridWebViewWebResourceRequestedEventArgs.SetResponse(int, string, System.Collections.Generic.IReadOnlyDictionary{string, string}?, System.Threading.Tasks.Task{System.IO.Stream?})"/>
84+
/// <see cref="WebViewWebResourceRequestedEventArgs.SetResponse(int, string, System.Collections.Generic.IReadOnlyDictionary{string, string}?, System.IO.Stream?)"/>
85+
/// or <see cref="WebViewWebResourceRequestedEventArgs.SetResponse(int, string, System.Collections.Generic.IReadOnlyDictionary{string, string}?, System.Threading.Tasks.Task{System.IO.Stream?})"/>
8586
/// method to provide a response to the request.
8687
/// </summary>
87-
public event EventHandler<HybridWebViewWebResourceRequestedEventArgs>? WebResourceRequested;
88+
public event EventHandler<WebViewWebResourceRequestedEventArgs>? WebResourceRequested;
8889

8990
/// <summary>
9091
/// Sends a raw message to the code running in the web view. Raw messages have no additional processing.

0 commit comments

Comments
 (0)