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