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;
47 public static class UrlHelper
49 static bool MatchesOrAny(
this string filter,
string value)
50 =>
string.IsNullOrEmpty(filter) || filter == value;
52 #region Storage Handling
113 private static string UnwrapStorageRead(
string data,
string key)
115 string retVal = data;
119 if (
false ==
string.IsNullOrEmpty(key) &&
false ==
string.IsNullOrWhiteSpace(data) &&
true == data.TrimStart().StartsWith(
"{"))
123 Newtonsoft.Json.Linq.JObject wrapper = Newtonsoft.Json.Linq.JObject.Parse(data);
127 bool isWrapper =
null != wrapper[
"value"]
128 && wrapper.Properties().All(each =>
"value" == each.Name ||
"key" == each.Name);
130 if (
true == isWrapper)
132 Newtonsoft.Json.Linq.JToken value = wrapper[
"value"];
136 retVal = Newtonsoft.Json.Linq.JTokenType.String == value.Type
138 : Newtonsoft.Json.JsonConvert.SerializeObject(value);
150 internal static bool TryPerformAction(IGPALUrl gpalUrl, BrowserSettings browserSettings, StorageAction storageActionParameters, out
string data)
158 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"No GPALUrl supplied to run [{storageActionParameters.StorageType}] [{storageActionParameters.Action}] action", browserSettings, GPALObjectType.Other);
161 else if (
null == storageActionParameters)
163 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"No StorageAction parameters supplied to run on [{gpalUrl.Url}]", browserSettings, GPALObjectType.Other);
167 string defaultOrigin = GetOrigin(gpalUrl.Url);
168 IEnumerable<StorageAction> storageActions =
null;
171 if (
null == storageActionParameters.Domain &&
null == storageActionParameters.Key &&
null == storageActionParameters.Path &&
null == storageActionParameters.StoreName
172 &&
null == storageActionParameters.UserDefined)
174 if (WebsiteStorageType.notSet == storageActionParameters.StorageType)
175 storageActions = gpalUrl.GetActions().Where(a => storageActionParameters.Action == a.Action);
177 storageActions = gpalUrl.GetActions().Where(a => storageActionParameters.StorageType == a.StorageType && storageActionParameters.Action == a.Action);
182 var p = storageActionParameters;
184 storageActions = gpalUrl.GetActions().Where(a =>
185 a.StorageType == p.StorageType &&
186 p.Domain.MatchesOrAny(a.Domain) &&
187 p.Path.MatchesOrAny(a.Path) &&
188 p.Key.MatchesOrAny(a.Key) &&
189 p.StoreName.MatchesOrAny(a.StoreName) &&
190 p.UserDefined.MatchesOrAny(a.UserDefined)
194 foreach (var action
in storageActions)
196 string origin = action.Domain !=
null
197 ? MagicHelper.GetFullUrl(action.Domain,
null, out
bool _,
true)
200 var paramsDict =
new Dictionary<string, object>();
202 if (WebsiteStorageType.cookie == action.StorageType)
203 origin = UrlHelper.GetWildcardCookieDomain(origin);
207 string.IsNullOrWhiteSpace(origin) ? null : $
"d[{origin}]",
208 string.IsNullOrWhiteSpace(action.Path) ? null : $
"p[{action.Path}]",
209 string.IsNullOrWhiteSpace(action.Key) ? null : $
"k[{action.Key}]",
210 string.IsNullOrWhiteSpace(action.StoreName) ? null : $
"sn[{action.StoreName}]"
212 .Where(s => s !=
null);
214 string message = $
"[{char.ToUpper(storageActionParameters.Action.ToString()[0])}{storageActionParameters.Action.ToString().Substring(1)}] [{action.StorageType}] for {string.Join(" ", details)}";
215 SeleniumStorageHelper seleniumStorageHelper =
null;
217 if (
true == browserSettings.UseSelenium)
218 seleniumStorageHelper =
new SeleniumStorageHelper((Browser) browserSettings.Browser);
220 if (WebsiteStorageAction.get == storageActionParameters.Action)
224 if (
true == browserSettings.UseOttoMagic)
225 data = browserSettings.MagicHelper.GetStorage(action.StorageType, origin, action.Path, action.Key, action.StoreName);
226 else if (
true == browserSettings.UsePuppeteer)
227 data = browserSettings.PuppeteerClient.GetStorage(action.StorageType)
228 .WithStorageDomain(origin).WithStoragePath(action.Path).WithStorageKey(action.Key).WithStorageStoreName(action.StoreName)
230 else if (
true == browserSettings.UseSelenium)
232 StorageAction sa =
new StorageAction(action);
234 data = seleniumStorageHelper.GetStorage(sa);
239 if (
false ==
string.IsNullOrEmpty(data))
240 while (data.StartsWith(
"\"") && data.EndsWith(
"\""))
245 var unescaped = Newtonsoft.Json.JsonConvert.DeserializeObject<
string>(data);
248 if (unescaped == data)
break;
259 data = UnwrapStorageRead(data, action.Key);
261 GPAL.PublishSimpleEvent(GPALEventType.INFO,
263 gpalUrl, GPALObjectType.Other);
269 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION,
271 gpalUrl, GPALObjectType.Other, ex);
274 else if (WebsiteStorageAction.delete == storageActionParameters.Action)
279 if (
true == browserSettings.UseOttoMagic)
280 retVal = browserSettings.MagicHelper.DeleteStorage(action.StorageType, action.DeleteAcrossOrigins, origin, action.Path, action.Key, action.StoreName);
281 else if (
true == browserSettings.UsePuppeteer)
282 retVal = browserSettings.PuppeteerClient.DeleteStorage(action.StorageType)
283 .WithStorageDomain(origin).WithStoragePath(action.Path).WithStorageKey(action.Key).WithStorageStoreName(action.StoreName)
285 else if (
true == browserSettings.UseSelenium)
287 StorageAction sa =
new StorageAction(action);
289 data = seleniumStorageHelper.DeleteStorage(sa).ToString();
290 retVal =
false ==
"0".Equals(data);
293 GPAL.PublishSimpleEvent(GPALEventType.INFO,
295 gpalUrl, GPALObjectType.Other);
299 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION,
301 gpalUrl, GPALObjectType.Other, ex);
304 else if (WebsiteStorageAction.set == storageActionParameters.Action)
309 if (
true == browserSettings.UseOttoMagic)
310 retVal = browserSettings.MagicHelper.SetStorage(action.StorageType, storageActionParameters.Data, origin, action.Path, action.Key, action.StoreName);
311 else if (
true == browserSettings.UsePuppeteer)
312 retVal = browserSettings.PuppeteerClient.SetStorage(action.StorageType)
313 .WithStorageDomain(origin).WithStoragePath(action.Path).WithStorageKey(action.Key).WithStorageStoreName(action.StoreName).WithStorageData(storageActionParameters.Data)
315 else if (
true == browserSettings.UseSelenium)
317 StorageAction sa =
new StorageAction(action);
319 sa.Data = storageActionParameters.Data;
320 retVal = seleniumStorageHelper.SetStorage(sa);
324 GPAL.PublishSimpleEvent(GPALEventType.INFO,
326 gpalUrl, GPALObjectType.Other);
328 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
330 gpalUrl, GPALObjectType.Other);
335 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION,
337 gpalUrl, GPALObjectType.Other, ex);
351 public static string GetOrigin(
string url)
353 if (Uri.TryCreate(url, UriKind.Absolute, out var uri))
355 return uri.GetLeftPart(UriPartial.Authority);
362 public static bool SameBaseUrl(
string url1,
string url2)
364 if (Uri.TryCreate(url1, UriKind.Absolute, out var uri1) && Uri.TryCreate(url2, UriKind.Absolute, out var uri2))
365 return string.Equals(uri1.GetLeftPart(UriPartial.Path), uri2.GetLeftPart(UriPartial.Path), StringComparison.OrdinalIgnoreCase);
374 private static DomainParser _parser;
376 private static DomainParser Parser
382 _parser =
new DomainParser(
new WebTldRuleProvider());
388 public static bool TryGetUri(
string url, out Uri uri)
392 if (
string.IsNullOrWhiteSpace(url))
398 if (!url.Contains(
"://"))
399 url =
"https://" + url;
401 return Uri.TryCreate(url, UriKind.Absolute, out uri);
404 public static string GetWildcardCookieDomain(
string url)
406 if (
string.IsNullOrWhiteSpace(url))
411 if (url.StartsWith(
"."))
412 url = url.Substring(1);
414 if (!TryGetUri(url, out var uri))
419 var domainInfo = Parser.Parse(uri.Host);
421 if (
string.IsNullOrEmpty(domainInfo?.RegistrableDomain))
424 return "." + domainInfo.RegistrableDomain;
443 internal static bool CookieDomainMatches(
string cookieDomain,
string domain)
448 if (
false ==
string.IsNullOrWhiteSpace(domain) &&
false ==
string.IsNullOrWhiteSpace(cookieDomain))
450 string wanted = CookieHostOf(domain);
451 string held = CookieHostOf(cookieDomain);
453 retVal =
true == held.Equals(wanted, StringComparison.OrdinalIgnoreCase)
454 ||
true == held.EndsWith(
"." + wanted, StringComparison.OrdinalIgnoreCase);
469 internal static bool CookieAppliesToHost(
string cookieDomain,
string host)
474 if (
false ==
string.IsNullOrWhiteSpace(cookieDomain) &&
false ==
string.IsNullOrWhiteSpace(host))
476 string bare = CookieHostOf(cookieDomain);
477 string target = CookieHostOf(host);
479 retVal =
true == target.Equals(bare, StringComparison.OrdinalIgnoreCase)
480 ||
true == target.EndsWith(
"." + bare, StringComparison.OrdinalIgnoreCase);
491 private static string CookieHostOf(
string value)
493 string retVal = value.Trim().TrimStart(
'.');
495 if (
true == Uri.TryCreate(retVal, UriKind.Absolute, out Uri uri) &&
false ==
string.IsNullOrEmpty(uri.Host))
500 #endregion Storage Handling
501 #region ROBOTS.TXT and ROBOTS META TAG
504 private static readonly ConcurrentDictionary<string, string[]> RobotsCache =
505 new ConcurrentDictionary<string, string[]>();
508 private static readonly ConcurrentDictionary<string, bool> UrlDecisionCache =
509 new ConcurrentDictionary<string, bool>();
511 static bool inRobotsCheck =
false;
516 private static readonly HashSet<string> NonHtmlExtensions =
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
519 ".xml",
".xsl",
".xslt",
".rss",
".atom",
".txt",
".json",
".jsonld",
".csv",
".tsv",
".yaml",
".yml",
".md",
521 ".pdf",
".doc",
".docx",
".xls",
".xlsx",
".ppt",
".pptx",
".rtf",
".odt",
".ods",
".odp",
".epub",
523 ".jpg",
".jpeg",
".png",
".gif",
".bmp",
".webp",
".svg",
".svgz",
".ico",
".tif",
".tiff",
".avif",
".heic",
525 ".mp3",
".wav",
".ogg",
".oga",
".flac",
".aac",
".m4a",
".mp4",
".m4v",
".webm",
".mov",
".avi",
".mkv",
".wmv",
".m3u8",
527 ".zip",
".gz",
".tgz",
".bz2",
".7z",
".rar",
".tar",
".exe",
".msi",
".dmg",
".apk",
".iso",
".bin",
529 ".js",
".mjs",
".css",
".map",
".wasm",
531 ".woff",
".woff2",
".ttf",
".otf",
".eot",
533 ".ics",
".vcf",
".sql",
".log"
542 public static bool IsNonHtmlResource(
string url)
544 if (
true ==
string.IsNullOrWhiteSpace(url))
551 if (
true == Uri.TryCreate(url, UriKind.Absolute, out Uri uri))
554 if (
false == uri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) &&
555 false == uri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
558 path = uri.AbsolutePath;
563 int cut = path.IndexOfAny(
new[] {
'?',
'#' });
565 path = path.Substring(0, cut);
568 string extension = System.IO.Path.GetExtension(path);
570 if (
true ==
string.IsNullOrWhiteSpace(extension))
573 return NonHtmlExtensions.Contains(extension);
590 public static bool CheckRobotsTxt(
string url, IBrowser browser, List<string> robotsTxt)
592 var uri =
new Uri(url);
593 string host = uri.Host;
594 bool isAllowed =
true;
596 bool cachedResult =
true;
600 if (
true == inRobotsCheck)
602 if (UrlDecisionCache.TryGetValue(url, out cachedResult))
604 RobotsCache.TryGetValue(host, out lines);
605 foreach (
string line
in lines)
614 inRobotsCheck =
true;
619 if (UrlDecisionCache.TryGetValue(url, out cachedResult))
621 GPAL.PublishSimpleEvent(
623 $
"Robots policy cache hit for [{url}] -> [{cachedResult}]");
625 RobotsCache.TryGetValue(host, out lines);
627 foreach (
string line
in lines)
633 GPAL.PublishSimpleEvent(
635 $
"Checking robots.txt for [{url}]");
637 using (var client =
new System.Net.Http.HttpClient())
640 client.DefaultRequestHeaders.Add(
642 MagicHelper.GetUserAgentString(browser.BrowserType));
648 lines = RobotsCache.GetOrAdd(host, h =>
654 var task = client.GetStringAsync($
"{uri.Scheme}://{h}/robots.txt");
656 if (!task.Wait(TimeSpan.FromSeconds(10)))
658 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $
"[{uri.Scheme}]://[{h}]/robots.txt request timed out after 10 seconds.");
659 throw new TimeoutException();
662 string robotsContent = task.Result;
664 .Split(
new[] {
'\n',
'\r' }, StringSplitOptions.RemoveEmptyEntries)
665 .Select(l => l.Trim())
671 string currentUrl = browser.CurrentUrl;
672 browser.NewTab($
"{uri.Scheme}://{h}/robots.txt").GetPageSource(out
string pageSource);
673 if (1 < ((Browser)browser).tabsWeOpened)
674 browser.CloseTab($
"{uri.Scheme}://{h}/robots.txt");
676 ((Browser)browser).CurrentUrl = currentUrl;
678 if (
true ==
string.IsNullOrEmpty(pageSource))
680 return Array.Empty<
string>();
684 string content = pageSource;
687 if (content.Contains(
"<pre"))
689 int preStart = content.IndexOf(
"<pre", StringComparison.OrdinalIgnoreCase);
692 int start = content.IndexOf(
">", preStart) + 1;
693 int end = content.LastIndexOf(
"</pre>", StringComparison.OrdinalIgnoreCase);
694 if (start > 0 && end > start)
696 content = content.Substring(start, end - start);
702 .Replace(
@"\n",
"\n").Replace(
@"\r",
"\r")
703 .Split(
new[] {
'\n',
'\r' }, StringSplitOptions.RemoveEmptyEntries)
704 .Select(l => l.Trim())
710 bool userAgentAll =
false;
711 var disallowedPaths =
new System.Collections.Generic.List<
string>();
714 foreach (var line
in lines)
716 if (line.StartsWith(
"User-agent:", StringComparison.OrdinalIgnoreCase))
718 userAgentAll = line.Substring(11).Trim() ==
"*";
720 else if (userAgentAll &&
721 line.StartsWith(
"Disallow:", StringComparison.OrdinalIgnoreCase))
723 string path = line.Substring(9).Trim();
725 if (!
string.IsNullOrEmpty(path))
726 disallowedPaths.Add(path);
729 GPAL.PublishSimpleEvent(
731 $
"Processed robots.txt line [{line}]");
736 string relativePath = uri.PathAndQuery;
738 foreach (var rule
in disallowedPaths)
740 if (relativePath.StartsWith(rule, StringComparison.OrdinalIgnoreCase))
742 GPAL.PublishSimpleEvent(
743 GPALEventType.WARNING,
744 $
"ACCESS DENIED by robots.txt [Disallow: [{rule}]]"
754 GPAL.PublishSimpleEvent(
755 GPALEventType.WARNING,
756 $
"Error checking robots.txt for [{url}]"
757 ,
null, GPALObjectType.None, ex);
763 inRobotsCheck =
false;
767 UrlDecisionCache[url] = isAllowed;
773 private static System.Collections.Concurrent.ConcurrentDictionary<string, HashSet<string>> _metaTagAuditCache;
777 private static string _lastDomain;
790 [InProgress(
"Output website robot meta tags. Will respect .WithRespectRobotMetaTags(true) on noindex/nofollow/none. Ottomagic cannot retrived head.")]
791 public static bool CheckRobotMetaTags(GPALUrl url, Browser browser)
793 GPAL.PublishSimpleEvent(GPALEventType.INFO,
"Searching for robots meta tag");
797 if (
true == url?.Url.Equals(
"about:blank") ||
true == IsNonHtmlResource(url?.Url))
800 bool isAllowed =
true;
802 var uri =
new Uri(url.Url);
803 string currentDomain = uri.Host;
804 _metaTagAuditCache ??=
new System.Collections.Concurrent.ConcurrentDictionary<string, HashSet<string>>();
807 if (
false == UrlHelper.AreEquivalent(_lastDomain, currentDomain))
809 _metaTagAuditCache?.Clear();
810 _lastDomain = currentDomain;
813 HashSet<string> loggedDirectives = _metaTagAuditCache.GetOrAdd(url.Url, _ =>
new HashSet<string>());
815 if (0 == loggedDirectives.Count)
817 if (
null != browser.Process || 0 != browser.BrowserSettings.ServiceDriverPid)
821 string content =
string.Empty;
824 if (
true == browser.UseOttoMagic)
827 List<GPALElement> theHead = browser.MagicHelper.QueryPersistentSelectors(
"meta[name='robots']");
833 foreach (GPALElement element
in theHead)
836 element.Browser = browser;
837 content = element.GetAttribute(
"content");
842 bool withReturn =
false == browser.UsePuppeteer;
848 content = BrowserHelper.ExecuteJavaScriptObj(
849 $@"var div = document.querySelector(""meta[name='robots']""); {(true == withReturn ? "return " : "")} div?.getAttribute('content');",
859 }
while (0 < retries--);
865 var directives = content.Split(
',')
866 .Select(d => d.Trim())
869 foreach (var directive
in directives)
873 if (directive ==
"noindex" || directive ==
"nofollow" || directive ==
"none" || directive ==
"index" || directive ==
"follow")
875 loggedDirectives.Add(directive);
887 foreach (
string directive
in loggedDirectives)
888 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Robots meta tag [{directive}]");
890 isAllowed = (
true == browser.BrowserSettings.RespectRobotMetaTags && (
true == loggedDirectives.Contains(
"noindex") ||
true == loggedDirectives.Contains(
"nofollow") ||
true == loggedDirectives.Contains(
"none"))) ? false :
true;
894 #endregion ROBOTS.TXT and ROBOTS META TAG
900 public static bool AreEquivalent(
string url1,
string url2,
bool treatHttpHttpsAsSame =
true)
902 if (
string.IsNullOrWhiteSpace(url1) ||
string.IsNullOrWhiteSpace(url2))
906 if (
string.Equals(url1, url2, StringComparison.OrdinalIgnoreCase))
911 var uri1 =
new Uri(url1, UriKind.Absolute);
912 var uri2 =
new Uri(url2, UriKind.Absolute);
915 if (treatHttpHttpsAsSame)
917 string scheme1 = uri1.Scheme.Equals(
"http", StringComparison.OrdinalIgnoreCase) ?
"https" : uri1.Scheme;
918 string scheme2 = uri2.Scheme.Equals(
"http", StringComparison.OrdinalIgnoreCase) ?
"https" : uri2.Scheme;
919 if (scheme1 != scheme2)
922 else if (!
string.Equals(uri1.Scheme, uri2.Scheme, StringComparison.OrdinalIgnoreCase))
928 string host1 = NormalizeHost(uri1.Host);
929 string host2 = NormalizeHost(uri2.Host);
934 string path1 = NormalizePath(uri1.AbsolutePath);
935 string path2 = NormalizePath(uri2.AbsolutePath);
940 if (uri1.Query != uri2.Query)
948 catch (UriFormatException)
951 return string.Equals(url1.TrimEnd(
'/'), url2.TrimEnd(
'/'), StringComparison.OrdinalIgnoreCase);
955 public static string GetNormalizedDomain(
string url)
957 Uri uri =
new Uri(url);
958 string currentDomain = uri.Host;
959 return NormalizeHost(currentDomain);
962 private static string NormalizeHost(
string host)
964 if (
string.IsNullOrEmpty(host))
return host;
966 host = host.TrimEnd(
'.').ToLowerInvariant();
969 if (host.StartsWith(
"www", StringComparison.OrdinalIgnoreCase))
971 int dotIndex = host.IndexOf(
'.');
972 if (dotIndex > 0 && dotIndex < 6)
974 host = host.Substring(dotIndex + 1);
981 public static string NormalizePath(
string path)
983 if (
string.IsNullOrEmpty(path))
return "/";
985 path = path.ToLowerInvariant();
988 if (path.Length > 1 && path.EndsWith(
"/"))
989 path = path.TrimEnd(
'/');