18using System.Collections.Concurrent;
19using System.Collections.Generic;
21using System.Threading;
22using DocumentFormat.OpenXml.ExtendedProperties;
24using OpenQA.Selenium.Chrome;
25using OpenQA.Selenium.Chromium;
26using OpenQA.Selenium.Support.Extensions;
28using Nager.PublicSuffix;
29using Nager.PublicSuffix.RuleProviders;
48 public static class UrlHelper
50 static bool MatchesOrAny(
this string filter,
string value)
51 =>
string.IsNullOrEmpty(filter) || filter == value;
53 #region Storage Handling
114 private static string UnwrapStorageRead(
string data,
string key)
116 string retVal = data;
120 if (
false ==
string.IsNullOrEmpty(key) &&
false ==
string.IsNullOrWhiteSpace(data) &&
true == data.TrimStart().StartsWith(
"{"))
124 Newtonsoft.Json.Linq.JObject wrapper = Newtonsoft.Json.Linq.JObject.Parse(data);
128 bool isWrapper =
null != wrapper[
"value"]
129 && wrapper.Properties().All(each =>
"value" == each.Name ||
"key" == each.Name);
131 if (
true == isWrapper)
133 Newtonsoft.Json.Linq.JToken value = wrapper[
"value"];
137 retVal = Newtonsoft.Json.Linq.JTokenType.String == value.Type
139 : Newtonsoft.Json.JsonConvert.SerializeObject(value);
151 internal static bool TryPerformAction(IGPALUrl gpalUrl, BrowserSettings browserSettings, StorageAction storageActionParameters, out
string data)
159 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"No GPALUrl supplied to run [{storageActionParameters.StorageType}] [{storageActionParameters.Action}] action", browserSettings, GPALObjectType.Other);
162 else if (
null == storageActionParameters)
164 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"No StorageAction parameters supplied to run on [{gpalUrl.Url}]", browserSettings, GPALObjectType.Other);
168 string defaultOrigin = GetOrigin(gpalUrl.Url);
169 IEnumerable<StorageAction> storageActions =
null;
172 if (
null == storageActionParameters.Domain &&
null == storageActionParameters.Key &&
null == storageActionParameters.Path &&
null == storageActionParameters.StoreName
173 &&
null == storageActionParameters.UserDefined)
175 if (WebsiteStorageType.notSet == storageActionParameters.StorageType)
176 storageActions = gpalUrl.GetActions().Where(a => storageActionParameters.Action == a.Action);
178 storageActions = gpalUrl.GetActions().Where(a => storageActionParameters.StorageType == a.StorageType && storageActionParameters.Action == a.Action);
183 var p = storageActionParameters;
185 storageActions = gpalUrl.GetActions().Where(a =>
186 a.StorageType == p.StorageType &&
187 p.Domain.MatchesOrAny(a.Domain) &&
188 p.Path.MatchesOrAny(a.Path) &&
189 p.Key.MatchesOrAny(a.Key) &&
190 p.StoreName.MatchesOrAny(a.StoreName) &&
191 p.UserDefined.MatchesOrAny(a.UserDefined)
195 foreach (var action
in storageActions)
197 string origin = action.Domain !=
null
198 ? MagicHelper.GetFullUrl(action.Domain,
null, out
bool _,
true)
201 var paramsDict =
new Dictionary<string, object>();
203 if (WebsiteStorageType.cookie == action.StorageType)
204 origin = UrlHelper.GetWildcardCookieDomain(origin);
208 string.IsNullOrWhiteSpace(origin) ? null : $
"d[{origin}]",
209 string.IsNullOrWhiteSpace(action.Path) ? null : $
"p[{action.Path}]",
210 string.IsNullOrWhiteSpace(action.Key) ? null : $
"k[{action.Key}]",
211 string.IsNullOrWhiteSpace(action.StoreName) ? null : $
"sn[{action.StoreName}]"
213 .Where(s => s !=
null);
215 string message = $
"[{char.ToUpper(storageActionParameters.Action.ToString()[0])}{storageActionParameters.Action.ToString().Substring(1)}] [{action.StorageType}] for {string.Join(" ", details)}";
216 SeleniumStorageHelper seleniumStorageHelper =
null;
218 if (
true == browserSettings.UseSelenium)
219 seleniumStorageHelper =
new SeleniumStorageHelper((Browser) browserSettings.Browser);
221 if (WebsiteStorageAction.get == storageActionParameters.Action)
225 if (
true == browserSettings.UseOttoMagic)
226 data = browserSettings.MagicHelper.GetStorage(action.StorageType, origin, action.Path, action.Key, action.StoreName);
227 else if (
true == browserSettings.UsePuppeteer)
228 data = browserSettings.PuppeteerClient.GetStorage(action.StorageType)
229 .WithStorageDomain(origin).WithStoragePath(action.Path).WithStorageKey(action.Key).WithStorageStoreName(action.StoreName)
231 else if (
true == browserSettings.UseSelenium)
233 StorageAction sa =
new StorageAction(action);
235 data = seleniumStorageHelper.GetStorage(sa);
239 if (
false ==
string.IsNullOrEmpty(data) && 1 < data.Length && data.StartsWith(
"\"") && data.EndsWith(
"\""))
240 data = Newtonsoft.Json.JsonConvert.DeserializeObject<
string>(data);
242 data = UnwrapStorageRead(data, action.Key);
244 GPAL.PublishSimpleEvent(GPALEventType.INFO,
246 gpalUrl, GPALObjectType.Other);
252 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION,
254 gpalUrl, GPALObjectType.Other, ex);
257 else if (WebsiteStorageAction.delete == storageActionParameters.Action)
262 if (
true == browserSettings.UseOttoMagic)
263 retVal = browserSettings.MagicHelper.DeleteStorage(action.StorageType, action.DeleteAcrossOrigins, origin, action.Path, action.Key, action.StoreName);
264 else if (
true == browserSettings.UsePuppeteer)
265 retVal = browserSettings.PuppeteerClient.DeleteStorage(action.StorageType)
266 .WithStorageDomain(origin).WithStoragePath(action.Path).WithStorageKey(action.Key).WithStorageStoreName(action.StoreName)
268 else if (
true == browserSettings.UseSelenium)
270 StorageAction sa =
new StorageAction(action);
272 data = seleniumStorageHelper.DeleteStorage(sa).ToString();
273 retVal =
false ==
"0".Equals(data);
276 GPAL.PublishSimpleEvent(GPALEventType.INFO,
278 gpalUrl, GPALObjectType.Other);
282 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION,
284 gpalUrl, GPALObjectType.Other, ex);
287 else if (WebsiteStorageAction.set == storageActionParameters.Action)
292 if (
true == browserSettings.UseOttoMagic)
293 retVal = browserSettings.MagicHelper.SetStorage(action.StorageType, storageActionParameters.Data, origin, action.Path, action.Key, action.StoreName);
294 else if (
true == browserSettings.UsePuppeteer)
295 retVal = browserSettings.PuppeteerClient.SetStorage(action.StorageType)
296 .WithStorageDomain(origin).WithStoragePath(action.Path).WithStorageKey(action.Key).WithStorageStoreName(action.StoreName).WithStorageData(storageActionParameters.Data)
298 else if (
true == browserSettings.UseSelenium)
300 StorageAction sa =
new StorageAction(action);
302 sa.Data = storageActionParameters.Data;
303 retVal = seleniumStorageHelper.SetStorage(sa);
307 GPAL.PublishSimpleEvent(GPALEventType.INFO,
309 gpalUrl, GPALObjectType.Other);
311 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
313 gpalUrl, GPALObjectType.Other);
318 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION,
320 gpalUrl, GPALObjectType.Other, ex);
334 public static string GetOrigin(
string url)
336 if (Uri.TryCreate(url, UriKind.Absolute, out var uri))
338 return uri.GetLeftPart(UriPartial.Authority);
345 public static bool SameBaseUrl(
string url1,
string url2)
347 if (Uri.TryCreate(url1, UriKind.Absolute, out var uri1) && Uri.TryCreate(url2, UriKind.Absolute, out var uri2))
348 return string.Equals(uri1.GetLeftPart(UriPartial.Path), uri2.GetLeftPart(UriPartial.Path), StringComparison.OrdinalIgnoreCase);
357 private static DomainParser _parser;
359 private static DomainParser Parser
367 SimpleHttpRuleProvider rules =
new SimpleHttpRuleProvider();
369 System.Threading.Tasks.Task.Run(() => rules.BuildAsync()).GetAwaiter().GetResult();
371 _parser =
new DomainParser(rules);
377 public static bool TryGetUri(
string url, out Uri uri)
381 if (
string.IsNullOrWhiteSpace(url))
387 if (!url.Contains(
"://"))
388 url =
"https://" + url;
390 return Uri.TryCreate(url, UriKind.Absolute, out uri);
393 public static string GetWildcardCookieDomain(
string url)
395 if (
string.IsNullOrWhiteSpace(url))
400 if (url.StartsWith(
"."))
401 url = url.Substring(1);
403 if (!TryGetUri(url, out var uri))
408 var domainInfo = Parser.Parse(uri.Host);
410 if (
string.IsNullOrEmpty(domainInfo?.RegistrableDomain))
413 return "." + domainInfo.RegistrableDomain;
432 internal static bool CookieDomainMatches(
string cookieDomain,
string domain)
437 if (
false ==
string.IsNullOrWhiteSpace(domain) &&
false ==
string.IsNullOrWhiteSpace(cookieDomain))
439 string wanted = CookieHostOf(domain);
440 string held = CookieHostOf(cookieDomain);
442 retVal =
true == held.Equals(wanted, StringComparison.OrdinalIgnoreCase)
443 ||
true == held.EndsWith(
"." + wanted, StringComparison.OrdinalIgnoreCase);
458 internal static bool CookieAppliesToHost(
string cookieDomain,
string host)
463 if (
false ==
string.IsNullOrWhiteSpace(cookieDomain) &&
false ==
string.IsNullOrWhiteSpace(host))
465 string bare = CookieHostOf(cookieDomain);
466 string target = CookieHostOf(host);
468 retVal =
true == target.Equals(bare, StringComparison.OrdinalIgnoreCase)
469 ||
true == target.EndsWith(
"." + bare, StringComparison.OrdinalIgnoreCase);
480 private static string CookieHostOf(
string value)
482 string retVal = value.Trim().TrimStart(
'.');
484 if (
true == Uri.TryCreate(retVal, UriKind.Absolute, out Uri uri) &&
false ==
string.IsNullOrEmpty(uri.Host))
489 #endregion Storage Handling
490 #region ROBOTS.TXT and ROBOTS META TAG
493 private static readonly ConcurrentDictionary<string, string[]> RobotsCache =
494 new ConcurrentDictionary<string, string[]>();
497 private static readonly ConcurrentDictionary<string, bool> UrlDecisionCache =
498 new ConcurrentDictionary<string, bool>();
500 static bool inRobotsCheck =
false;
505 private static readonly HashSet<string> NonHtmlExtensions =
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
508 ".xml",
".xsl",
".xslt",
".rss",
".atom",
".txt",
".json",
".jsonld",
".csv",
".tsv",
".yaml",
".yml",
".md",
510 ".pdf",
".doc",
".docx",
".xls",
".xlsx",
".ppt",
".pptx",
".rtf",
".odt",
".ods",
".odp",
".epub",
512 ".jpg",
".jpeg",
".png",
".gif",
".bmp",
".webp",
".svg",
".svgz",
".ico",
".tif",
".tiff",
".avif",
".heic",
514 ".mp3",
".wav",
".ogg",
".oga",
".flac",
".aac",
".m4a",
".mp4",
".m4v",
".webm",
".mov",
".avi",
".mkv",
".wmv",
".m3u8",
516 ".zip",
".gz",
".tgz",
".bz2",
".7z",
".rar",
".tar",
".exe",
".msi",
".dmg",
".apk",
".iso",
".bin",
518 ".js",
".mjs",
".css",
".map",
".wasm",
520 ".woff",
".woff2",
".ttf",
".otf",
".eot",
522 ".ics",
".vcf",
".sql",
".log"
531 public static bool IsNonHtmlResource(
string url)
533 if (
true ==
string.IsNullOrWhiteSpace(url))
540 if (
true == Uri.TryCreate(url, UriKind.Absolute, out Uri uri))
543 if (
false == uri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) &&
544 false == uri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
547 path = uri.AbsolutePath;
552 int cut = path.IndexOfAny(
new[] {
'?',
'#' });
554 path = path.Substring(0, cut);
557 string extension = System.IO.Path.GetExtension(path);
559 if (
true ==
string.IsNullOrWhiteSpace(extension))
562 return NonHtmlExtensions.Contains(extension);
579 public static bool CheckRobotsTxt(
string url, IBrowser browser, List<string> robotsTxt)
581 var uri =
new Uri(url);
582 string host = uri.Host;
583 bool isAllowed =
true;
585 bool cachedResult =
true;
589 if (
true == inRobotsCheck)
591 if (UrlDecisionCache.TryGetValue(url, out cachedResult))
593 RobotsCache.TryGetValue(host, out lines);
594 foreach (
string line
in lines)
603 inRobotsCheck =
true;
608 if (UrlDecisionCache.TryGetValue(url, out cachedResult))
610 GPAL.PublishSimpleEvent(
612 $
"Robots policy cache hit for [{url}] -> [{cachedResult}]");
614 RobotsCache.TryGetValue(host, out lines);
616 foreach (
string line
in lines)
622 GPAL.PublishSimpleEvent(
624 $
"Checking robots.txt for [{url}]");
626 using (var client =
new System.Net.Http.HttpClient())
629 client.DefaultRequestHeaders.Add(
631 MagicHelper.GetUserAgentString(browser.BrowserType));
637 lines = RobotsCache.GetOrAdd(host, h =>
643 var task = client.GetStringAsync($
"{uri.Scheme}://{h}/robots.txt");
645 if (!task.Wait(TimeSpan.FromSeconds(10)))
647 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"[{uri.Scheme}]://[{h}]/robots.txt request timed out after 10 seconds.");
648 throw new TimeoutException();
651 string robotsContent = task.Result;
653 .Split(
new[] {
'\n',
'\r' }, StringSplitOptions.RemoveEmptyEntries)
654 .Select(l => l.Trim())
660 string currentUrl = browser.CurrentUrl;
661 browser.NewTab($
"{uri.Scheme}://{h}/robots.txt").GetPageSource(out
string pageSource);
662 if (1 < ((Browser)browser).tabsWeOpened)
663 browser.CloseTab($
"{uri.Scheme}://{h}/robots.txt");
665 ((Browser)browser).CurrentUrl = currentUrl;
667 if (
true ==
string.IsNullOrEmpty(pageSource))
669 return Array.Empty<
string>();
673 string content = pageSource;
676 if (content.Contains(
"<pre"))
678 int preStart = content.IndexOf(
"<pre", StringComparison.OrdinalIgnoreCase);
681 int start = content.IndexOf(
">", preStart) + 1;
682 int end = content.LastIndexOf(
"</pre>", StringComparison.OrdinalIgnoreCase);
683 if (start > 0 && end > start)
685 content = content.Substring(start, end - start);
691 .Replace(
@"\n",
"\n").Replace(
@"\r",
"\r")
692 .Split(
new[] {
'\n',
'\r' }, StringSplitOptions.RemoveEmptyEntries)
693 .Select(l => l.Trim())
699 bool userAgentAll =
false;
700 var disallowedPaths =
new System.Collections.Generic.List<
string>();
703 foreach (var line
in lines)
705 if (line.StartsWith(
"User-agent:", StringComparison.OrdinalIgnoreCase))
707 userAgentAll = line.Substring(11).Trim() ==
"*";
709 else if (userAgentAll &&
710 line.StartsWith(
"Disallow:", StringComparison.OrdinalIgnoreCase))
712 string path = line.Substring(9).Trim();
714 if (!
string.IsNullOrEmpty(path))
715 disallowedPaths.Add(path);
718 GPAL.PublishSimpleEvent(
720 $
"Processed robots.txt line [{line}]");
725 string relativePath = uri.PathAndQuery;
727 foreach (var rule
in disallowedPaths)
729 if (relativePath.StartsWith(rule, StringComparison.OrdinalIgnoreCase))
731 GPAL.PublishSimpleEvent(
732 GPALEventType.WARNING,
733 $
"ACCESS DENIED by robots.txt [Disallow: [{rule}]]"
743 GPAL.PublishSimpleEvent(
744 GPALEventType.WARNING,
745 $
"Error checking robots.txt for [{url}]"
746 ,
null, GPALObjectType.None, ex);
752 inRobotsCheck =
false;
756 UrlDecisionCache[url] = isAllowed;
762 private static System.Collections.Concurrent.ConcurrentDictionary<string, HashSet<string>> _metaTagAuditCache;
766 private static string _lastDomain;
779 [InProgress(
"Output website robot meta tags. Will respect .WithRespectRobotMetaTags(true) on noindex/nofollow/none. Ottomagic cannot retrived head.")]
780 public static bool CheckRobotMetaTags(GPALUrl url, Browser browser)
782 GPAL.PublishSimpleEvent(GPALEventType.INFO,
"Searching for robots meta tag");
786 if (
true == url?.Url.Equals(
"about:blank") ||
true == IsNonHtmlResource(url?.Url))
789 bool isAllowed =
true;
791 var uri =
new Uri(url.Url);
792 string currentDomain = uri.Host;
793 _metaTagAuditCache ??=
new System.Collections.Concurrent.ConcurrentDictionary<string, HashSet<string>>();
796 if (
false == UrlHelper.AreEquivalent(_lastDomain, currentDomain))
798 _metaTagAuditCache?.Clear();
799 _lastDomain = currentDomain;
802 HashSet<string> loggedDirectives = _metaTagAuditCache.GetOrAdd(url.Url, _ =>
new HashSet<string>());
804 if (0 == loggedDirectives.Count)
806 if (
null != browser.Process || 0 != browser.BrowserSettings.ServiceDriverPid)
810 string content =
string.Empty;
813 if (
true == browser.UseOttoMagic)
816 List<GPALElement> theHead = browser.MagicHelper.QuerySelectors(
"meta[name='robots']");
822 foreach (GPALElement element
in theHead)
825 element.Browser = browser;
826 content = element.GetAttribute(
"content");
831 bool withReturn =
false == browser.UsePuppeteer;
837 content = BrowserHelper.ExecuteJavaScriptObj(
838 $@"var div = document.querySelector(""meta[name='robots']""); {(true == withReturn ? "return " : "")} div?.getAttribute('content');",
848 }
while (0 < retries--);
854 var directives = content.Split(
',')
855 .Select(d => d.Trim())
858 foreach (var directive
in directives)
862 if (directive ==
"noindex" || directive ==
"nofollow" || directive ==
"none" || directive ==
"index" || directive ==
"follow")
864 loggedDirectives.Add(directive);
876 foreach (
string directive
in loggedDirectives)
877 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Robots meta tag [{directive}]");
879 isAllowed = (
true == browser.BrowserSettings.RespectRobotMetaTags && (
true == loggedDirectives.Contains(
"noindex") ||
true == loggedDirectives.Contains(
"nofollow") ||
true == loggedDirectives.Contains(
"none"))) ? false :
true;
883 #endregion ROBOTS.TXT and ROBOTS META TAG
889 public static bool AreEquivalent(
string url1,
string url2,
bool treatHttpHttpsAsSame =
true)
891 if (
string.IsNullOrWhiteSpace(url1) ||
string.IsNullOrWhiteSpace(url2))
895 if (
string.Equals(url1, url2, StringComparison.OrdinalIgnoreCase))
900 var uri1 =
new Uri(url1, UriKind.Absolute);
901 var uri2 =
new Uri(url2, UriKind.Absolute);
904 if (treatHttpHttpsAsSame)
906 string scheme1 = uri1.Scheme.Equals(
"http", StringComparison.OrdinalIgnoreCase) ?
"https" : uri1.Scheme;
907 string scheme2 = uri2.Scheme.Equals(
"http", StringComparison.OrdinalIgnoreCase) ?
"https" : uri2.Scheme;
908 if (scheme1 != scheme2)
911 else if (!
string.Equals(uri1.Scheme, uri2.Scheme, StringComparison.OrdinalIgnoreCase))
917 string host1 = NormalizeHost(uri1.Host);
918 string host2 = NormalizeHost(uri2.Host);
923 string path1 = NormalizePath(uri1.AbsolutePath);
924 string path2 = NormalizePath(uri2.AbsolutePath);
929 if (uri1.Query != uri2.Query)
937 catch (UriFormatException)
940 return string.Equals(url1.TrimEnd(
'/'), url2.TrimEnd(
'/'), StringComparison.OrdinalIgnoreCase);
944 public static string GetNormalizedDomain(
string url)
946 Uri uri =
new Uri(url);
947 string currentDomain = uri.Host;
948 return NormalizeHost(currentDomain);
951 private static string NormalizeHost(
string host)
953 if (
string.IsNullOrEmpty(host))
return host;
955 host = host.TrimEnd(
'.').ToLowerInvariant();
958 if (host.StartsWith(
"www", StringComparison.OrdinalIgnoreCase))
960 int dotIndex = host.IndexOf(
'.');
961 if (dotIndex > 0 && dotIndex < 6)
963 host = host.Substring(dotIndex + 1);
970 public static string NormalizePath(
string path)
972 if (
string.IsNullOrEmpty(path))
return "/";
974 path = path.ToLowerInvariant();
977 if (path.Length > 1 && path.EndsWith(
"/"))
978 path = path.TrimEnd(
'/');