-
-
Notifications
You must be signed in to change notification settings - Fork 100
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
16 changed files
with
783 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
using System.Globalization; | ||
|
||
namespace Meziantou.Framework.Http; | ||
|
||
public sealed class HstsClientHandler : DelegatingHandler | ||
{ | ||
private readonly HstsDomainPolicyCollection _configuration; | ||
|
||
public HstsClientHandler(HttpMessageHandler innerHandler) | ||
: this(innerHandler, HstsDomainPolicyCollection.Default) | ||
{ | ||
} | ||
|
||
public HstsClientHandler(HttpMessageHandler innerHandler, HstsDomainPolicyCollection configuration) | ||
: base(innerHandler) | ||
{ | ||
_configuration = configuration; | ||
} | ||
|
||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) | ||
{ | ||
if (request.RequestUri?.Scheme == Uri.UriSchemeHttp && request.RequestUri.Port == 80) | ||
{ | ||
if (_configuration.Match(request.RequestUri.Host)) | ||
{ | ||
var builder = new UriBuilder(request.RequestUri) { Scheme = Uri.UriSchemeHttps }; | ||
builder.Port = 443; | ||
builder.Scheme = Uri.UriSchemeHttps; | ||
request.RequestUri = builder.Uri; | ||
} | ||
} | ||
|
||
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); | ||
|
||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security | ||
// Note: The Strict-Transport-Security header is ignored by the browser when your site has only been accessed using HTTP. | ||
// Once your site is accessed over HTTPS with no certificate errors, the browser knows your site is HTTPS-capable and | ||
// will honor the Strict-Transport-Security header. | ||
if (response.RequestMessage?.RequestUri?.Scheme == Uri.UriSchemeHttps && response.Headers.TryGetValues("Strict-Transport-Security", out var headers)) | ||
{ | ||
TimeSpan maxAge = default; | ||
var includeSubdomains = false; | ||
foreach (var header in headers) | ||
{ | ||
var headerSpan = header.AsSpan(); | ||
foreach (var part in headerSpan.Split(';')) | ||
{ | ||
var trimmed = headerSpan[part].Trim(); | ||
if (trimmed.StartsWith("max-age=", StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
var maxAgeValue = int.Parse(trimmed[8..], NumberStyles.None, CultureInfo.InvariantCulture); | ||
maxAge = TimeSpan.FromSeconds(maxAgeValue); | ||
} | ||
else if (trimmed.Equals("includeSubDomains", StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
includeSubdomains = true; | ||
} | ||
} | ||
} | ||
|
||
if (maxAge > TimeSpan.Zero) | ||
{ | ||
_configuration.Add(response.RequestMessage.RequestUri.Host, maxAge, includeSubdomains); | ||
} | ||
} | ||
|
||
return response; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
namespace Meziantou.Framework.Http; | ||
|
||
public sealed class HstsDomainPolicy | ||
{ | ||
internal HstsDomainPolicy(string host, DateTimeOffset expiresAt, bool includeSubdomains) | ||
{ | ||
Host = host; | ||
ExpiresAt = expiresAt; | ||
IncludeSubdomains = includeSubdomains; | ||
} | ||
|
||
public string Host { get; } | ||
public DateTimeOffset ExpiresAt { get; private set; } | ||
public bool IncludeSubdomains { get; private set; } | ||
|
||
public override string ToString() | ||
{ | ||
var result = Host + "; expires=" + ExpiresAt; | ||
if (IncludeSubdomains) | ||
{ | ||
result += "; includeSubdomains"; | ||
} | ||
return result; | ||
} | ||
} |
131 changes: 131 additions & 0 deletions
131
src/Meziantou.Framework.Http.Hsts/HstsDomainPolicyCollection.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
using System.Collections.Concurrent; | ||
using System.Collections; | ||
using System.ComponentModel.Design; | ||
|
||
namespace Meziantou.Framework.Http; | ||
|
||
public sealed partial class HstsDomainPolicyCollection : IEnumerable<HstsDomainPolicy> | ||
{ | ||
private readonly List<ConcurrentDictionary<string, HstsDomainPolicy>> _policies = new(capacity: 8); | ||
private readonly TimeProvider _timeProvider; | ||
|
||
public static HstsDomainPolicyCollection Default { get; } = new HstsDomainPolicyCollection(); | ||
|
||
[SetsRequiredMembers] | ||
public HstsDomainPolicyCollection(bool includePreloadDomains = true) | ||
: this(timeProvider: null, includePreloadDomains) | ||
{ | ||
} | ||
|
||
[SetsRequiredMembers] | ||
public HstsDomainPolicyCollection(TimeProvider? timeProvider, bool includePreloadDomains = true) | ||
{ | ||
_timeProvider = timeProvider ?? TimeProvider.System; | ||
if (includePreloadDomains) | ||
{ | ||
Initialize(_timeProvider); | ||
} | ||
} | ||
|
||
public void Add(string host, TimeSpan maxAge, bool includeSubdomains) | ||
{ | ||
Add(host, _timeProvider.GetUtcNow().Add(maxAge), includeSubdomains); | ||
} | ||
|
||
public void Add(string host, DateTimeOffset expiresAt, bool includeSubdomains) | ||
{ | ||
var partCount = CountSegments(host); | ||
ConcurrentDictionary<string, HstsDomainPolicy> dictionary; | ||
lock (_policies) | ||
{ | ||
for (var i = _policies.Count; i < partCount; i++) | ||
{ | ||
_policies.Add(new ConcurrentDictionary<string, HstsDomainPolicy>(StringComparer.OrdinalIgnoreCase)); | ||
} | ||
|
||
dictionary = _policies[partCount - 1]; | ||
} | ||
|
||
dictionary.AddOrUpdate(host, | ||
(key, arg) => new HstsDomainPolicy(key, arg.expiresAt, arg.includeSubdomains), | ||
(key, value, arg) => new HstsDomainPolicy(key, arg.expiresAt, arg.includeSubdomains), | ||
factoryArgument: (expiresAt, includeSubdomains)); | ||
} | ||
|
||
public bool Match(string host) | ||
{ | ||
var segments = CountSegments(host); | ||
for (var i = 0; i < _policies.Count && i < segments; i++) | ||
{ | ||
var dictionary = _policies[i]; | ||
var lastSegments = i == segments - 1 ? host : GetLastSegments(host, i + 1); | ||
|
||
#if NET9_0_OR_GREATER | ||
var lookup = dictionary.GetAlternateLookup<ReadOnlySpan<char>>(); | ||
if (lookup.TryGetValue(lastSegments, out var hsts)) | ||
#else | ||
if (dictionary.TryGetValue(lastSegments.ToString(), out var hsts)) | ||
#endif | ||
{ | ||
if (hsts.ExpiresAt < _timeProvider.GetUtcNow()) | ||
{ | ||
return false; | ||
} | ||
|
||
if (!hsts.IncludeSubdomains && i != segments - 1) | ||
{ | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
|
||
private static ReadOnlySpan<char> GetLastSegments(string host, int count) | ||
{ | ||
var hostSpan = host.AsSpan(); | ||
for (var i = 0; i < count; i++) | ||
{ | ||
var start = hostSpan.LastIndexOf('.'); | ||
hostSpan = hostSpan.Slice(0, start); | ||
} | ||
|
||
return host.AsSpan(hostSpan.Length + 1); | ||
} | ||
|
||
// internal for tests | ||
internal static int CountSegments(string host) | ||
{ | ||
// foo.bar.com -> 3 | ||
var count = 1; | ||
|
||
var index = -1; | ||
while (host.IndexOf('.', index + 1) is >= 0 and var newIndex) | ||
{ | ||
index = newIndex; | ||
count++; | ||
} | ||
|
||
return count; | ||
} | ||
|
||
public IEnumerator<HstsDomainPolicy> GetEnumerator() | ||
{ | ||
for (var i = 0; i < _policies.Count; i++) | ||
{ | ||
var dictionary = _policies[i]; | ||
if (dictionary is null) | ||
continue; | ||
|
||
foreach (var kvp in dictionary) | ||
{ | ||
yield return kvp.Value; | ||
} | ||
} | ||
} | ||
|
||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); | ||
} |
73 changes: 73 additions & 0 deletions
73
src/Meziantou.Framework.Http.Hsts/HstsDomainPolicyCollection.g.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
// <auto-generated /> | ||
// Data source: https://raw.githubusercontent.com/chromium/chromium/ba720cd91299fe45b6345be1971ee628af9bc3f5/net/http/transport_security_state_static.json | ||
// Commit date: 2024-12-18T23:38:47.0000000Z | ||
// #nullable enable // Roslyn doesn't like it :( | ||
|
||
using System.Collections.Concurrent; | ||
|
||
namespace Meziantou.Framework.Http; | ||
|
||
partial class HstsDomainPolicyCollection | ||
{ | ||
private void Initialize(TimeProvider timeProvider) | ||
{ | ||
var expires126Days = timeProvider.GetUtcNow().Add(TimeSpan.FromDays(126)); | ||
var expires365Days = timeProvider.GetUtcNow().Add(TimeSpan.FromDays(365)); | ||
var dict1 = new ConcurrentDictionary<string, HstsDomainPolicy>(concurrencyLevel: -1, capacity: 61, comparer: StringComparer.OrdinalIgnoreCase); | ||
_policies.Add(dict1); | ||
var dict2 = new ConcurrentDictionary<string, HstsDomainPolicy>(concurrencyLevel: -1, capacity: 149553, comparer: StringComparer.OrdinalIgnoreCase); | ||
_policies.Add(dict2); | ||
var dict3 = new ConcurrentDictionary<string, HstsDomainPolicy>(concurrencyLevel: -1, capacity: 11247, comparer: StringComparer.OrdinalIgnoreCase); | ||
_policies.Add(dict3); | ||
var dict4 = new ConcurrentDictionary<string, HstsDomainPolicy>(concurrencyLevel: -1, capacity: 197, comparer: StringComparer.OrdinalIgnoreCase); | ||
_policies.Add(dict4); | ||
var dict5 = new ConcurrentDictionary<string, HstsDomainPolicy>(concurrencyLevel: -1, capacity: 11, comparer: StringComparer.OrdinalIgnoreCase); | ||
_policies.Add(dict5); | ||
// Segment size: 1 | ||
_ = dict1.TryAdd("amazon", new HstsDomainPolicy("amazon", expires365Days, true)); | ||
_ = dict1.TryAdd("android", new HstsDomainPolicy("android", expires365Days, true)); | ||
_ = dict1.TryAdd("app", new HstsDomainPolicy("app", expires365Days, true)); | ||
_ = dict1.TryAdd("audible", new HstsDomainPolicy("audible", expires365Days, true)); | ||
_ = dict1.TryAdd("azure", new HstsDomainPolicy("azure", expires365Days, true)); | ||
_ = dict1.TryAdd("bank", new HstsDomainPolicy("bank", expires365Days, true)); | ||
_ = dict1.TryAdd("bing", new HstsDomainPolicy("bing", expires365Days, true)); | ||
_ = dict1.TryAdd("boo", new HstsDomainPolicy("boo", expires365Days, true)); | ||
_ = dict1.TryAdd("channel", new HstsDomainPolicy("channel", expires365Days, true)); | ||
_ = dict1.TryAdd("chrome", new HstsDomainPolicy("chrome", expires365Days, true)); | ||
// Segment size: 2 | ||
_ = dict2.TryAdd("0--1.de", new HstsDomainPolicy("0--1.de", expires365Days, true)); | ||
_ = dict2.TryAdd("0-0.io", new HstsDomainPolicy("0-0.io", expires365Days, true)); | ||
_ = dict2.TryAdd("0-0.lt", new HstsDomainPolicy("0-0.lt", expires365Days, true)); | ||
_ = dict2.TryAdd("0-24.com", new HstsDomainPolicy("0-24.com", expires365Days, true)); | ||
_ = dict2.TryAdd("0-24.net", new HstsDomainPolicy("0-24.net", expires365Days, true)); | ||
_ = dict2.TryAdd("0-9.com", new HstsDomainPolicy("0-9.com", expires365Days, true)); | ||
_ = dict2.TryAdd("0.sb", new HstsDomainPolicy("0.sb", expires365Days, true)); | ||
_ = dict2.TryAdd("00.eco", new HstsDomainPolicy("00.eco", expires365Days, true)); | ||
_ = dict2.TryAdd("00010110.nl", new HstsDomainPolicy("00010110.nl", expires365Days, true)); | ||
_ = dict2.TryAdd("0008.life", new HstsDomainPolicy("0008.life", expires365Days, true)); | ||
// Segment size: 3 | ||
_ = dict3.TryAdd("0.com.ms", new HstsDomainPolicy("0.com.ms", expires365Days, true)); | ||
_ = dict3.TryAdd("0ii0.eu.org", new HstsDomainPolicy("0ii0.eu.org", expires365Days, true)); | ||
_ = dict3.TryAdd("1-2-3bounce.co.uk", new HstsDomainPolicy("1-2-3bounce.co.uk", expires365Days, true)); | ||
_ = dict3.TryAdd("100plus.com.my", new HstsDomainPolicy("100plus.com.my", expires365Days, true)); | ||
_ = dict3.TryAdd("100plus.com.sg", new HstsDomainPolicy("100plus.com.sg", expires365Days, true)); | ||
_ = dict3.TryAdd("101warehousing.com.au", new HstsDomainPolicy("101warehousing.com.au", expires365Days, true)); | ||
_ = dict3.TryAdd("106.hi.cn", new HstsDomainPolicy("106.hi.cn", expires365Days, true)); | ||
_ = dict3.TryAdd("11tv.dp.ua", new HstsDomainPolicy("11tv.dp.ua", expires365Days, true)); | ||
_ = dict3.TryAdd("123host.com.au", new HstsDomainPolicy("123host.com.au", expires365Days, true)); | ||
_ = dict3.TryAdd("123noticias.com.br", new HstsDomainPolicy("123noticias.com.br", expires365Days, true)); | ||
// Segment size: 4 | ||
_ = dict4.TryAdd("1.0.0.1", new HstsDomainPolicy("1.0.0.1", expires365Days, false)); | ||
_ = dict4.TryAdd("1022996493.rsc.cdn77.org", new HstsDomainPolicy("1022996493.rsc.cdn77.org", expires126Days, true)); | ||
_ = dict4.TryAdd("1464424382.rsc.cdn77.org", new HstsDomainPolicy("1464424382.rsc.cdn77.org", expires126Days, true)); | ||
_ = dict4.TryAdd("1844329061.rsc.cdn77.org", new HstsDomainPolicy("1844329061.rsc.cdn77.org", expires126Days, true)); | ||
_ = dict4.TryAdd("1972969867.rsc.cdn77.org", new HstsDomainPolicy("1972969867.rsc.cdn77.org", expires126Days, true)); | ||
_ = dict4.TryAdd("agriculture.vic.gov.au", new HstsDomainPolicy("agriculture.vic.gov.au", expires365Days, true)); | ||
_ = dict4.TryAdd("alanburr.us.eu.org", new HstsDomainPolicy("alanburr.us.eu.org", expires365Days, true)); | ||
_ = dict4.TryAdd("allamakee.k12.ia.us", new HstsDomainPolicy("allamakee.k12.ia.us", expires365Days, true)); | ||
_ = dict4.TryAdd("api.mega.co.nz", new HstsDomainPolicy("api.mega.co.nz", expires365Days, true)); | ||
_ = dict4.TryAdd("armadale.wa.gov.au", new HstsDomainPolicy("armadale.wa.gov.au", expires365Days, true)); | ||
// Segment size: 5 | ||
_ = dict5.TryAdd("wnc-frontend-alb-1765173526.ap-northeast-2.elb.amazonaws.com", new HstsDomainPolicy("wnc-frontend-alb-1765173526.ap-northeast-2.elb.amazonaws.com", expires365Days, true)); | ||
} | ||
} |
14 changes: 14 additions & 0 deletions
14
src/Meziantou.Framework.Http.Hsts/Meziantou.Framework.Http.Hsts.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFrameworks>$(LatestTargetFrameworks)</TargetFrameworks> | ||
<RootNamespace>Meziantou.Framework.Http</RootNamespace> | ||
<Version>1.0.0</Version> | ||
<IsTrimmable>true</IsTrimmable> | ||
<Description>Provide an HttpClientHandler to upgrade request from http to https if a policy is set</Description> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<InternalsVisibleTo Include="Meziantou.Framework.Http.Hsts.Tests" /> | ||
</ItemGroup> | ||
</Project> |
Oops, something went wrong.