GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
EdgePatcher.cs
1// =============================================================================
2// GPAL - Generally Positive Automation Library
3// Copyright © 2026 Software Decisions, Inc. All rights reserved.
4//
5// This file is part of GPAL.
6// Licensed under the Business Source License 1.1
7//
8// Primary development, architecture, and vision by Michael B. Vederman,
9// CEO of Software Decisions, Inc., Texas.
10//
11// Internal development maintained privately.
12// Public releases appear on GitHub: https://github.com/SoftwareDecisionsInc/GPAL.
13//
14// See LICENSE for full terms, including Additional Use Grant.
15// =============================================================================
16
17using System;
18using System.Collections.Generic;
19using System.IO;
20using System.Linq;
21using System.Text;
22using System.Text.RegularExpressions;
23using System.Threading.Tasks;
25
27{
35 internal static class EdgePatcher
36 {
42 static readonly string[] edgedriverSourceMatches = new[]
43 {
44 "WebDriver"
45 , "W3C"
46 , "Execute-Script"
47 , "Chromium"
48 //, "shadow-6066-11e4-a52e-4f735466cecf" // part of w3c protocol - other scripts could use this, change breaks selenium - use tostring override
49 //, "element-6066-11e4-a52e-4f735466cecf" // part of w3c protocol - other scripts could use this, change breaks selenium - use tostring override
50 , "STALE_ELEMENT_REFERENCE" // enum will still work with munged name
51 , "crbug.com"
52 , "shadow root is detached from the current frame"
53 , "stale element not found in the current frame"
54 };
55
63 public static void PatchEdgeDriver(IBrowser browser)
64 {
65 if (null == ((Browser)browser).BrowserSettings.DriverLocation)
66 // driver is located with executable
67 ((Browser)browser).BrowserSettings.DriverLocation = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase).Substring(6); // skip file:\\
68
69 string filePath = FileHelper.EnsureDirectoryEndsWithBackslash(((Browser)browser).BrowserSettings.DriverLocation) + $@"{GPAL.GPALSettings.EdgeDriverFilename}";
70
71 long fileSize = new FileInfo(filePath).Length;
72 int totalSteps = 6 + edgedriverSourceMatches.Length;
73 GPAL.PublishSimpleEvent(Enums.GPALEventType.INFO, $"Stealth patching [{filePath}] ({fileSize / 1024 / 1024} MB, {totalSteps} steps)", browser, Enums.GPALObjectType.Browser);
74
75 if (true == CheckIfPatched(filePath))
76 {
77 GPAL.PublishSimpleEvent(Enums.GPALEventType.INFO, $"[{filePath}] already patched.", browser, Enums.GPALObjectType.Browser);
78 return;
79 }
80
81 // Create a backup of the original file before patching
82 string backupPath = filePath + ".org";
83 if (File.Exists(backupPath))
84 {
85 GPAL.PublishSimpleEvent(Enums.GPALEventType.INFO, $"[{backupPath}] backup already exists. Deleting.", browser, Enums.GPALObjectType.Browser);
86 File.Delete(backupPath);
87 }
88
89 File.Copy(filePath, backupPath);
90 GPAL.PublishSimpleEvent(Enums.GPALEventType.INFO, $"[{backupPath}] backup created.", browser, Enums.GPALObjectType.Browser);
91
92 Action<int, int, string> onProgress = (step, total, description) =>
93 GPAL.PublishSimpleEvent(Enums.GPALEventType.INFO, $"Patch step [{step}/{total}]: {description}", browser, Enums.GPALObjectType.Browser);
94
95 // Patch the executable file directly (no renaming the patched file)
96 if (PatchExe(filePath, onProgress))
97 {
98 if (true == CheckIfPatched(filePath))
99 GPAL.PublishSimpleEvent(Enums.GPALEventType.INFO, $"[{filePath}] patched successfully.", browser, Enums.GPALObjectType.Browser);
100 else
101 GPAL.PublishSimpleEvent(Enums.GPALEventType.ERROR, $"[{filePath}] patching failed.", browser, Enums.GPALObjectType.Browser);
102 }
103 else
104 GPAL.PublishSimpleEvent(Enums.GPALEventType.ERROR, $"[{filePath}] patching failed.", browser, Enums.GPALObjectType.Browser);
105 }
106
113 static bool PatchExe(string executablePath, Action<int, int, string> onProgress)
114 {
115 // Read the binary content of the file
116 byte[] content = File.ReadAllBytes(executablePath);
117
118 // Apply the regex replacements
119 content = ApplyRegexPatch(content, onProgress);
120
121 // Write the patched content back to the original file
122 File.WriteAllBytes(executablePath, content);
123 return true;
124 }
125
137 static byte[] ApplyRegexPatch(byte[] content, Action<int, int, string> onProgress)
138 {
139 int total = 6 + edgedriverSourceMatches.Length;
140 int step = 0;
141
142 onProgress(++step, total, "cdc variable assignments");
143 content = ApplyPattern(content,
144 @"window\.cdc_[a-zA-Z0-9]{22,}_(Array|Promise|Symbol|Object|Proxy|JSON) = window\.(Array|Promise|Symbol|Object|Proxy|JSON);",
145 match => new string('\n', match.Length));
146
147 onProgress(++step, total, "cdc variable guards");
148 content = ApplyPattern(content,
149 @"window\.cdc_[a-zA-Z0-9]{22,}_(Array|Promise|Symbol|Object|Proxy|JSON) \|\|",
150 match => new string('\n', match.Length));
151
152 onProgress(++step, total, "cdc Window references");
153 content = ApplyPattern(content,
154 @"window\.cdc_[a-zA-Z0-9]{22,}_Window",
155 match => "window.Window");
156
157 onProgress(++step, total, "WindowProxy binding");
158 content = ApplyPattern(content,
159 @"let WindowProxy = window\.cdc_[a-zA-Z0-9]{22,}_Window(?:\s*\|\|\s*window\.Window;)?",
160 match => "let WindowProxy = window.Window");
161
162 onProgress(++step, total, "item.cdc Window references");
163 content = ApplyPattern(content,
164 @"item\.cdc_[a-zA-Z0-9]{22,}_Window",
165 match => "item.Window");
166
167 onProgress(++step, total, "console.log stamp");
168 byte[] logMessageBytes = Encoding.ASCII.GetBytes($"{{console.log(\"GPAL {GPAL.Version} msedgedriver patch!\")}}");
169 content = ApplyPattern(content,
170 @"console\.log\‍(([^)]+)\‍)",
171 match =>
172 {
173 byte[] newTargetBytes = new byte[match.Length];
174 Array.Copy(logMessageBytes, 0, newTargetBytes, 0, logMessageBytes.Length);
175 for (int i = logMessageBytes.Length; i < match.Length; i++)
176 {
177 newTargetBytes[i] = (byte)' '; // Fill the rest with space characters
178 }
179 return Encoding.ASCII.GetString(newTargetBytes);
180 });
181
182 foreach (var originalString in edgedriverSourceMatches)
183 {
184 onProgress(++step, total, $"string [{originalString}]");
185 string alteredString = AlterString(originalString);
186 content = ApplyPattern(content, Regex.Escape(originalString), _ => alteredString);
187 }
188
189 return content;
190 }
199 static string AlterString(string input)
200 {
201 if (string.IsNullOrEmpty(input)) return input;
202
203 // Shift the first character by one
204 char firstChar = input[0];
205 char shiftedChar = (char)(firstChar + 1);
206
207 // Handle wrap-around for alphabets
208 if (char.IsLetter(firstChar))
209 {
210 if (char.IsLower(firstChar) && shiftedChar > 'z') shiftedChar = 'a';
211 if (char.IsUpper(firstChar) && shiftedChar > 'Z') shiftedChar = 'A';
212 }
213 // Handle wrap-around for digits
214 else if (char.IsDigit(firstChar))
215 {
216 if (shiftedChar > '9') shiftedChar = '0';
217 }
218
219 // Reconstruct the string
220 return shiftedChar + input.Substring(1);
221 }
222
233 static byte[] ApplyPattern(byte[] content, string pattern, Func<Match, string> replacementFunc)
234 {
235 // Compile regex
236 Regex regex = new Regex(pattern, RegexOptions.Compiled);
237 string contentStr = Encoding.UTF8.GetString(content);
238 MatchCollection matches = regex.Matches(contentStr);
239
240 foreach (Match match in matches)
241 {
242 byte[] targetBytes = Encoding.UTF8.GetBytes(match.Value);
243 string replacement = replacementFunc(match);
244 byte[] replacementBytes = Encoding.UTF8.GetBytes(replacement);
245
246 // Pad the replacementBytes to match the targetBytes length
247 byte[] paddedReplacementBytes = PadByteArray(replacementBytes, targetBytes.Length);
248
249 content = ReplaceBytes(content, targetBytes, paddedReplacementBytes);
250 }
251
252 return content;
253 }
254
263 static byte[] PadByteArray(byte[] byteArray, int desiredLength)
264 {
265 if (byteArray.Length >= desiredLength)
266 return byteArray;
267
268 byte[] paddedArray = new byte[desiredLength];
269 byteArray.CopyTo(paddedArray, 0);
270 for (int i = byteArray.Length; i < desiredLength; i++)
271 {
272 paddedArray[i] = (byte)' '; // Pad with space character
273 }
274 return paddedArray;
275 }
276
285 static byte[] ReplaceBytes(byte[] content, byte[] oldBytes, byte[] newBytes)
286 {
287 using (MemoryStream stream = new MemoryStream())
288 {
289 int index = 0;
290
291 while (index <= content.Length - oldBytes.Length)
292 {
293 bool match = true;
294
295 for (int j = 0; j < oldBytes.Length; j++)
296 {
297 if (content[index + j] != oldBytes[j])
298 {
299 match = false;
300 break;
301 }
302 }
303
304 if (match)
305 {
306 stream.Write(newBytes, 0, newBytes.Length);
307 index += oldBytes.Length;
308 }
309 else
310 {
311 stream.WriteByte(content[index]);
312 index++;
313 }
314 }
315
316 stream.Write(content, index, content.Length - index);
317 return stream.ToArray();
318 }
319 }
327 static bool CheckIfPatched(string filePath)
328 {
329 byte[] content = File.ReadAllBytes(filePath);
330 string contentStr = Encoding.UTF8.GetString(content);
331
332 // Check for the GPAL version message in the console.log
333 if (Regex.IsMatch(contentStr, @"console\.log\‍(\"".*?msedgedriver patch!\""\‍)"))
334 {
335// GPAL.PublishSimpleEvent(Enums.GPALEventType.DEBUG, "EdgeDriver is patched.", filePath, Enums.GPALObjectType.Other);
336 return true;
337 }
338 return false;
339 }
340 }
341}