HostSettingsManager.cs 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using Best.HTTP.HostSetting;
  5. namespace Best.HTTP.Hosts.Settings
  6. {
  7. /**
  8. * Host Settings Hierarchy for the following hosts, settings are stored as leafs:
  9. *
  10. * *.com
  11. * *.example.com
  12. * example.com
  13. *
  14. * '*' matches one or more subdomains so *.example.com
  15. * - matches a.example.com and a.b.example.com
  16. * - but doesn't match example.com!
  17. *
  18. *
  19. *
  20. * [com] [localhost] [org] [*]
  21. * +------+------+ | | |
  22. * | | [setting] [*] [setting]
  23. * [example] [*] |
  24. * / \ | [setting]
  25. * [b] [setting] [setting]
  26. * |
  27. * [a]
  28. * |
  29. * [setting]
  30. * */
  31. /// <summary>
  32. /// Manages host-specific settings for HTTP requests based on hostnames.
  33. /// The HostSettingsManager is a powerful tool for fine-tuning HTTP request and connection behaviors
  34. /// on a per-host basis. It enables you to define custom settings for specific hostnames
  35. /// while maintaining default settings for all other hosts. This level of granularity allows you to
  36. /// optimize and customize HTTP requests for different endpoints within your application.
  37. /// </summary>
  38. /// <remarks>
  39. /// When host-specific settings are not found for a given host variant, the default <see cref="HostSettings"/>
  40. /// associated with the "*" host will be returned.
  41. /// </remarks>
  42. public sealed class HostSettingsManager
  43. {
  44. SortedList<string, Node> _rootNodes = new SortedList<string, Node>(AsteriskStringComparer.Instance);
  45. /// <summary>
  46. /// Initializes a new instance of the <see cref="HostSettingsManager"/> class with default settings for all hosts ("*").
  47. /// </summary>
  48. public HostSettingsManager() => Add("*", new HostSettings());
  49. /// <summary>
  50. /// Adds default settings for the host part of the specified URI. This is equivalent to calling <see cref="Add(Uri, HostSettings)"/> with the a new <see cref="HostSettings"/>.
  51. /// </summary>
  52. /// <param name="uri">The URI for which default settings should be applied. Only the host part of the URI will be used.</param>
  53. /// <returns>A <see cref="HostSettings"/> instance with default values.</returns>
  54. public HostSettings AddDefault(Uri uri) => Add(uri, new HostSettings());
  55. /// <summary>
  56. /// Adds default settings for the the specified host name. This is equivalent to calling <see cref="Add(string, HostSettings)"/> with the a new <see cref="HostSettings"/>.
  57. /// </summary>
  58. /// <param name="hostname">The hostname for which default settings should be applied.</param>
  59. /// <returns>A <see cref="HostSettings"/> instance with default values.</returns>
  60. public HostSettings AddDefault(string hostname) => Add(hostname, new HostSettings());
  61. /// <summary>
  62. /// Adds host-specific settings for the host part of the specified URI.
  63. /// </summary>
  64. /// <param name="uri">The URI for which settings should be applied. Only the host part of the URI will be used.</param>
  65. /// <param name="settings">The <see cref="HostSettings"/> to apply.</param>
  66. public HostSettings Add(Uri uri, HostSettings settings) => Add(uri.Host, settings);
  67. /// <summary>
  68. /// Adds host-specific settings for the specified hostname.
  69. /// </summary>
  70. /// <param name="hostname">The hostname for which settings should be applied.</param>
  71. /// <param name="settings">The <see cref="HostSettings"/> to apply.</param>
  72. /// <exception cref="ArgumentNullException">Thrown when either the hostname or settings is null.</exception>
  73. /// <exception cref="FormatException">Thrown when the hostname contains more than one asterisk ('*').</exception>
  74. public HostSettings Add(string hostname, HostSettings settings)
  75. {
  76. if (string.IsNullOrEmpty(hostname))
  77. throw new ArgumentNullException(nameof(hostname));
  78. if (settings == null)
  79. throw new ArgumentNullException(nameof(settings));
  80. if (hostname.IndexOf('*') != hostname.LastIndexOf('*'))
  81. throw new FormatException($"{nameof(hostname)} (\"{hostname}\") MUST contain only one '*'!");
  82. // From "a.b.example.com" create a list: [ "com", "example", "b", "a"]
  83. var segments = hostname.Split(new char[] { '.' }, StringSplitOptions.RemoveEmptyEntries).Reverse().ToList();
  84. string subKey = segments[0];
  85. segments.RemoveAt(0);
  86. if (!_rootNodes.TryGetValue(subKey, out var node))
  87. _rootNodes.Add(subKey, node = new Node(subKey, null));
  88. node.Add(segments, settings);
  89. return settings;
  90. }
  91. /// <summary>
  92. /// Gets <see cref="HostSettings"/> for the host part of the specified <see cref="HostVariant"/>. Returns the default settings associated with "*" when not found.
  93. /// </summary>
  94. /// <param name="variant">The <see cref="HostVariant"/> for which settings should be retrieved. Only the host part of the variant will be used.</param>
  95. /// <returns>The host settings for the specified host variant or the default settings for "*" if not found.</returns>
  96. public HostSettings Get(HostVariant variant, bool fallbackToWildcard = true) => Get(variant.Host, fallbackToWildcard);
  97. /// <summary>
  98. /// Gets <see cref="HostSettings"/> for the host part of the specified <see cref="HostKey"/>. Returns the default settings associated with "*" when not found.
  99. /// </summary>
  100. /// <param name="hostKey">The <see cref="HostKey"/> for which settings should be retrieved. Only the host part of the host key will be used.</param>
  101. /// <returns>The host settings for the specified host key or the default settings for "*" if not found.</returns>
  102. public HostSettings Get(HostKey hostKey, bool fallbackToWildcard = true) => Get(hostKey.Host, fallbackToWildcard);
  103. /// <summary>
  104. /// Gets <see cref="HostSettings"/> for the host part of the specified <see cref="Uri"/>. Returns the default settings associated with "*" when not found.
  105. /// </summary>
  106. /// <param name="uri">The <see cref="Uri"/> for which settings should be retrieved. Only the host part of the URI will be used.</param>
  107. /// <returns>The host settings for the specified URI or the default settings for "*" if not found.</returns>
  108. public HostSettings Get(Uri uri, bool fallbackToWildcard = true) => Get(uri.Host, fallbackToWildcard);
  109. /// <summary>
  110. /// Gets <see cref="HostSettings"/> for the host part of the specified hostname. Returns the default settings associated with "*" when not found.
  111. /// </summary>
  112. /// <param name="hostname">The hostname for which settings should be retrieved. Only the host part of the hostname will be used.</param>
  113. /// <returns>The host settings for the specified hostname or the default settings for "*" if not found.</returns>
  114. /// <exception cref="ArgumentNullException">Thrown when the hostname is null.</exception>
  115. public HostSettings Get(string hostname, bool fallbackToWildcard = true)
  116. {
  117. if (string.IsNullOrEmpty(hostname))
  118. throw new ArgumentNullException(nameof(hostname));
  119. // This splits the hostname (a.b.c.tld) into segments (["a", "b", "c", "tld"]), reverse it (["tld", "c", "b", "a"])
  120. // and creates a final List<string> object.
  121. var segments = hostname.Split(new char[] { '.' }, StringSplitOptions.RemoveEmptyEntries)
  122. .Reverse()
  123. .ToList();
  124. string subKey = segments[0];
  125. segments.RemoveAt(0);
  126. HostSettings foundSettings = null;
  127. if (_rootNodes.TryGetValue(subKey, out var node))
  128. foundSettings = node.Find(segments);
  129. if (fallbackToWildcard && foundSettings == null && _rootNodes.TryGetValue("*", out var asteriskNode))
  130. foundSettings = asteriskNode.hostSettings;
  131. return foundSettings;
  132. }
  133. /// <summary>
  134. /// Clears all host-specific settings and resetting the default ("*") with default values.
  135. /// </summary>
  136. public void Clear()
  137. {
  138. _rootNodes.Clear();
  139. Add("*", new HostSettings());
  140. }
  141. }
  142. }