| | | 1 | | /******************************************************************************** |
| | | 2 | | * HttpListenerRouter.cs * |
| | | 3 | | * * |
| | | 4 | | * Author: Denes Solti * |
| | | 5 | | ********************************************************************************/ |
| | | 6 | | using System; |
| | | 7 | | using System.Collections.Frozen; |
| | | 8 | | using System.Collections.Generic; |
| | | 9 | | using System.IO; |
| | | 10 | | using System.Net; |
| | | 11 | | using System.Net.Http; |
| | | 12 | | using System.Net.Http.Headers; |
| | | 13 | | using System.Threading; |
| | | 14 | | using System.Threading.Tasks; |
| | | 15 | | |
| | | 16 | | namespace NanoRoute |
| | | 17 | | { |
| | | 18 | | using Internals; |
| | | 19 | | |
| | | 20 | | /// <summary> |
| | | 21 | | /// Routes <see cref="HttpListenerContext"/> instances through a NanoRoute pipeline. |
| | | 22 | | /// </summary> |
| | | 23 | | /// <remarks> |
| | | 24 | | /// This adapter converts incoming <see cref="HttpListener"/> traffic into the core |
| | | 25 | | /// <see cref="HttpRequestMessage"/>/<see cref="HttpResponseMessage"/> pipeline used by NanoRoute. |
| | | 26 | | /// </remarks> |
| | | 27 | | public class HttpListenerRouter: Router |
| | | 28 | | { |
| | | 29 | | // https://learn.microsoft.com/en-us/dotnet/api/system.net.httplistenerresponse.headers?view=net-10.0#remarks |
| | 1 | 30 | | private static readonly FrozenSet<string> s_reservedHeaders = new List<string> |
| | 1 | 31 | | { |
| | 1 | 32 | | "Content-Length", |
| | 1 | 33 | | "Transfer-Encoding", |
| | 1 | 34 | | "Keep-Alive", |
| | 1 | 35 | | "Server" |
| | 1 | 36 | | }.ToFrozenSet(StringComparer.OrdinalIgnoreCase); |
| | | 37 | | |
| | | 38 | | private static async Task HandleResponse(HttpResponseMessage responseMessage, HttpListenerResponse response, Can |
| | | 39 | | { |
| | | 40 | | response.StatusCode = (int) responseMessage.StatusCode; |
| | | 41 | | |
| | | 42 | | CopyResponseHeaders(responseMessage.Headers); |
| | | 43 | | |
| | | 44 | | if (responseMessage.Content is not null) |
| | | 45 | | { |
| | | 46 | | CopyResponseHeaders(responseMessage.Content.Headers); |
| | | 47 | | |
| | | 48 | | using Stream buffer = await responseMessage.Content.ReadAsStreamAsync(); |
| | | 49 | | |
| | | 50 | | // https://github.com/dotnet/dotnet/blob/b0f34d51fccc69fd334253924abd8d6853fad7aa/src/runtime/src/librar |
| | | 51 | | await buffer.CopyToAsync(response.OutputStream, 81920, cancellation); |
| | | 52 | | } |
| | | 53 | | |
| | | 54 | | response.Close(); |
| | | 55 | | |
| | | 56 | | void CopyResponseHeaders(IEnumerable<KeyValuePair<string, IEnumerable<string>>> headers) |
| | | 57 | | { |
| | | 58 | | foreach (KeyValuePair<string, IEnumerable<string>> header in headers) |
| | | 59 | | if (!s_reservedHeaders.Contains(header.Key)) |
| | | 60 | | response.Headers.Add(header.Key, string.Join(",", header.Value)); |
| | | 61 | | } |
| | | 62 | | } |
| | | 63 | | |
| | | 64 | | private static HttpRequestMessage GetRequest(HttpListenerRequest request) |
| | 1 | 65 | | { |
| | 1 | 66 | | HttpRequestMessage requestMessage = new |
| | 1 | 67 | | ( |
| | 1 | 68 | | new HttpMethod(request.HttpMethod), |
| | 1 | 69 | | request.Url |
| | 1 | 70 | | ); |
| | | 71 | | |
| | 1 | 72 | | if (request.HasEntityBody) |
| | 1 | 73 | | requestMessage.Content = new StreamContent(request.InputStream); |
| | | 74 | | |
| | 1 | 75 | | foreach (string headerName in request.Headers.AllKeys) |
| | 1 | 76 | | { |
| | 1 | 77 | | HttpHeaders headers = requestMessage.Content is not null && HttpRequestMessage.ContentHeaders.Contains(h |
| | 1 | 78 | | ? requestMessage.Content.Headers |
| | 1 | 79 | | : requestMessage.Headers; |
| | | 80 | | |
| | | 81 | | // Some header (like Content-Type) has its default value. Without this line we'd just append the value l |
| | 1 | 82 | | headers.Remove(headerName); |
| | 1 | 83 | | headers.TryAddWithoutValidation(headerName, request.Headers.GetValues(headerName)); |
| | 1 | 84 | | } |
| | | 85 | | |
| | 1 | 86 | | requestMessage.Properties[OriginalRequestName] = request; |
| | 1 | 87 | | requestMessage.Properties[TraceIdName] = request.RequestTraceIdentifier.ToString("N"); |
| | | 88 | | |
| | 1 | 89 | | return requestMessage; |
| | 1 | 90 | | } |
| | | 91 | | |
| | 1 | 92 | | private HttpListenerRouter(RouterBuilder<HttpListenerRouter, HttpListenerRouterConfig> builder) : base(builder, |
| | | 93 | | |
| | | 94 | | /// <summary> |
| | | 95 | | /// Routes a single <see cref="HttpListener"/> request and writes the produced response. |
| | | 96 | | /// </summary> |
| | | 97 | | /// <param name="context">The listener context that supplies the request and receives the response.</param> |
| | | 98 | | /// <param name="services">The service provider exposed to handlers through <see cref="RequestContext.Services"/ |
| | | 99 | | /// <param name="cancellation">A token that can cancel request processing and response streaming.</param> |
| | | 100 | | /// <returns>A task that completes after the router has finished writing the response.</returns> |
| | | 101 | | /// <exception cref="OperationCanceledException"> |
| | | 102 | | /// Thrown when the caller cancels <paramref name="cancellation"/>. The listener response is aborted before the |
| | | 103 | | /// exception is rethrown. |
| | | 104 | | /// </exception> |
| | | 105 | | /// <remarks> |
| | | 106 | | /// Request and content headers are copied into the intermediate <see cref="HttpRequestMessage"/>. |
| | | 107 | | /// The original <see cref="HttpListenerRequest"/> is stored in |
| | | 108 | | /// <see cref="Router.OriginalRequestName"/> on the generated request message. |
| | | 109 | | /// Response headers are copied back except for reserved <see cref="HttpListenerResponse"/> headers that |
| | | 110 | | /// must be managed by <see cref="HttpListener"/> itself. Cancellation is not translated into an HTTP error |
| | | 111 | | /// response by this adapter. |
| | | 112 | | /// </remarks> |
| | | 113 | | /// <example> |
| | | 114 | | /// <code> |
| | | 115 | | /// HttpListenerRouter router = HttpListenerRouter |
| | | 116 | | /// .CreateBuilder() |
| | | 117 | | /// .AddJsonErrorDetails() |
| | | 118 | | /// .AddDefaultValueParsers() |
| | | 119 | | /// .AddHandler("GET", "/hello/{name:str}", (context, _) => |
| | | 120 | | /// Task.FromResult(HttpResponseMessage.Json(new |
| | | 121 | | /// { |
| | | 122 | | /// message = $"Hello {context.Parameters["name"]}" |
| | | 123 | | /// }))) |
| | | 124 | | /// .CreateRouter(); |
| | | 125 | | /// |
| | | 126 | | /// await router.Route(listenerContext, services, cancellationToken); |
| | | 127 | | /// </code> |
| | | 128 | | /// </example> |
| | | 129 | | public async Task Route(HttpListenerContext context, IServiceProvider services, CancellationToken cancellation = |
| | | 130 | | { |
| | | 131 | | Ensure.NotNull(context); |
| | | 132 | | Ensure.NotNull(services); |
| | | 133 | | |
| | | 134 | | using HttpRequestMessage request = GetRequest(context.Request); |
| | | 135 | | |
| | | 136 | | try |
| | | 137 | | { |
| | | 138 | | using HttpResponseMessage response = await Handle(request, services, cancellation); |
| | | 139 | | |
| | | 140 | | await HandleResponse(response, context.Response, cancellation); |
| | | 141 | | } |
| | | 142 | | catch (OperationCanceledException) |
| | | 143 | | { |
| | | 144 | | context.Response.Abort(); |
| | | 145 | | throw; |
| | | 146 | | } |
| | | 147 | | } |
| | | 148 | | |
| | | 149 | | /// <summary> |
| | | 150 | | /// Creates a strongly typed builder for configuring an <see cref="HttpListenerRouter"/>. |
| | | 151 | | /// </summary> |
| | | 152 | | /// <returns>A builder that can register handlers, value parsers, and router configuration.</returns> |
| | 1 | 153 | | public static RouterBuilder<HttpListenerRouter, HttpListenerRouterConfig> CreateBuilder() => new(static bldr => |
| | | 154 | | } |
| | | 155 | | } |