Skip to content

Commit 189ed00

Browse files
committed
Add the ability to intercept web requests
1 parent 35ffcb7 commit 189ed00

32 files changed

+1633
-266
lines changed

src/Controls/src/Core/Controls.Core.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@
4040
<ProjectReference Include="..\..\..\Core\src\Core.csproj" />
4141
</ItemGroup>
4242

43+
<ItemGroup>
44+
<Compile Include="..\..\..\Essentials\src\Types\Shared\WebUtils.shared.cs" />
45+
</ItemGroup>
46+
4347
<ItemGroup Condition=" '$(_MauiDesignDllBuild)' == 'True' and '$(TargetFramework)' == '$(_MauiDotNetTfm)'">
4448
<ProjectReference Include="..\..\..\Controls\src\Core.Design\Controls.Core.Design.csproj" ReferenceOutputAssembly="false" />
4549
</ItemGroup>

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

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

69+
bool IHybridWebView.WebResourceRequested(WebResourceRequestedEventArgs args)
70+
{
71+
var platformArgs = new PlatformHybridWebViewWebResourceRequestedEventArgs(args);
72+
var e = new HybridWebViewWebResourceRequestedEventArgs(platformArgs);
73+
WebResourceRequested?.Invoke(this, e);
74+
return e.Handled;
75+
}
76+
77+
/// <summary>
78+
/// Raised when a web resource is requested. This event allows the application to intercept the request and provide a
79+
/// custom response.
80+
/// The event handler can set the <see cref="HybridWebViewWebResourceRequestedEventArgs.Handled"/> property to true
81+
/// to indicate that the request has been handled and no further processing is needed. If the event handler does set this
82+
/// 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?})"/>
85+
/// method to provide a response to the request.
86+
/// </summary>
87+
public event EventHandler<HybridWebViewWebResourceRequestedEventArgs>? WebResourceRequested;
88+
6989
/// <summary>
7090
/// Sends a raw message to the code running in the web view. Raw messages have no additional processing.
7191
/// </summary>
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
namespace Microsoft.Maui.Controls;
8+
9+
/// <summary>
10+
/// Event arguments for the <see cref="HybridWebView.WebResourceRequested"/> event.
11+
/// </summary>
12+
public class HybridWebViewWebResourceRequestedEventArgs
13+
{
14+
IReadOnlyDictionary<string, string>? _headers;
15+
IReadOnlyDictionary<string, string>? _queryParams;
16+
17+
internal HybridWebViewWebResourceRequestedEventArgs(PlatformHybridWebViewWebResourceRequestedEventArgs platformArgs)
18+
{
19+
PlatformArgs = platformArgs;
20+
Uri = platformArgs.GetRequestUri() is string uri ? new Uri(uri) : throw new InvalidOperationException("Platform web request did not have a request URI.");
21+
Method = platformArgs.GetRequestMethod() ?? throw new InvalidOperationException("Platform web request did not have a request METHOD.");
22+
}
23+
24+
/// <summary>
25+
/// Initializes a new instance of the <see cref="HybridWebViewWebResourceRequestedEventArgs"/> class
26+
/// with the specified URI and method.
27+
/// </summary>
28+
public HybridWebViewWebResourceRequestedEventArgs(Uri uri, string method)
29+
{
30+
Uri = uri;
31+
Method = method;
32+
}
33+
34+
/// <summary>
35+
/// Gets the platform-specific event arguments.
36+
/// </summary>
37+
public PlatformHybridWebViewWebResourceRequestedEventArgs? PlatformArgs { get; }
38+
39+
/// <summary>
40+
/// Gets the URI of the requested resource.
41+
/// </summary>
42+
public Uri Uri { get; }
43+
44+
/// <summary>
45+
/// Gets the HTTP method used for the request (e.g., GET, POST).
46+
/// </summary>
47+
public string Method { get; }
48+
49+
/// <summary>
50+
/// Gets the headers associated with the request.
51+
/// </summary>
52+
public IReadOnlyDictionary<string, string> Headers =>
53+
_headers ??= PlatformArgs?.GetRequestHeaders() ?? new Dictionary<string, string>();
54+
55+
/// <summary>
56+
/// Gets the query parameters from the URI.
57+
/// </summary>
58+
public IReadOnlyDictionary<string, string> QueryParameters =>
59+
_queryParams ??= WebUtils.ParseQueryString(Uri, false) ?? new Dictionary<string, string>();
60+
61+
/// <summary>
62+
/// Gets or sets a value indicating whether the request has been handled.
63+
///
64+
/// If set to true, the web view will not process the request further and a response
65+
/// must be provided using the
66+
/// <see cref="SetResponse(int, string, System.Collections.Generic.IReadOnlyDictionary{string, string}?, System.IO.Stream?)"/>
67+
/// or <see cref="SetResponse(int, string, System.Collections.Generic.IReadOnlyDictionary{string, string}?, System.Threading.Tasks.Task{System.IO.Stream?})"/> method.
68+
/// If set to false, the web view will continue processing the request as normal.
69+
/// </summary>
70+
public bool Handled { get; set; }
71+
72+
/// <summary>
73+
/// Sets the response for the web resource request.
74+
///
75+
/// This method must be called if the <see cref="Handled"/> property is set to true.
76+
/// </summary>
77+
/// <param name="code">The HTTP status code for the response.</param>
78+
/// <param name="reason">The reason phrase for the response.</param>
79+
/// <param name="headers">The headers to include in the response.</param>
80+
/// <param name="content">The content of the response as a stream.</param>
81+
public void SetResponse(int code, string reason, IReadOnlyDictionary<string, string>? headers, Stream? content)
82+
{
83+
_ = PlatformArgs ?? throw new InvalidOperationException("Platform web request was not valid.");
84+
85+
#if WINDOWS
86+
87+
// create the response
88+
PlatformArgs.RequestEventArgs.Response = PlatformArgs.Sender.Environment.CreateWebResourceResponse(
89+
content?.AsRandomAccessStream(),
90+
code,
91+
reason,
92+
PlatformHeaders(headers));
93+
94+
#elif IOS || MACCATALYST
95+
96+
// iOS and MacCatalyst will just wait until DidFinish is called
97+
var task = PlatformArgs.UrlSchemeTask;
98+
99+
// create and send the response headers
100+
task.DidReceiveResponse(new Foundation.NSHttpUrlResponse(
101+
PlatformArgs.Request.Url,
102+
code,
103+
"HTTP/1.1",
104+
PlatformHeaders(headers)));
105+
106+
// send the data
107+
if (content is not null && Foundation.NSData.FromStream(content) is { } nsdata)
108+
{
109+
task.DidReceiveData(nsdata);
110+
}
111+
112+
// let the webview know
113+
task.DidFinish();
114+
115+
#elif ANDROID
116+
117+
// Android requires that we return immediately, even if the data is coming later
118+
119+
// create and send the response headers
120+
var platformHeaders = PlatformHeaders(headers, out var contentType);
121+
PlatformArgs.Response = new global::Android.Webkit.WebResourceResponse(
122+
contentType,
123+
"UTF-8",
124+
code,
125+
reason,
126+
platformHeaders,
127+
content);
128+
129+
#endif
130+
}
131+
132+
/// <summary>
133+
/// Sets the asynchronous response for the web resource request.
134+
///
135+
/// This method must be called if the <see cref="Handled"/> property is set to true.
136+
/// </summary>
137+
/// <param name="code">The HTTP status code for the response.</param>
138+
/// <param name="reason">The reason phrase for the response.</param>
139+
/// <param name="headers">The headers to include in the response.</param>
140+
/// <param name="contentTask">A task that represents the asynchronous operation of getting the response content.</param>
141+
/// <remarks>
142+
/// This method is not asynchronous and will return immediately. The actual response will be sent when the content task completes.
143+
/// </remarks>
144+
public void SetResponse(int code, string reason, IReadOnlyDictionary<string, string>? headers, Task<Stream?> contentTask) =>
145+
SetResponseAsync(code, reason, headers, contentTask).FireAndForget();
146+
147+
#pragma warning disable CS1998 // Android implememntation does not use async/await
148+
async Task SetResponseAsync(int code, string reason, IReadOnlyDictionary<string, string>? headers, Task<Stream?> contentTask)
149+
#pragma warning restore CS1998
150+
{
151+
_ = PlatformArgs ?? throw new InvalidOperationException("Platform web request was not valid.");
152+
153+
#if WINDOWS
154+
155+
// Windows uses a deferral to let the webview know that we are going to be async
156+
using var deferral = PlatformArgs.RequestEventArgs.GetDeferral();
157+
158+
// get the actual content
159+
var data = await contentTask;
160+
161+
// create the response
162+
PlatformArgs.RequestEventArgs.Response = PlatformArgs.Sender.Environment.CreateWebResourceResponse(
163+
data?.AsRandomAccessStream(),
164+
code,
165+
reason,
166+
PlatformHeaders(headers));
167+
168+
// let the webview know
169+
deferral.Complete();
170+
171+
#elif IOS || MACCATALYST
172+
173+
// iOS and MacCatalyst will just wait until DidFinish is called
174+
var task = PlatformArgs.UrlSchemeTask;
175+
176+
// create and send the response headers
177+
task.DidReceiveResponse(new Foundation.NSHttpUrlResponse(
178+
PlatformArgs.Request.Url,
179+
code,
180+
"HTTP/1.1",
181+
PlatformHeaders(headers)));
182+
183+
// get the actual content
184+
var data = await contentTask;
185+
186+
// send the data
187+
if (data is not null && Foundation.NSData.FromStream(data) is { } nsdata)
188+
{
189+
task.DidReceiveData(nsdata);
190+
}
191+
192+
// let the webview know
193+
task.DidFinish();
194+
195+
#elif ANDROID
196+
197+
// Android requires that we return immediately, even if the data is coming later
198+
199+
// get the actual content
200+
var stream = new AsyncStream(contentTask, null);
201+
202+
// create and send the response headers
203+
var platformHeaders = PlatformHeaders(headers, out var contentType);
204+
PlatformArgs.Response = new global::Android.Webkit.WebResourceResponse(
205+
contentType,
206+
"UTF-8",
207+
code,
208+
reason,
209+
platformHeaders,
210+
stream);
211+
212+
#endif
213+
}
214+
215+
#if WINDOWS
216+
static string? PlatformHeaders(IReadOnlyDictionary<string, string>? headers)
217+
{
218+
if (headers?.Count > 0)
219+
{
220+
var sb = new StringBuilder();
221+
foreach (var header in headers)
222+
{
223+
sb.AppendLine($"{header.Key}: {header.Value}");
224+
}
225+
return sb.ToString();
226+
}
227+
return null;
228+
}
229+
#elif IOS || MACCATALYST
230+
static Foundation.NSMutableDictionary? PlatformHeaders(IReadOnlyDictionary<string, string>? headers)
231+
{
232+
if (headers?.Count > 0)
233+
{
234+
var dic = new Foundation.NSMutableDictionary();
235+
foreach (var header in headers)
236+
{
237+
dic.Add((Foundation.NSString)header.Key, (Foundation.NSString)header.Value);
238+
}
239+
return dic;
240+
}
241+
return null;
242+
}
243+
#elif ANDROID
244+
static global::Android.Runtime.JavaDictionary<string, string>? PlatformHeaders(IReadOnlyDictionary<string, string>? headers, out string contentType)
245+
{
246+
contentType = "application/octet-stream";
247+
if (headers?.Count > 0)
248+
{
249+
var dic = new global::Android.Runtime.JavaDictionary<string, string>();
250+
foreach (var header in headers)
251+
{
252+
if ("Content-Type".Equals(header.Key, StringComparison.OrdinalIgnoreCase))
253+
{
254+
contentType = header.Value;
255+
}
256+
257+
dic.Add(header.Key, header.Value);
258+
}
259+
return dic;
260+
}
261+
return null;
262+
}
263+
#endif
264+
}
265+
266+
/// <summary>
267+
/// Extension methods for the <see cref="HybridWebViewWebResourceRequestedEventArgs"/> class.
268+
/// </summary>
269+
public static class HybridWebViewWebResourceRequestedEventArgsExtensions
270+
{
271+
/// <summary>
272+
/// Sets the response for the web resource request with a status code and reason.
273+
/// </summary>
274+
/// <param name="e">The event arguments.</param>
275+
/// <param name="code">The HTTP status code for the response.</param>
276+
/// <param name="reason">The reason phrase for the response.</param>
277+
public static void SetResponse(this HybridWebViewWebResourceRequestedEventArgs e, int code, string reason) =>
278+
e.SetResponse(code, reason, null, (Stream?)null);
279+
280+
/// <summary>
281+
/// Sets the response for the web resource request with a status code, reason, and content type.
282+
/// </summary>
283+
/// <param name="e">The event arguments.</param>
284+
/// <param name="code">The HTTP status code for the response.</param>
285+
/// <param name="reason">The reason phrase for the response.</param>
286+
/// <param name="contentType">The content type of the response.</param>
287+
/// <param name="content">The content of the response as a stream.</param>
288+
public static void SetResponse(this HybridWebViewWebResourceRequestedEventArgs e, int code, string reason, string contentType, Stream? content) =>
289+
e.SetResponse(code, reason, new Dictionary<string, string> { ["Content-Type"] = contentType }, content);
290+
291+
/// <summary>
292+
/// Sets the response for the web resource request with a status code, reason, and content type.
293+
/// </summary>
294+
/// <param name="e">The event arguments.</param>
295+
/// <param name="code">The HTTP status code for the response.</param>
296+
/// <param name="reason">The reason phrase for the response.</param>
297+
/// <param name="contentType">The content type of the response.</param>
298+
/// <param name="contentTask">A task that represents the asynchronous operation of getting the response content.</param>
299+
public static void SetResponse(this HybridWebViewWebResourceRequestedEventArgs e, int code, string reason, string contentType, Task<Stream?> contentTask) =>
300+
e.SetResponse(code, reason, new Dictionary<string, string> { ["Content-Type"] = contentType }, contentTask);
301+
}

0 commit comments

Comments
 (0)