Skip to content

Commit 9b25d52

Browse files
committed
Add the ability to intercept web requests
1 parent a14c225 commit 9b25d52

File tree

20 files changed

+1350
-270
lines changed

20 files changed

+1350
-270
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: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System;
1+
#pragma warning disable RS0016 // Add public types and members to the declared API
2+
using System;
23
using System.Diagnostics.CodeAnalysis;
34
using System.Text.Json;
45
using System.Text.Json.Serialization.Metadata;
@@ -66,6 +67,16 @@ void IHybridWebView.RawMessageReceived(string rawMessage)
6667
/// </summary>
6768
public event EventHandler<HybridWebViewRawMessageReceivedEventArgs>? RawMessageReceived;
6869

70+
bool IHybridWebView.WebResourceRequested(WebResourceRequestedEventArgs args)
71+
{
72+
var platformArgs = new PlatformHybridWebViewWebResourceRequestedEventArgs(args);
73+
var e = new HybridWebViewWebResourceRequestedEventArgs(platformArgs);
74+
WebResourceRequested?.Invoke(this, e);
75+
return e.Handled;
76+
}
77+
78+
public event EventHandler<HybridWebViewWebResourceRequestedEventArgs>? WebResourceRequested;
79+
6980
/// <summary>
7081
/// Sends a raw message to the code running in the web view. Raw messages have no additional processing.
7182
/// </summary>
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
#pragma warning disable RS0016 // Add public types and members to the declared API
2+
using System;
3+
using System.Collections.Generic;
4+
using System.IO;
5+
using System.Text;
6+
using System.Threading.Tasks;
7+
8+
namespace Microsoft.Maui.Controls;
9+
10+
public class HybridWebViewWebResourceRequestedEventArgs
11+
{
12+
IReadOnlyDictionary<string, string>? _headers;
13+
IReadOnlyDictionary<string, string>? _queryParams;
14+
15+
internal HybridWebViewWebResourceRequestedEventArgs(PlatformHybridWebViewWebResourceRequestedEventArgs platformArgs)
16+
{
17+
PlatformArgs = platformArgs;
18+
Uri = platformArgs.GetRequestUri() is string uri ? new Uri(uri) : throw new InvalidOperationException("Platform web request did not have a request URI.");
19+
Method = platformArgs.GetRequestMethod() ?? throw new InvalidOperationException("Platform web request did not have a request METHOD.");
20+
}
21+
22+
public HybridWebViewWebResourceRequestedEventArgs(Uri uri, string method)
23+
{
24+
Uri = uri;
25+
Method = method;
26+
}
27+
28+
public PlatformHybridWebViewWebResourceRequestedEventArgs? PlatformArgs { get; }
29+
30+
public Uri Uri { get; }
31+
32+
public string Method { get; }
33+
34+
public IReadOnlyDictionary<string, string> Headers =>
35+
_headers ??= PlatformArgs?.GetRequestHeaders() ?? new Dictionary<string, string>();
36+
37+
public IReadOnlyDictionary<string, string> QueryParameters =>
38+
_queryParams ??= WebUtils.ParseQueryString(Uri, false) ?? new Dictionary<string, string>();
39+
40+
public bool Handled { get; set; }
41+
42+
public void SetResponse(int code, string reason, IReadOnlyDictionary<string, string>? headers, Stream? content)
43+
{
44+
_ = PlatformArgs ?? throw new InvalidOperationException("Platform web request was not valid.");
45+
46+
#if WINDOWS
47+
48+
// create the response
49+
PlatformArgs.RequestEventArgs.Response = PlatformArgs.Sender.Environment.CreateWebResourceResponse(
50+
content?.AsRandomAccessStream(),
51+
code,
52+
reason,
53+
PlatformHeaders(headers));
54+
55+
#elif IOS || MACCATALYST
56+
57+
// iOS and MacCatalyst will just wait until DidFinish is called
58+
var task = PlatformArgs.UrlSchemeTask;
59+
60+
// create and send the response headers
61+
task.DidReceiveResponse(new Foundation.NSHttpUrlResponse(
62+
PlatformArgs.Request.Url,
63+
code,
64+
"HTTP/1.1",
65+
PlatformHeaders(headers)));
66+
67+
// send the data
68+
if (content is not null && Foundation.NSData.FromStream(content) is { } nsdata)
69+
{
70+
task.DidReceiveData(nsdata);
71+
}
72+
73+
// let the webview know
74+
task.DidFinish();
75+
76+
#elif ANDROID
77+
78+
// Android requires that we return immediately, even if the data is coming later
79+
80+
// create and send the response headers
81+
var platformHeaders = PlatformHeaders(headers, out var contentType);
82+
PlatformArgs.Response = new global::Android.Webkit.WebResourceResponse(
83+
contentType,
84+
"UTF-8",
85+
code,
86+
reason,
87+
platformHeaders,
88+
content);
89+
90+
#endif
91+
}
92+
public void SetResponse(int code, string reason, IReadOnlyDictionary<string, string>? headers, Task<Stream?> contentTask) =>
93+
SetResponseAsync(code, reason, headers, contentTask).FireAndForget();
94+
95+
#pragma warning disable CS1998 // Android implememntation does not use async/await
96+
async Task SetResponseAsync(int code, string reason, IReadOnlyDictionary<string, string>? headers, Task<Stream?> contentTask)
97+
#pragma warning restore CS1998
98+
{
99+
_ = PlatformArgs ?? throw new InvalidOperationException("Platform web request was not valid.");
100+
101+
#if WINDOWS
102+
103+
// Windows uses a deferral to let the webview know that we are going to be async
104+
using var deferral = PlatformArgs.RequestEventArgs.GetDeferral();
105+
106+
// get the actual content
107+
var data = await contentTask;
108+
109+
// create the response
110+
PlatformArgs.RequestEventArgs.Response = PlatformArgs.Sender.Environment.CreateWebResourceResponse(
111+
data?.AsRandomAccessStream(),
112+
code,
113+
reason,
114+
PlatformHeaders(headers));
115+
116+
// let the webview know
117+
deferral.Complete();
118+
119+
#elif IOS || MACCATALYST
120+
121+
// iOS and MacCatalyst will just wait until DidFinish is called
122+
var task = PlatformArgs.UrlSchemeTask;
123+
124+
// create and send the response headers
125+
task.DidReceiveResponse(new Foundation.NSHttpUrlResponse(
126+
PlatformArgs.Request.Url,
127+
code,
128+
"HTTP/1.1",
129+
PlatformHeaders(headers)));
130+
131+
// get the actual content
132+
var data = await contentTask;
133+
134+
// send the data
135+
if (data is not null && Foundation.NSData.FromStream(data) is { } nsdata)
136+
{
137+
task.DidReceiveData(nsdata);
138+
}
139+
140+
// let the webview know
141+
task.DidFinish();
142+
143+
#elif ANDROID
144+
145+
// Android requires that we return immediately, even if the data is coming later
146+
147+
// get the actual content
148+
var stream = new AsyncStream(contentTask, null);
149+
150+
// create and send the response headers
151+
var platformHeaders = PlatformHeaders(headers, out var contentType);
152+
PlatformArgs.Response = new global::Android.Webkit.WebResourceResponse(
153+
contentType,
154+
"UTF-8",
155+
code,
156+
reason,
157+
platformHeaders,
158+
stream);
159+
160+
#endif
161+
}
162+
163+
#if WINDOWS
164+
static string? PlatformHeaders(IReadOnlyDictionary<string, string>? headers)
165+
{
166+
if (headers?.Count > 0)
167+
{
168+
var sb = new StringBuilder();
169+
foreach (var header in headers)
170+
{
171+
sb.AppendLine($"{header.Key}: {header.Value}");
172+
}
173+
return sb.ToString();
174+
}
175+
return null;
176+
}
177+
#elif IOS || MACCATALYST
178+
static Foundation.NSMutableDictionary? PlatformHeaders(IReadOnlyDictionary<string, string>? headers)
179+
{
180+
if (headers?.Count > 0)
181+
{
182+
var dic = new Foundation.NSMutableDictionary();
183+
foreach (var header in headers)
184+
{
185+
dic.Add((Foundation.NSString)header.Key, (Foundation.NSString)header.Value);
186+
}
187+
return dic;
188+
}
189+
return null;
190+
}
191+
#elif ANDROID
192+
static global::Android.Runtime.JavaDictionary<string, string>? PlatformHeaders(IReadOnlyDictionary<string, string>? headers, out string contentType)
193+
{
194+
contentType = "application/octet-stream";
195+
if (headers?.Count > 0)
196+
{
197+
var dic = new global::Android.Runtime.JavaDictionary<string, string>();
198+
foreach (var header in headers)
199+
{
200+
if ("Content-Type".Equals(header.Key, StringComparison.OrdinalIgnoreCase))
201+
{
202+
contentType = header.Value;
203+
}
204+
205+
dic.Add(header.Key, header.Value);
206+
}
207+
return dic;
208+
}
209+
return null;
210+
}
211+
#endif
212+
}
213+
214+
public static class HybridWebViewWebResourceRequestedEventArgsExtensions
215+
{
216+
public static void SetResponse(this HybridWebViewWebResourceRequestedEventArgs e, int code, string reason, string contentType, Task<Stream?> contentTask) =>
217+
e.SetResponse(code, reason, new Dictionary<string, string> { ["Content-Type"] = contentType }, contentTask);
218+
219+
public static void SetResponse(this HybridWebViewWebResourceRequestedEventArgs e, int code, string reason) =>
220+
e.SetResponse(code, reason, null, (Stream?)null);
221+
222+
public static void SetResponse(this HybridWebViewWebResourceRequestedEventArgs e, int code, string reason, string contentType, Stream? content) =>
223+
e.SetResponse(code, reason, new Dictionary<string, string> { ["Content-Type"] = contentType }, content);
224+
225+
public static void SetResponse(this HybridWebViewWebResourceRequestedEventArgs e, int code, string reason, IReadOnlyDictionary<string, string>? headers, byte[]? content) =>
226+
e.SetResponse(code, reason, headers, content is null ? null : new MemoryStream(content));
227+
228+
public static void SetResponse(this HybridWebViewWebResourceRequestedEventArgs e, int code, string reason, string contentType, byte[]? content) =>
229+
e.SetResponse(code, reason, contentType, content is null ? null : new MemoryStream(content));
230+
}

0 commit comments

Comments
 (0)