Upgrading requests
editUpgrade the requests of an HttpClient
Create an HstsClientHandler with the handler that sends the requests and the collection of policies to apply:
var policies = new HstsDomainPolicyCollection(includePreloadDomains: true);
using var client = new HttpClient(new HstsClientHandler(new SocketsHttpHandler(), policies), disposeHandler: true);
// Automatically upgrade to HTTPS as github.com is in the HSTS preload list
using var response = await client.GetAsync("http://github.com");A request is upgraded when its scheme is http and its host has a policy in the collection, either its own or the one of a parent domain that includes the subdomains. The handler then replaces the scheme with https:
- The default port becomes 443, and an explicit port is kept:
http://example.com:8080/becomeshttps://example.com:8080/. - The path, the query, the fragment and the user information are kept as they are.
- An internationalized host name matches the policies in its Unicode and its Punycode forms.
The other requests, including https ones, are sent unchanged.
The constructors that take no collection use HstsDomainPolicyCollection.Default, a collection shared by the whole process. See The shared collection.
Learn policies from the Strict-Transport-Security header
The handler reads the Strict-Transport-Security header of every response. When an HTTPS response has a valid one, the host is added to the collection, and the next requests to it are upgraded:
var policies = new HstsDomainPolicyCollection(includePreloadDomains: false);
using var client = new HttpClient(new HstsClientHandler(innerHandler, policies), disposeHandler: true);
// The HTTPS response carries "Strict-Transport-Security: max-age=31536000; includeSubDomains"
using var response1 = await client.GetAsync(new Uri("https://example.com/"), cancellationToken);
Assert.True(policies.MustUpgradeRequest("www.example.com"));
// From now on, the requests to the host and its subdomains are sent over HTTPS
using var response2 = await client.GetAsync(new Uri("http://www.example.com/page"), cancellationToken);
Assert.Equal(new Uri("https://www.example.com/page"), response2.RequestMessage?.RequestUri);Here, innerHandler stands for the handler that sends the requests; the server answers with Strict-Transport-Security: max-age=31536000; includeSubDomains.
The header is processed as RFC 6797 describes:
max-age(in seconds) is required: the policy expires after it. A header without it, or with a malformed one, is ignored, and the response is returned as usual.includeSubDomainsextends the policy to every subdomain of the host.- A header that repeats a directive, such as two
includeSubDomains, is ignored.includeSubDomainstakes no value:includeSubDomains=1does not extend the policy, but it still counts as a repetition. max-age=0removes the policy the host had declared. It does not remove a host of the preload list (see The preload list is a floor).- A header received over plaintext HTTP is ignored, so a network attacker cannot add or remove a policy.
- A header received from an IP address is ignored: only host names get a policy.
- When a response has several
Strict-Transport-Securityheaders, only the first one is read. - A
max-agelarger than 100 years is reduced to 100 years.
The policies learned this way are kept in memory only: they are lost when the process exits.
Register the handler with IHttpClientFactory
The constructors that take no inner handler let IHttpClientFactory set it:
services.AddTransient(_ => new HstsClientHandler(policies));
services.AddHttpClient("api")
.AddHttpMessageHandler<HstsClientHandler>()
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler());The handler takes over the redirects of the primary handler that the factory creates. See Redirects.