| | | 1 | | using System.Security.Claims; |
| | | 2 | | using System.Security.Cryptography; |
| | | 3 | | using System.Text; |
| | | 4 | | using System.Text.Json; |
| | | 5 | | using Elsa.Abstractions; |
| | | 6 | | using Elsa.Resilience.Options; |
| | | 7 | | using Microsoft.AspNetCore.Http; |
| | | 8 | | using Microsoft.Extensions.Options; |
| | | 9 | | using static Elsa.Resilience.Endpoints.SimulateResponse.StatusCodeMessageLookup; |
| | | 10 | | |
| | | 11 | | namespace Elsa.Resilience.Endpoints.SimulateResponse; |
| | | 12 | | |
| | 31 | 13 | | public class SimulateResponseEndpoint(SimulateResponseSessionStore sessionStore, IOptions<SimulateResponseOptions> optio |
| | | 14 | | { |
| | 1 | 15 | | private static readonly int[] DefaultCodes = [429, 503, 200]; |
| | 31 | 16 | | private readonly SimulateResponseOptions _options = options.Value; |
| | | 17 | | |
| | | 18 | | public override void Configure() |
| | | 19 | | { |
| | 14 | 20 | | Get("/simulate-response"); |
| | 14 | 21 | | ConfigurePermissions("exec:*", "exec:resilience", "exec:resilience:simulate-response"); |
| | 14 | 22 | | } |
| | | 23 | | |
| | | 24 | | public override async Task HandleAsync(CancellationToken ct) |
| | | 25 | | { |
| | 17 | 26 | | if (!TryGetSessionId(out var sessionId, out var error) || !TryGetCodes(out var codes, out error)) |
| | | 27 | | { |
| | 6 | 28 | | AddError(error); |
| | 6 | 29 | | await Send.ErrorsAsync(StatusCodes.Status400BadRequest, ct); |
| | 6 | 30 | | return; |
| | | 31 | | } |
| | | 32 | | |
| | 11 | 33 | | var scopedSessionId = CreateScopedSessionId(sessionId); |
| | 11 | 34 | | if (!sessionStore.TryGetNextIndex(scopedSessionId, codes.Length, out var nextIndex)) |
| | | 35 | | { |
| | 1 | 36 | | AddError($"The maximum number of active simulate-response sessions ({_options.SessionCapacity}) has been rea |
| | 1 | 37 | | await Send.ErrorsAsync(StatusCodes.Status429TooManyRequests, ct); |
| | 1 | 38 | | return; |
| | | 39 | | } |
| | | 40 | | |
| | 10 | 41 | | var currentCode = codes[nextIndex]; |
| | 10 | 42 | | var message = StatusMessages.TryGetValue(currentCode, out var reason) |
| | 10 | 43 | | ? reason |
| | 10 | 44 | | : $"Status Code {currentCode}"; |
| | | 45 | | |
| | 10 | 46 | | await Send.ResponseAsync(new(message), currentCode, ct); |
| | 17 | 47 | | } |
| | | 48 | | |
| | | 49 | | private bool TryGetSessionId(out string sessionId, out string error) |
| | | 50 | | { |
| | 17 | 51 | | sessionId = "default"; |
| | 17 | 52 | | error = ""; |
| | 17 | 53 | | var sessionIdParam = HttpContext.Request.Query["sessionId"].FirstOrDefault(); |
| | | 54 | | |
| | 17 | 55 | | if (string.IsNullOrWhiteSpace(sessionIdParam)) |
| | 5 | 56 | | return true; |
| | | 57 | | |
| | 12 | 58 | | if (sessionIdParam.Length > _options.MaxSessionIdLength) |
| | | 59 | | { |
| | 1 | 60 | | error = $"The sessionId query parameter must be {_options.MaxSessionIdLength} characters or fewer."; |
| | 1 | 61 | | return false; |
| | | 62 | | } |
| | | 63 | | |
| | 11 | 64 | | sessionId = sessionIdParam; |
| | 11 | 65 | | return true; |
| | | 66 | | } |
| | | 67 | | |
| | | 68 | | private bool TryGetCodes(out int[] codes, out string error) |
| | | 69 | | { |
| | 16 | 70 | | codes = DefaultCodes; |
| | 16 | 71 | | error = ""; |
| | 16 | 72 | | var codesParam = HttpContext.Request.Query["codes"].FirstOrDefault(); |
| | | 73 | | |
| | 16 | 74 | | if (string.IsNullOrWhiteSpace(codesParam)) |
| | 0 | 75 | | return true; |
| | | 76 | | |
| | 16 | 77 | | if (codesParam.Length > _options.MaxCodesQueryLength) |
| | | 78 | | { |
| | 1 | 79 | | error = $"The codes query parameter must be {_options.MaxCodesQueryLength} characters or fewer."; |
| | 1 | 80 | | return false; |
| | | 81 | | } |
| | | 82 | | |
| | | 83 | | try |
| | | 84 | | { |
| | 15 | 85 | | using var document = JsonDocument.Parse(codesParam); |
| | | 86 | | |
| | 14 | 87 | | if (document.RootElement.ValueKind != JsonValueKind.Array) |
| | | 88 | | { |
| | 0 | 89 | | error = "The codes query parameter must be a JSON array of HTTP status codes."; |
| | 0 | 90 | | return false; |
| | | 91 | | } |
| | | 92 | | |
| | 14 | 93 | | var parsedCodes = new List<int>(); |
| | 79 | 94 | | foreach (var codeElement in document.RootElement.EnumerateArray()) |
| | | 95 | | { |
| | 27 | 96 | | if (parsedCodes.Count >= _options.MaxCodes) |
| | | 97 | | { |
| | 1 | 98 | | error = $"The codes query parameter can contain at most {_options.MaxCodes} status codes."; |
| | 1 | 99 | | return false; |
| | | 100 | | } |
| | | 101 | | |
| | 26 | 102 | | if (codeElement.ValueKind != JsonValueKind.Number || !codeElement.TryGetInt32(out var code) || code is < |
| | | 103 | | { |
| | 2 | 104 | | error = "The codes query parameter must contain HTTP status codes between 100 and 599."; |
| | 2 | 105 | | return false; |
| | | 106 | | } |
| | | 107 | | |
| | 24 | 108 | | parsedCodes.Add(code); |
| | | 109 | | } |
| | | 110 | | |
| | 11 | 111 | | if (parsedCodes.Count == 0) |
| | | 112 | | { |
| | 0 | 113 | | error = "The codes query parameter must contain at least one status code."; |
| | 0 | 114 | | return false; |
| | | 115 | | } |
| | | 116 | | |
| | 11 | 117 | | codes = parsedCodes.ToArray(); |
| | 11 | 118 | | return true; |
| | | 119 | | } |
| | 1 | 120 | | catch (JsonException) |
| | | 121 | | { |
| | 1 | 122 | | error = "The codes query parameter must be valid JSON."; |
| | 1 | 123 | | return false; |
| | | 124 | | } |
| | 15 | 125 | | } |
| | | 126 | | |
| | | 127 | | private string CreateScopedSessionId(string sessionId) |
| | | 128 | | { |
| | 11 | 129 | | var key = $"{GetAuthenticatedIdentityKey()}\0{sessionId}"; |
| | 11 | 130 | | return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(key))); |
| | | 131 | | } |
| | | 132 | | |
| | | 133 | | private string GetAuthenticatedIdentityKey() |
| | | 134 | | { |
| | 11 | 135 | | var user = HttpContext.User; |
| | 11 | 136 | | var identityClaim = user.FindFirst(ClaimTypes.NameIdentifier) |
| | 11 | 137 | | ?? user.FindFirst("sub") |
| | 11 | 138 | | ?? user.FindFirst("client_id") |
| | 11 | 139 | | ?? user.FindFirst("name") |
| | 11 | 140 | | ?? user.FindFirst(ClaimTypes.Name); |
| | | 141 | | |
| | 11 | 142 | | if (identityClaim != null) |
| | 11 | 143 | | return $"{identityClaim.Type}:{identityClaim.Value}"; |
| | | 144 | | |
| | 0 | 145 | | if (!string.IsNullOrWhiteSpace(user.Identity?.Name)) |
| | 0 | 146 | | return $"name:{user.Identity.Name}"; |
| | | 147 | | |
| | 0 | 148 | | var claimsKey = string.Join('\u001e', user.Claims |
| | 0 | 149 | | .Where(x => x.Type != "permissions") |
| | 0 | 150 | | .OrderBy(x => x.Type, StringComparer.Ordinal) |
| | 0 | 151 | | .ThenBy(x => x.Issuer, StringComparer.Ordinal) |
| | 0 | 152 | | .ThenBy(x => x.Value, StringComparer.Ordinal) |
| | 0 | 153 | | .Select(x => $"{x.Type}:{x.Issuer}:{x.Value}")); |
| | | 154 | | |
| | 0 | 155 | | return !string.IsNullOrEmpty(claimsKey) |
| | 0 | 156 | | ? claimsKey |
| | 0 | 157 | | : $"auth:{user.Identity?.AuthenticationType ?? "unknown"}"; |
| | | 158 | | } |
| | | 159 | | } |