| | 1 | | using Kestrun.Utilities; |
| | 2 | | using Microsoft.AspNetCore.Antiforgery; |
| | 3 | | using Microsoft.AspNetCore.Cors.Infrastructure; |
| | 4 | | using Microsoft.AspNetCore.RateLimiting; |
| | 5 | | using Microsoft.AspNetCore.ResponseCompression; |
| | 6 | | using Serilog.Events; |
| | 7 | |
|
| | 8 | | namespace Kestrun.Hosting; |
| | 9 | |
|
| | 10 | | /// <summary> |
| | 11 | | /// Provides extension methods for configuring common HTTP middleware in Kestrun. |
| | 12 | | /// </summary> |
| | 13 | | public static class KestrunHttpMiddlewareExtensions |
| | 14 | | { |
| | 15 | | /// <summary> |
| | 16 | | /// Adds response compression to the application. |
| | 17 | | /// This overload allows you to specify configuration options. |
| | 18 | | /// </summary> |
| | 19 | | /// <param name="host">The KestrunHost instance to configure.</param> |
| | 20 | | /// <param name="options">The configuration options for response compression.</param> |
| | 21 | | /// <returns>The current KestrunHost instance.</returns> |
| | 22 | | public static KestrunHost AddResponseCompression(this KestrunHost host, ResponseCompressionOptions? options) |
| | 23 | | { |
| 2 | 24 | | if (host.HostLogger.IsEnabled(LogEventLevel.Debug)) |
| | 25 | | { |
| 0 | 26 | | host.HostLogger.Debug("Adding response compression with options: {@Options}", options); |
| | 27 | | } |
| | 28 | |
|
| 2 | 29 | | if (options == null) |
| | 30 | | { |
| 1 | 31 | | return host.AddResponseCompression(); // no options, use defaults |
| | 32 | | } |
| | 33 | |
|
| | 34 | | // delegate shim – re‑use the existing pipeline |
| 1 | 35 | | return host.AddResponseCompression(o => |
| 1 | 36 | | { |
| 0 | 37 | | o.EnableForHttps = options.EnableForHttps; |
| 0 | 38 | | o.MimeTypes = options.MimeTypes; |
| 0 | 39 | | o.ExcludedMimeTypes = options.ExcludedMimeTypes; |
| 1 | 40 | | // copy provider lists, levels, etc. if you expose them |
| 0 | 41 | | foreach (var p in options.Providers) |
| 1 | 42 | | { |
| 0 | 43 | | o.Providers.Add(p); |
| 1 | 44 | | } |
| 1 | 45 | | }); |
| | 46 | | } |
| | 47 | |
|
| | 48 | | /// <summary> |
| | 49 | | /// Adds response compression to the application. |
| | 50 | | /// This overload allows you to specify configuration options. |
| | 51 | | /// </summary> |
| | 52 | | /// <param name="host">The KestrunHost instance to configure.</param> |
| | 53 | | /// <param name="cfg">The configuration options for response compression.</param> |
| | 54 | | /// <returns>The current KestrunHost instance.</returns> |
| | 55 | | public static KestrunHost AddResponseCompression(this KestrunHost host, Action<ResponseCompressionOptions>? cfg = nu |
| | 56 | | { |
| 4 | 57 | | if (host.HostLogger.IsEnabled(LogEventLevel.Debug)) |
| | 58 | | { |
| 0 | 59 | | host.HostLogger.Debug("Adding response compression with configuration: {Config}", cfg); |
| | 60 | | } |
| | 61 | | // Service side |
| 4 | 62 | | _ = host.AddService(services => |
| 4 | 63 | | { |
| 0 | 64 | | _ = cfg == null ? services.AddResponseCompression() : services.AddResponseCompression(cfg); |
| 4 | 65 | | }); |
| | 66 | |
|
| | 67 | | // Middleware side |
| 4 | 68 | | return host.Use(app => app.UseResponseCompression()); |
| | 69 | | } |
| | 70 | |
|
| | 71 | | /// <summary> |
| | 72 | | /// Adds rate limiting to the application using the specified <see cref="RateLimiterOptions"/>. |
| | 73 | | /// </summary> |
| | 74 | | /// <param name="host">The KestrunHost instance to configure.</param> |
| | 75 | | /// <param name="cfg">The configuration options for rate limiting.</param> |
| | 76 | | /// <returns>The current KestrunHost instance.</returns> |
| | 77 | | public static KestrunHost AddRateLimiter(this KestrunHost host, RateLimiterOptions cfg) |
| | 78 | | { |
| 2 | 79 | | if (host.HostLogger.IsEnabled(LogEventLevel.Debug)) |
| | 80 | | { |
| 0 | 81 | | host.HostLogger.Debug("Adding rate limiter with configuration: {@Config}", cfg); |
| | 82 | | } |
| | 83 | |
|
| 2 | 84 | | if (cfg == null) |
| | 85 | | { |
| 1 | 86 | | return host.AddRateLimiter(); // fall back to your “blank” overload |
| | 87 | | } |
| | 88 | |
|
| 1 | 89 | | _ = host.AddService(services => |
| 1 | 90 | | { |
| 0 | 91 | | _ = services.AddRateLimiter(opts => opts.CopyFrom(cfg)); // ← single line! |
| 1 | 92 | | }); |
| | 93 | |
|
| 1 | 94 | | return host.Use(app => app.UseRateLimiter()); |
| | 95 | | } |
| | 96 | |
|
| | 97 | |
|
| | 98 | | /// <summary> |
| | 99 | | /// Adds rate limiting to the application using the specified configuration delegate. |
| | 100 | | /// </summary> |
| | 101 | | /// <param name="host">The KestrunHost instance to configure.</param> |
| | 102 | | /// <param name="cfg">An optional delegate to configure rate limiting options.</param> |
| | 103 | | /// <returns>The current KestrunHost instance.</returns> |
| | 104 | | public static KestrunHost AddRateLimiter(this KestrunHost host, Action<RateLimiterOptions>? cfg = null) |
| | 105 | | { |
| 3 | 106 | | if (host.HostLogger.IsEnabled(LogEventLevel.Debug)) |
| | 107 | | { |
| 0 | 108 | | host.HostLogger.Debug("Adding rate limiter with configuration: {HasConfig}", cfg != null); |
| | 109 | | } |
| | 110 | |
|
| | 111 | | // Register the rate limiter service |
| 3 | 112 | | _ = host.AddService(services => |
| 3 | 113 | | { |
| 0 | 114 | | _ = services.AddRateLimiter(cfg ?? (_ => { })); // Always pass a delegate |
| 3 | 115 | | }); |
| | 116 | |
|
| | 117 | | // Apply the middleware |
| 3 | 118 | | return host.Use(app => |
| 3 | 119 | | { |
| 0 | 120 | | if (host.HostLogger.IsEnabled(LogEventLevel.Debug)) |
| 3 | 121 | | { |
| 0 | 122 | | host.HostLogger.Debug("Registering rate limiter middleware"); |
| 3 | 123 | | } |
| 3 | 124 | |
|
| 0 | 125 | | _ = app.UseRateLimiter(); |
| 3 | 126 | | }); |
| | 127 | | } |
| | 128 | |
|
| | 129 | |
|
| | 130 | |
|
| | 131 | | /// <summary> |
| | 132 | | /// Adds antiforgery protection to the application. |
| | 133 | | /// This overload allows you to specify configuration options. |
| | 134 | | /// </summary> |
| | 135 | | /// <param name="host">The KestrunHost instance to configure.</param> |
| | 136 | | /// <param name="options">The antiforgery options to configure.</param> |
| | 137 | | /// <returns>The current KestrunHost instance.</returns> |
| | 138 | | public static KestrunHost AddAntiforgery(this KestrunHost host, AntiforgeryOptions? options) |
| | 139 | | { |
| 2 | 140 | | if (host.HostLogger.IsEnabled(LogEventLevel.Debug)) |
| | 141 | | { |
| 0 | 142 | | host.HostLogger.Debug("Adding Antiforgery with configuration: {@Config}", options); |
| | 143 | | } |
| | 144 | |
|
| 2 | 145 | | if (options == null) |
| | 146 | | { |
| 1 | 147 | | return host.AddAntiforgery(); // no config, use defaults |
| | 148 | | } |
| | 149 | |
|
| | 150 | | // Delegate to the Action-based overload |
| 1 | 151 | | return host.AddAntiforgery(cfg => |
| 1 | 152 | | { |
| 0 | 153 | | cfg.Cookie = options.Cookie; |
| 0 | 154 | | cfg.FormFieldName = options.FormFieldName; |
| 0 | 155 | | cfg.HeaderName = options.HeaderName; |
| 0 | 156 | | cfg.SuppressXFrameOptionsHeader = options.SuppressXFrameOptionsHeader; |
| 1 | 157 | | }); |
| | 158 | | } |
| | 159 | |
|
| | 160 | | /// <summary> |
| | 161 | | /// Adds antiforgery protection to the application. |
| | 162 | | /// </summary> |
| | 163 | | /// <param name="host">The KestrunHost instance to configure.</param> |
| | 164 | | /// <param name="setupAction">An optional action to configure the antiforgery options.</param> |
| | 165 | | /// <returns>The current KestrunHost instance.</returns> |
| | 166 | | public static KestrunHost AddAntiforgery(this KestrunHost host, Action<AntiforgeryOptions>? setupAction = null) |
| | 167 | | { |
| 4 | 168 | | if (host.HostLogger.IsEnabled(LogEventLevel.Debug)) |
| | 169 | | { |
| 0 | 170 | | host.HostLogger.Debug("Adding Antiforgery with configuration: {@Config}", setupAction); |
| | 171 | | } |
| | 172 | | // Service side |
| 4 | 173 | | _ = host.AddService(services => |
| 4 | 174 | | { |
| 0 | 175 | | _ = setupAction == null ? services.AddAntiforgery() : services.AddAntiforgery(setupAction); |
| 4 | 176 | | }); |
| | 177 | |
|
| | 178 | | // Middleware side |
| 4 | 179 | | return host.Use(app => app.UseAntiforgery()); |
| | 180 | | } |
| | 181 | |
|
| | 182 | |
|
| | 183 | | /// <summary> |
| | 184 | | /// Adds a CORS policy named "AllowAll" that allows any origin, method, and header. |
| | 185 | | /// </summary> |
| | 186 | | /// <param name="host">The KestrunHost instance to configure.</param> |
| | 187 | | /// <returns>The current KestrunHost instance.</returns> |
| | 188 | | public static KestrunHost AddCorsAllowAll(this KestrunHost host) => |
| 1 | 189 | | host.AddCors("AllowAll", b => b.AllowAnyOrigin() |
| 1 | 190 | | .AllowAnyMethod() |
| 1 | 191 | | .AllowAnyHeader()); |
| | 192 | |
|
| | 193 | | /// <summary> |
| | 194 | | /// Registers a named CORS policy that was already composed with a |
| | 195 | | /// <see cref="CorsPolicyBuilder"/> and applies that policy in the pipeline. |
| | 196 | | /// </summary> |
| | 197 | | /// <param name="host">The KestrunHost instance to configure.</param> |
| | 198 | | /// <param name="policyName">The name to store/apply the policy under.</param> |
| | 199 | | /// <param name="builder"> |
| | 200 | | /// A fully‑configured <see cref="CorsPolicyBuilder"/>. |
| | 201 | | /// Callers typically chain <c>.WithOrigins()</c>, <c>.WithMethods()</c>, |
| | 202 | | /// etc. before passing it here. |
| | 203 | | /// </param> |
| | 204 | | public static KestrunHost AddCors(this KestrunHost host, string policyName, CorsPolicyBuilder builder) |
| | 205 | | { |
| 2 | 206 | | ArgumentException.ThrowIfNullOrWhiteSpace(policyName); |
| 2 | 207 | | ArgumentNullException.ThrowIfNull(builder); |
| | 208 | |
|
| | 209 | | // 1️⃣ Service‑time registration |
| 1 | 210 | | _ = host.AddService(services => |
| 1 | 211 | | { |
| 0 | 212 | | _ = services.AddCors(options => |
| 0 | 213 | | { |
| 0 | 214 | | options.AddPolicy(policyName, builder.Build()); |
| 0 | 215 | | }); |
| 1 | 216 | | }); |
| | 217 | |
|
| | 218 | | // 2️⃣ Middleware‑time application |
| 1 | 219 | | return host.Use(app => app.UseCors(policyName)); |
| | 220 | | } |
| | 221 | |
|
| | 222 | | /// <summary> |
| | 223 | | /// Registers a named CORS policy that was already composed with a |
| | 224 | | /// <see cref="CorsPolicyBuilder"/> and applies that policy in the pipeline. |
| | 225 | | /// </summary> |
| | 226 | | /// <param name="host">The KestrunHost instance to configure.</param> |
| | 227 | | /// <param name="policyName">The name to store/apply the policy under.</param> |
| | 228 | | /// <param name="buildPolicy">An action to configure the CORS policy.</param> |
| | 229 | | /// <returns>The current KestrunHost instance.</returns> |
| | 230 | | /// <exception cref="ArgumentException">Thrown when the policy name is null or whitespace.</exception> |
| | 231 | | public static KestrunHost AddCors(this KestrunHost host, string policyName, Action<CorsPolicyBuilder> buildPolicy) |
| | 232 | | { |
| 5 | 233 | | if (host.HostLogger.IsEnabled(LogEventLevel.Debug)) |
| | 234 | | { |
| 0 | 235 | | host.HostLogger.Debug("Adding CORS policy: {PolicyName}", policyName); |
| | 236 | | } |
| | 237 | |
|
| 5 | 238 | | if (string.IsNullOrWhiteSpace(policyName)) |
| | 239 | | { |
| 2 | 240 | | throw new ArgumentException("Policy name required.", nameof(policyName)); |
| | 241 | | } |
| | 242 | |
|
| 3 | 243 | | ArgumentNullException.ThrowIfNull(buildPolicy); |
| | 244 | |
|
| 2 | 245 | | _ = host.AddService(s => |
| 2 | 246 | | { |
| 0 | 247 | | _ = s.AddCors(o => o.AddPolicy(policyName, buildPolicy)); |
| 2 | 248 | | }); |
| | 249 | |
|
| | 250 | | // apply only that policy |
| 2 | 251 | | return host.Use(app => app.UseCors(policyName)); |
| | 252 | | } |
| | 253 | | } |