| | | 1 | | using System.Net; |
| | | 2 | | |
| | | 3 | | namespace ArturRios.Heimdall.WebApi.Security; |
| | | 4 | | |
| | | 5 | | /// <summary> |
| | | 6 | | /// Chooses the rate-limit partition an anonymous-auth request is counted in: the address of the |
| | | 7 | | /// connection, unless a trusted client system names the address of the end user it is acting for. |
| | | 8 | | /// </summary> |
| | | 9 | | /// <remarks> |
| | | 10 | | /// The decision is a pure function of the request and <see cref="TrustedCallerOptions" /> so it can |
| | | 11 | | /// be tested without a host. It never fails a request: a wrong or missing key, an unusable address, |
| | | 12 | | /// or no configured keys all fall back to the connection address, so the headers cannot be used to |
| | | 13 | | /// probe for the keys by their effect on anything but the caller's own bucket. A named address is |
| | | 14 | | /// counted under <see cref="NamedAddressPrefix" /> so it can never be the bucket of a real |
| | | 15 | | /// connection from that address. Neither header value is logged. |
| | | 16 | | /// </remarks> |
| | | 17 | | public static class AuthRateLimitPartition |
| | | 18 | | { |
| | | 19 | | /// <summary>Header carrying one of the configured <see cref="TrustedCallerOptions" /> keys.</summary> |
| | | 20 | | public const string CallerKeyHeader = "X-Heimdall-Caller-Key"; |
| | | 21 | | |
| | | 22 | | /// <summary>Header carrying the address of the end user the trusted caller is acting for.</summary> |
| | | 23 | | public const string ClientAddressHeader = "X-Heimdall-Client-Address"; |
| | | 24 | | |
| | | 25 | | /// <summary> |
| | | 26 | | /// Prefix of every partition a trusted caller names. A direct connection keeps the unprefixed |
| | | 27 | | /// key, so the two never share a bucket. |
| | | 28 | | /// </summary> |
| | | 29 | | public const string NamedAddressPrefix = "caller:"; |
| | | 30 | | |
| | | 31 | | public static string KeyFor(HttpContext context, TrustedCallerOptions callers) |
| | | 32 | | { |
| | 773 | 33 | | if (callers.Configured && |
| | 773 | 34 | | TryReadSingle(context.Request.Headers[CallerKeyHeader], out var key) && |
| | 773 | 35 | | callers.Matches(key) && |
| | 773 | 36 | | TryReadSingle(context.Request.Headers[ClientAddressHeader], out var address) && |
| | 773 | 37 | | IPAddress.TryParse(address, out var clientAddress)) |
| | | 38 | | { |
| | | 39 | | // Its own namespace: a named address is counted apart from a direct connection from the |
| | | 40 | | // same address, so a key holder cannot spend a real caller's budget by naming them. |
| | 138 | 41 | | return NamedAddressPrefix + Startup.RateLimitPartitionKey(clientAddress); |
| | | 42 | | } |
| | | 43 | | |
| | 635 | 44 | | return Startup.RateLimitPartitionKey(context.Connection.RemoteIpAddress); |
| | | 45 | | } |
| | | 46 | | |
| | | 47 | | // A header sent twice is ambiguous, and the safe reading of an ambiguous credential is none. |
| | | 48 | | private static bool TryReadSingle(Microsoft.Extensions.Primitives.StringValues values, out string value) |
| | | 49 | | { |
| | 916 | 50 | | value = values.Count == 1 ? values[0] ?? string.Empty : string.Empty; |
| | | 51 | | |
| | 916 | 52 | | return value.Length > 0; |
| | | 53 | | } |
| | | 54 | | } |