|
5 | 5 | using System.Linq;
|
6 | 6 | using System.Net;
|
7 | 7 | using System.Net.Http;
|
| 8 | +using System.Net.Sockets; |
| 9 | +using System.Text; |
8 | 10 | using System.Text.Json;
|
9 | 11 | using System.Text.Json.Serialization;
|
10 | 12 | using Microsoft.AspNetCore.Http;
|
| 13 | +using System.Dynamic; |
11 | 14 |
|
12 | 15 | namespace Microsoft.AspNetCore.Components.WebAssembly.Server;
|
13 | 16 |
|
@@ -37,6 +40,221 @@ public TargetPickerUi([StringSyntax(StringSyntaxAttribute.Uri)] string debugProx
|
37 | 40 | _browserHost = devToolsHost;
|
38 | 41 | }
|
39 | 42 |
|
| 43 | + /// <summary> |
| 44 | + /// Display the ui. |
| 45 | + /// </summary> |
| 46 | + /// <param name="context">The <see cref="HttpContext"/>.</param> |
| 47 | + /// <returns>The <see cref="Task"/>.</returns> |
| 48 | + public async Task DisplayFirefox(HttpContext context) |
| 49 | + { |
| 50 | + static async Task SendMessageToBrowser(NetworkStream toStream, ExpandoObject args, CancellationToken token) |
| 51 | + { |
| 52 | + var msg = JsonSerializer.Serialize(args); |
| 53 | + var bytes = Encoding.UTF8.GetBytes(msg); |
| 54 | + var bytesWithHeader = Encoding.UTF8.GetBytes($"{bytes.Length}:").Concat(bytes).ToArray(); |
| 55 | + await toStream.WriteAsync(bytesWithHeader, token).AsTask(); |
| 56 | + } |
| 57 | +#pragma warning disable CA1835 |
| 58 | + static async Task<string> ReceiveMessageLoop(TcpClient browserDebugClientConnect, CancellationToken token) |
| 59 | + { |
| 60 | + var toStream = browserDebugClientConnect.GetStream(); |
| 61 | + var bytesRead = 0; |
| 62 | + var _lengthBuffer = new byte[10]; |
| 63 | + while (bytesRead == 0 || Convert.ToChar(_lengthBuffer[bytesRead - 1]) != ':') |
| 64 | + { |
| 65 | + if (!browserDebugClientConnect.Connected) |
| 66 | + { |
| 67 | + return ""; |
| 68 | + } |
| 69 | + |
| 70 | + if (bytesRead + 1 > _lengthBuffer.Length) |
| 71 | + { |
| 72 | + throw new IOException($"Protocol error: did not get the expected length preceding a message, " + |
| 73 | + $"after reading {bytesRead} bytes. Instead got: {Encoding.UTF8.GetString(_lengthBuffer)}"); |
| 74 | + } |
| 75 | + |
| 76 | + int readLen = await toStream.ReadAsync(_lengthBuffer, bytesRead, 1, token); |
| 77 | + bytesRead += readLen; |
| 78 | + } |
| 79 | + string str = Encoding.UTF8.GetString(_lengthBuffer, 0, bytesRead - 1); |
| 80 | + if (!int.TryParse(str, out int messageLen)) |
| 81 | + { |
| 82 | + return ""; |
| 83 | + } |
| 84 | + byte[] buffer = new byte[messageLen]; |
| 85 | + bytesRead = await toStream.ReadAsync(buffer, 0, messageLen, token); |
| 86 | + while (bytesRead != messageLen) |
| 87 | + { |
| 88 | + if (!browserDebugClientConnect.Connected) |
| 89 | + { |
| 90 | + return ""; |
| 91 | + } |
| 92 | + bytesRead += await toStream.ReadAsync(buffer, bytesRead, messageLen - bytesRead, token); |
| 93 | + } |
| 94 | + var messageReceived = Encoding.UTF8.GetString(buffer, 0, messageLen); |
| 95 | + return messageReceived; |
| 96 | + } |
| 97 | + static async Task EvaluateOnBrowser(NetworkStream toStream, string? to, string text, CancellationToken token) |
| 98 | + { |
| 99 | + dynamic message = new ExpandoObject(); |
| 100 | + dynamic options = new ExpandoObject(); |
| 101 | + dynamic awaitObj = new ExpandoObject(); |
| 102 | + awaitObj.@await = true; |
| 103 | + options.eager = true; |
| 104 | + options.mapped = awaitObj; |
| 105 | + message.to = to; |
| 106 | + message.type = "evaluateJSAsync"; |
| 107 | + message.text = text; |
| 108 | + message.options = options; |
| 109 | + await SendMessageToBrowser(toStream, message, token); |
| 110 | + } |
| 111 | +#pragma warning restore CA1835 |
| 112 | + |
| 113 | + context.Response.ContentType = "text/html"; |
| 114 | + var request = context.Request; |
| 115 | + var targetApplicationUrl = request.Query["url"]; |
| 116 | + var browserDebugClientConnect = new TcpClient(); |
| 117 | + if (IPEndPoint.TryParse(_debugProxyUrl, out IPEndPoint? endpoint)) |
| 118 | + { |
| 119 | + try |
| 120 | + { |
| 121 | + await browserDebugClientConnect.ConnectAsync(endpoint.Address, 6000); |
| 122 | + } |
| 123 | + catch (Exception) |
| 124 | + { |
| 125 | + context.Response.StatusCode = 404; |
| 126 | + await context.Response.WriteAsync($@"WARNING: |
| 127 | +Open about:config: |
| 128 | +- enable devtools.debugger.remote-enabled |
| 129 | +- enable devtools.chrome.enabled |
| 130 | +- disable devtools.debugger.prompt-connection |
| 131 | +Open firefox with remote debugging enabled on port 6000: |
| 132 | +firefox --start-debugger-server 6000 -new-tab about:debugging"); |
| 133 | + return; |
| 134 | + } |
| 135 | + var source = new CancellationTokenSource(); |
| 136 | + var token = source.Token; |
| 137 | + var toStream = browserDebugClientConnect.GetStream(); |
| 138 | + dynamic messageListTabs = new ExpandoObject(); |
| 139 | + messageListTabs.type = "listTabs"; |
| 140 | + messageListTabs.to = "root"; |
| 141 | + await SendMessageToBrowser(toStream, messageListTabs, token); |
| 142 | + var tabToRedirect = -1; |
| 143 | + var foundAboutDebugging = false; |
| 144 | + string? consoleActorId = null; |
| 145 | + string? toCmd = null; |
| 146 | + while (browserDebugClientConnect.Connected) |
| 147 | + { |
| 148 | + var res = System.Text.Json.JsonDocument.Parse(await ReceiveMessageLoop(browserDebugClientConnect, token)).RootElement; |
| 149 | + var hasTabs = res.TryGetProperty("tabs", out var tabs); |
| 150 | + var hasType = res.TryGetProperty("type", out var type); |
| 151 | + if (hasType && type.GetString()?.Equals("tabListChanged", StringComparison.Ordinal) == true) |
| 152 | + { |
| 153 | + await SendMessageToBrowser(toStream, messageListTabs, token); |
| 154 | + } |
| 155 | + else |
| 156 | + { |
| 157 | + if (hasTabs) |
| 158 | + { |
| 159 | + var tabsList = tabs.Deserialize<JsonElement[]>(); |
| 160 | + if (tabsList == null) |
| 161 | + { |
| 162 | + continue; |
| 163 | + } |
| 164 | + foreach (var tab in tabsList) |
| 165 | + { |
| 166 | + var hasUrl = tab.TryGetProperty("url", out var urlInTab); |
| 167 | + var hasActor = tab.TryGetProperty("actor", out var actorInTab); |
| 168 | + var hasBrowserId = tab.TryGetProperty("browserId", out var browserIdInTab); |
| 169 | + if (string.IsNullOrEmpty(consoleActorId)) |
| 170 | + { |
| 171 | + if (hasUrl && urlInTab.GetString()?.StartsWith("about:debugging#", StringComparison.InvariantCultureIgnoreCase) == true) |
| 172 | + { |
| 173 | + foundAboutDebugging = true; |
| 174 | + |
| 175 | + toCmd = hasActor ? actorInTab.GetString() : ""; |
| 176 | + if (tabToRedirect != -1) |
| 177 | + { |
| 178 | + break; |
| 179 | + } |
| 180 | + } |
| 181 | + if (hasUrl && urlInTab.GetString()?.Equals(targetApplicationUrl, StringComparison.Ordinal) == true) |
| 182 | + { |
| 183 | + tabToRedirect = hasBrowserId ? browserIdInTab.GetInt32() : -1; |
| 184 | + if (foundAboutDebugging) |
| 185 | + { |
| 186 | + break; |
| 187 | + } |
| 188 | + } |
| 189 | + } |
| 190 | + else if (hasUrl && urlInTab.GetString()?.StartsWith("about:devtools", StringComparison.InvariantCultureIgnoreCase) == true) |
| 191 | + { |
| 192 | + return; |
| 193 | + } |
| 194 | + } |
| 195 | + if (!foundAboutDebugging) |
| 196 | + { |
| 197 | + context.Response.StatusCode = 404; |
| 198 | + await context.Response.WriteAsync("WARNING: Open about:debugging tab before pressing Debugging Hotkey"); |
| 199 | + return; |
| 200 | + } |
| 201 | + if (string.IsNullOrEmpty(consoleActorId)) |
| 202 | + { |
| 203 | + await EvaluateOnBrowser(toStream, consoleActorId, $"if (AboutDebugging.store.getState().runtimes.networkRuntimes.find(element => element.id == \"{_debugProxyUrl}\").runtimeDetails !== null) {{ AboutDebugging.actions.selectPage(\"runtime\", \"{_debugProxyUrl}\"); if (AboutDebugging.store.getState().runtimes.selectedRuntimeId == \"{_debugProxyUrl}\") AboutDebugging.actions.inspectDebugTarget(\"tab\", {tabToRedirect})}};", token); |
| 204 | + } |
| 205 | + } |
| 206 | + } |
| 207 | + if (!string.IsNullOrEmpty(consoleActorId)) |
| 208 | + { |
| 209 | + var hasInput = res.TryGetProperty("input", out var input); |
| 210 | + if (hasInput && input.GetString()?.StartsWith("AboutDebugging.actions.addNetworkLocation(", StringComparison.InvariantCultureIgnoreCase) == true) |
| 211 | + { |
| 212 | + await EvaluateOnBrowser(toStream, consoleActorId, $"if (AboutDebugging.store.getState().runtimes.networkRuntimes.find(element => element.id == \"{_debugProxyUrl}\").runtimeDetails !== null) {{ AboutDebugging.actions.selectPage(\"runtime\", \"{_debugProxyUrl}\"); if (AboutDebugging.store.getState().runtimes.selectedRuntimeId == \"{_debugProxyUrl}\") AboutDebugging.actions.inspectDebugTarget(\"tab\", {tabToRedirect})}};", token); |
| 213 | + } |
| 214 | + if (hasInput && input.GetString()?.StartsWith("if (AboutDebugging.store.getState()", StringComparison.InvariantCultureIgnoreCase) == true) |
| 215 | + { |
| 216 | + await EvaluateOnBrowser(toStream, consoleActorId, $"if (AboutDebugging.store.getState().runtimes.networkRuntimes.find(element => element.id == \"{_debugProxyUrl}\").runtimeDetails !== null) {{ AboutDebugging.actions.selectPage(\"runtime\", \"{_debugProxyUrl}\"); if (AboutDebugging.store.getState().runtimes.selectedRuntimeId == \"{_debugProxyUrl}\") AboutDebugging.actions.inspectDebugTarget(\"tab\", {tabToRedirect})}};", token); |
| 217 | + } |
| 218 | + } |
| 219 | + else |
| 220 | + { |
| 221 | + var hasTarget = res.TryGetProperty("target", out var target); |
| 222 | + JsonElement consoleActor = new(); |
| 223 | + var hasConsoleActor = hasTarget && target.TryGetProperty("consoleActor", out consoleActor); |
| 224 | + var hasActor = res.TryGetProperty("actor", out var actor); |
| 225 | + if (hasConsoleActor && !string.IsNullOrEmpty(consoleActor.GetString())) |
| 226 | + { |
| 227 | + consoleActorId = consoleActor.GetString(); |
| 228 | + await EvaluateOnBrowser(toStream, consoleActorId, $"AboutDebugging.actions.addNetworkLocation(\"{_debugProxyUrl}\"); AboutDebugging.actions.connectRuntime(\"{_debugProxyUrl}\");", token); |
| 229 | + } |
| 230 | + else if (hasActor && !string.IsNullOrEmpty(actor.GetString())) |
| 231 | + { |
| 232 | + dynamic messageWatchTargets = new ExpandoObject(); |
| 233 | + messageWatchTargets.type = "watchTargets"; |
| 234 | + messageWatchTargets.targetType = "frame"; |
| 235 | + messageWatchTargets.to = actor.GetString(); |
| 236 | + await SendMessageToBrowser(toStream, messageWatchTargets, token); |
| 237 | + dynamic messageWatchResources = new ExpandoObject(); |
| 238 | + messageWatchResources.type = "watchResources"; |
| 239 | + messageWatchResources.resourceTypes = new string[1] { "console-message" }; |
| 240 | + messageWatchResources.to = actor.GetString(); |
| 241 | + await SendMessageToBrowser(toStream, messageWatchResources, token); |
| 242 | + } |
| 243 | + else if (!string.IsNullOrEmpty(toCmd)) |
| 244 | + { |
| 245 | + dynamic messageGetWatcher = new ExpandoObject(); |
| 246 | + messageGetWatcher.type = "getWatcher"; |
| 247 | + messageGetWatcher.isServerTargetSwitchingEnabled = true; |
| 248 | + messageGetWatcher.to = toCmd; |
| 249 | + await SendMessageToBrowser(toStream, messageGetWatcher, token); |
| 250 | + } |
| 251 | + } |
| 252 | + } |
| 253 | + |
| 254 | + } |
| 255 | + return; |
| 256 | + } |
| 257 | + |
40 | 258 | /// <summary>
|
41 | 259 | /// Display the ui.
|
42 | 260 | /// </summary>
|
|
0 commit comments