-
Notifications
You must be signed in to change notification settings - Fork 1
Add GuardHandler for basic request filtering and DI extensions #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| namespace Yllibed.HttpServer.Tests; | ||
|
|
||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.Extensions.Options; | ||
| using Yllibed.HttpServer.Extensions; | ||
| using Yllibed.HttpServer.Handlers; | ||
|
|
||
| [TestClass] | ||
| public class GenericDiHandlersFixture : FixtureBase | ||
| { | ||
| private sealed class EchoOptions | ||
| { | ||
| public string? Message { get; set; } | ||
| } | ||
|
|
||
| private sealed class EchoHandler : IHttpHandler | ||
| { | ||
| private readonly string _message; | ||
| public EchoHandler(IOptions<EchoOptions> options) | ||
| { | ||
| _message = options.Value.Message ?? "(null)"; | ||
| } | ||
|
|
||
| public Task HandleRequest(CancellationToken ct, IHttpServerRequest request, string relativePath) | ||
| { | ||
| if (string.Equals(relativePath, "/echo", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| request.SetResponse("text/plain", _message); | ||
| } | ||
| return Task.CompletedTask; | ||
| } | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public async Task AddHttpHandlerAndRegister_RegistersSimpleHandler() | ||
| { | ||
| var services = new ServiceCollection(); | ||
| services.AddYllibedHttpServer(_ => { }); | ||
|
|
||
| // Register a simple preconfigured StaticHandler via DI factory and ensure generic method can still wire it: | ||
| services.AddSingleton(new StaticHandler("/ping", "text/plain", "pong")); | ||
| services.AddHttpHandlerAndRegister<StaticHandler>(); // Remove the duplicate call | ||
|
|
||
| using var sp = services.BuildServiceProvider(); | ||
| var server = sp.GetRequiredService<Server>(); | ||
| var (uri4, _) = server.Start(); | ||
|
|
||
| using var client = new HttpClient(); | ||
| var res = await client.GetAsync(new Uri(uri4, "/ping"), CT).ConfigureAwait(false); | ||
| res.StatusCode.Should().Be(HttpStatusCode.OK); | ||
| (await res.Content.ReadAsStringAsync(CT).ConfigureAwait(false)).Should().Be("pong"); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public async Task AddHttpHandlerAndRegister_WithOptions_ConfiguresAndRegisters() | ||
| { | ||
| var services = new ServiceCollection(); | ||
| services.AddYllibedHttpServer(_ => { }); | ||
| services.AddHttpHandlerAndRegister<EchoHandler, EchoOptions>(o => o.Message = "hello from options"); | ||
|
|
||
| using var sp = services.BuildServiceProvider(); | ||
| var server = sp.GetRequiredService<Server>(); | ||
| var (uri4, _) = server.Start(); | ||
|
|
||
| using var client = new HttpClient(); | ||
| var res = await client.GetAsync(new Uri(uri4, "/echo"), CT).ConfigureAwait(false); | ||
| res.StatusCode.Should().Be(HttpStatusCode.OK); | ||
| (await res.Content.ReadAsStringAsync(CT).ConfigureAwait(false)).Should().Be("hello from options"); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| namespace Yllibed.HttpServer.Tests; | ||
|
|
||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Yllibed.HttpServer.Extensions; | ||
| using Yllibed.HttpServer.Handlers; | ||
|
|
||
| [TestClass] | ||
| public class GuardHandlerDiFixture : FixtureBase | ||
| { | ||
| [TestMethod] | ||
| public async Task AddGuardHandlerAndRegister_RegistersIntoServerPipeline() | ||
| { | ||
| var services = new ServiceCollection(); | ||
| services.AddYllibedHttpServer(); | ||
| services.AddGuardHandlerAndRegister(opts => | ||
| { | ||
| opts.AllowedMethods = new[] { "GET" }; | ||
| }); | ||
|
|
||
| using var sp = services.BuildServiceProvider(); | ||
| var server = sp.GetRequiredService<Server>(); | ||
| server.RegisterHandler(new StaticHandler("/ok", "text/plain", "OK")); | ||
| var (uri4, _) = server.Start(); | ||
|
|
||
| var requestUri = new Uri(uri4, "/ok"); | ||
|
|
||
| using var client = new HttpClient(); | ||
| // DELETE should be blocked by protection without manually resolving the handler | ||
| var blocked = await client.SendAsync(new HttpRequestMessage(HttpMethod.Delete, requestUri), CT).ConfigureAwait(false); | ||
| blocked.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); | ||
|
|
||
| // GET passes | ||
| var allowed = await client.GetAsync(requestUri, CT).ConfigureAwait(false); | ||
| allowed.StatusCode.Should().Be(HttpStatusCode.OK); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| namespace Yllibed.HttpServer.Tests; | ||
|
|
||
| using Yllibed.HttpServer.Handlers; | ||
|
|
||
| [TestClass] | ||
| public class GuardHandlerFixture : FixtureBase | ||
| { | ||
| [TestMethod] | ||
| public async Task GuardHandler_UrlTooLong_Returns414() | ||
| { | ||
| using var sut = new Server(); | ||
| // Very small limit for test | ||
| sut.RegisterHandler(new GuardHandler(maxUrlLength: 10)); | ||
| sut.RegisterHandler(new StaticHandler("ok", "text/plain", "OK")); | ||
|
|
||
| var (uri4, _) = sut.Start(); | ||
| var longPath = new string('a', 50); | ||
| var requestUri = new Uri(uri4, $"{longPath}"); | ||
|
|
||
| using var client = new HttpClient(); | ||
| var response = await client.GetAsync(requestUri, CT).ConfigureAwait(false); | ||
| response.StatusCode.Should().Be(HttpStatusCode.RequestUriTooLong); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public async Task GuardHandler_ContentLengthTooLarge_Returns413() | ||
| { | ||
| using var sut = new Server(); | ||
| // Limit to 16 bytes | ||
| sut.RegisterHandler(new GuardHandler(maxBodyBytes: 16)); | ||
| sut.RegisterHandler(new StaticHandler("ok", "text/plain", "OK")); | ||
|
|
||
| var (uri4, _) = sut.Start(); | ||
| var requestUri = new Uri(uri4, "ok"); | ||
|
|
||
| using var client = new HttpClient(); | ||
| var content = new string('x', 100); // > 16 bytes | ||
| var response = await client.PostAsync(requestUri, new StringContent(content), CT).ConfigureAwait(false); | ||
| response.StatusCode.Should().Be(HttpStatusCode.RequestEntityTooLarge); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public async Task GuardHandler_HeadersTooLarge_Returns431() | ||
| { | ||
| using var sut = new Server(); | ||
| // Keep overall header size tiny to force 431 | ||
| sut.RegisterHandler(new GuardHandler(maxHeadersTotalSize: 100)); | ||
| sut.RegisterHandler(new StaticHandler("ok", "text/plain", "OK")); | ||
|
|
||
| var (uri4, _) = sut.Start(); | ||
| var requestUri = new Uri(uri4, "ok"); | ||
|
|
||
| using var client = new HttpClient(); | ||
| var msg = new HttpRequestMessage(HttpMethod.Get, requestUri); | ||
| msg.Headers.Add("X-Long", new string('z', 1024)); | ||
| var response = await client.SendAsync(msg, CT).ConfigureAwait(false); | ||
| response.StatusCode.Should().Be((HttpStatusCode)431); | ||
| } | ||
| [TestMethod] | ||
| public async Task GuardHandler_MethodNotAllowed_Returns405() | ||
| { | ||
| using var sut = new Server(); | ||
| sut.RegisterHandler(new GuardHandler(allowedMethods: new[] { "GET" })); | ||
| sut.RegisterHandler(new StaticHandler("/ok", "text/plain", "OK")); | ||
|
|
||
| var (uri4, _) = sut.Start(); | ||
| var requestUri = new Uri(uri4, "/ok"); | ||
|
|
||
| using var client = new HttpClient(); | ||
| var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Delete, requestUri), CT).ConfigureAwait(false); | ||
| response.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public async Task GuardHandler_ForbiddenHost_Returns403() | ||
| { | ||
| using var sut = new Server(); | ||
| sut.RegisterHandler(new GuardHandler(allowedHosts: new[] { "localhost" })); | ||
| sut.RegisterHandler(new StaticHandler("/ok", "text/plain", "OK")); | ||
|
|
||
| var (uri4, _) = sut.Start(); | ||
| var requestUri = new Uri(uri4, "/ok"); | ||
|
|
||
| using var client = new HttpClient(); | ||
| var msg = new HttpRequestMessage(HttpMethod.Get, requestUri); | ||
| // Force a different Host header value to trigger 403: use machine name if not localhost | ||
| msg.Headers.Host = "example.com"; | ||
| var response = await client.SendAsync(msg, CT).ConfigureAwait(false); | ||
| response.StatusCode.Should().Be(HttpStatusCode.Forbidden); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public async Task GuardHandler_TooManyHeaders_Returns431() | ||
| { | ||
| using var sut = new Server(); | ||
| // Set very low header count limit to trigger 431 (Host header alone is 1; we'll add another) | ||
| sut.RegisterHandler(new GuardHandler(maxHeadersCount: 1)); | ||
| sut.RegisterHandler(new StaticHandler("/ok", "text/plain", "OK")); | ||
|
|
||
| var (uri4, _) = sut.Start(); | ||
| var requestUri = new Uri(uri4, "/ok"); | ||
|
|
||
| using var client = new HttpClient(); | ||
| var msg = new HttpRequestMessage(HttpMethod.Get, requestUri); | ||
| msg.Headers.Add("X-One", "1"); | ||
| var response = await client.SendAsync(msg, CT).ConfigureAwait(false); | ||
| response.StatusCode.Should().Be((HttpStatusCode)431); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public async Task GuardHandler_PassesThrough_ToNextHandler_WhenValid() | ||
| { | ||
| using var sut = new Server(); | ||
| sut.RegisterHandler(new GuardHandler(allowedMethods: new[] { "GET" })); | ||
| sut.RegisterHandler(new StaticHandler("/ok", "text/plain", "OK")); | ||
|
|
||
| var (uri4, _) = sut.Start(); | ||
| var requestUri = new Uri(uri4, "/ok"); | ||
|
|
||
| using var client = new HttpClient(); | ||
| var response = await client.GetAsync(requestUri, CT).ConfigureAwait(false); | ||
| response.StatusCode.Should().Be(HttpStatusCode.OK); | ||
| (await response.Content.ReadAsStringAsync(CT).ConfigureAwait(false)).Should().Be("OK"); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public async Task GuardHandler_AsWrapper_CallsInnerOnlyOnPass() | ||
| { | ||
| using var sut = new Server(); | ||
| var inner = new StaticHandler("/ok", "text/plain", "OK"); | ||
| var guard = new GuardHandler(allowedMethods: new[] { "GET" }, inner: inner); | ||
| sut.RegisterHandler(guard); | ||
|
|
||
| var (uri4, _) = sut.Start(); | ||
| var requestUri = new Uri(uri4, "/ok"); | ||
|
|
||
| using var client = new HttpClient(); | ||
| // DELETE should be blocked by protection and inner should not run | ||
| var blocked = await client.SendAsync(new HttpRequestMessage(HttpMethod.Delete, requestUri), CT).ConfigureAwait(false); | ||
| blocked.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); | ||
|
|
||
| // GET should pass and inner returns OK | ||
| var allowed = await client.GetAsync(requestUri, CT).ConfigureAwait(false); | ||
| allowed.StatusCode.Should().Be(HttpStatusCode.OK); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.