• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

loresoft / HashGate / 29697765278

19 Jul 2026 05:55PM UTC coverage: 65.982% (+2.8%) from 63.149%
29697765278

push

github

pwelter34
Add AddHmacClient and runtime options updates

235 of 472 branches covered (49.79%)

Branch coverage included in aggregate %.

146 of 173 new or added lines in 11 files covered. (84.39%)

1 existing line in 1 file now uncovered.

696 of 939 relevant lines covered (74.12%)

26.54 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.44
/src/HashGate.HttpClient/HmacAuthenticationHttpHandler.cs
1
using System.Net.Http;
2

3
using Microsoft.Extensions.Options;
4

5
namespace HashGate.HttpClient;
6

7
/// <summary>
8
/// An HTTP message handler that automatically adds HMAC authentication headers to outgoing HTTP requests.
9
/// This handler integrates with the .NET HTTP client pipeline to transparently sign requests using HMAC-SHA256.
10
/// </summary>
11
/// <remarks>
12
/// <para>
13
/// The handler automatically adds the following headers to requests that don't already have an Authorization header:
14
/// </para>
15
/// <list type="bullet">
16
/// <item><description><c>x-timestamp</c> - Current Unix timestamp</description></item>
17
/// <item><description><c>x-content-sha256</c> - Base64-encoded SHA256 hash of the request body</description></item>
18
/// <item><description><c>Authorization</c> - HMAC authentication header with client ID, signed headers, and signature</description></item>
19
/// </list>
20
/// <para>
21
/// This handler should be registered in the dependency injection container and used with HttpClient instances
22
/// that need to authenticate using HMAC. It respects existing Authorization headers and will not overwrite them.
23
/// </para>
24
/// </remarks>
25
/// <example>
26
/// <para>Register and use with HttpClient:</para>
27
/// <code>
28
/// // Register the handler
29
/// services.AddHmacAuthentication(options =>
30
/// {
31
///     options.Client = "my-client-id";
32
///     options.Secret = "my-secret-key";
33
/// });
34
///
35
/// // Use with HttpClient
36
/// services.AddHttpClient("ApiClient")
37
///     .AddHttpMessageHandler&lt;HmacAuthenticationHttpHandler&gt;();
38
///
39
/// // The handler will automatically sign all requests made through this client
40
/// var client = httpClientFactory.CreateClient("ApiClient");
41
/// var response = await client.GetAsync("https://api.example.com/data");
42
/// </code>
43
/// </example>
44
public class HmacAuthenticationHttpHandler : DelegatingHandler
45
{
46
    /// <summary>
47
    /// A placeholder base address used to indicate that the actual base address should be applied later.
48
    /// </summary>
49
    internal static readonly Uri DeferredBaseAddress = new("http://hashgate.invalid/");
1✔
50

51
    private readonly IOptionsMonitor<HmacAuthenticationOptions> _optionsMonitor;
52
    private readonly string _optionsName;
53

54
    /// <summary>
55
    /// Initializes a new instance of the <see cref="HmacAuthenticationHttpHandler"/> class.
56
    /// </summary>
57
    /// <param name="optionsMonitor">The monitored HMAC authentication options containing client credentials and configuration.</param>
58
    /// <param name="optionsName">The named options instance to use, or <c>null</c> to use the default options instance.</param>
59
    /// <exception cref="ArgumentNullException">Thrown when <paramref name="optionsMonitor"/> is <c>null</c>.</exception>
60
    public HmacAuthenticationHttpHandler(IOptionsMonitor<HmacAuthenticationOptions> optionsMonitor, string? optionsName = null)
12✔
61
    {
62
        if (optionsMonitor is null)
12!
NEW
63
            throw new ArgumentNullException(nameof(optionsMonitor));
×
64

65
        // CurrentValue is equivalent to Get(Options.DefaultName), so the default and named
66
        // cases collapse into a single Get(...) call at request time.
67
        _optionsMonitor = optionsMonitor;
12✔
68
        _optionsName = optionsName ?? Microsoft.Extensions.Options.Options.DefaultName;
12✔
69
    }
12✔
70

71
    /// <summary>
72
    /// Sends an HTTP request to the inner handler to send to the server as an asynchronous operation.
73
    /// If the request does not already contain an Authorization header, HMAC authentication headers are automatically added.
74
    /// </summary>
75
    /// <param name="request">The HTTP request message to send to the server.</param>
76
    /// <param name="cancellationToken">A cancellation token to cancel operation.</param>
77
    /// <returns>
78
    /// A task that represents the asynchronous operation. The task result contains the HTTP response message.
79
    /// </returns>
80
    /// <remarks>
81
    /// <para>
82
    /// This method checks if the request already has an Authorization header. If not, it calls the
83
    /// <see cref="HttpRequestMessageExtensions.AddHmacAuthentication(HttpRequestMessage, HmacAuthenticationOptions, CancellationToken)"/>
84
    /// extension method to add the required HMAC authentication headers including:
85
    /// </para>
86
    /// <list type="bullet">
87
    /// <item><description>Timestamp header for request timing validation</description></item>
88
    /// <item><description>Content hash header for request body integrity</description></item>
89
    /// <item><description>Authorization header with HMAC signature</description></item>
90
    /// </list>
91
    /// <para>
92
    /// The handler preserves any existing Authorization header to allow for manual authentication control
93
    /// or to prevent double-signing of requests.
94
    /// </para>
95
    /// </remarks>
96
    /// <exception cref="ArgumentNullException">Thrown when <paramref name="request"/> is <c>null</c>.</exception>
97
    /// <exception cref="InvalidOperationException">Thrown when HMAC authentication options are invalid or incomplete.</exception>
98
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
99
    {
100
        if (request == null)
11!
NEW
101
            throw new ArgumentNullException(nameof(request));
×
102

103
        var options = _optionsMonitor.Get(_optionsName);
11✔
104
        ApplyBaseAddress(request, options);
11✔
105

106
        // If the request does not already have an Authorization header, add HMAC headers
107
        if (request.Headers.Authorization == null)
11✔
108
            await request.AddHmacAuthentication(options, cancellationToken).ConfigureAwait(false);
11✔
109

110
        return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
11✔
111
    }
11✔
112

113
    private static void ApplyBaseAddress(HttpRequestMessage request, HmacAuthenticationOptions options)
114
    {
115
        if (request.RequestUri == null)
11!
NEW
116
            return;
×
117

118
        if (options.BaseAddress == null)
11✔
119
        {
120
            // A null options base address is valid when the HttpClient supplies its own base address
121
            // (or the caller uses absolute request URIs); those requests already carry a usable absolute URI.
122
            // Only fail when there is nothing to resolve against: a relative URI, or the unresolved deferred
123
            // placeholder, either of which would otherwise be sent to the invalid placeholder host.
124
            if (!request.RequestUri.IsAbsoluteUri || IsDeferredBaseAddress(request.RequestUri))
2!
125
            {
NEW
126
                throw new InvalidOperationException(
×
NEW
127
                    "The HMAC authentication base address has not been configured. " +
×
NEW
128
                    "Set HmacAuthenticationOptions.BaseAddress, configure the HttpClient base address, or provide an absolute request URI.");
×
129
            }
130

131
            return;
2✔
132
        }
133

134
        if (!request.RequestUri.IsAbsoluteUri)
9!
135
        {
NEW
136
            request.RequestUri = new Uri(options.BaseAddress, request.RequestUri);
×
NEW
137
            return;
×
138
        }
139

140
        if (IsDeferredBaseAddress(request.RequestUri))
9✔
141
        {
142
            var relativeUri = request.RequestUri.PathAndQuery.TrimStart('/');
1✔
143
            request.RequestUri = new Uri(options.BaseAddress, relativeUri);
1✔
144
        }
145
    }
9✔
146

147
    private static bool IsDeferredBaseAddress(Uri requestUri)
148
        => string.Equals(requestUri.Scheme, DeferredBaseAddress.Scheme, StringComparison.OrdinalIgnoreCase)
11✔
149
            && string.Equals(requestUri.Host, DeferredBaseAddress.Host, StringComparison.OrdinalIgnoreCase)
11✔
150
            && requestUri.Port == DeferredBaseAddress.Port;
11✔
151
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc