From ceaa7c16850e38b3784ccac8c919c47e4f1429f4 Mon Sep 17 00:00:00 2001 From: haraldwolff Date: Fri, 26 Apr 2024 14:49:44 +0200 Subject: [PATCH] Start of change to Json.NET, Rework HttpEndpointController, beta HttpClient Implementation --- ln.http/HttpEndpointController.cs | 161 ++++++++++++++++----------- ln.http/HttpRequest.cs | 26 +++++ ln.http/HttpResponse.cs | 7 ++ ln.http/HttpRouter.cs | 2 +- ln.http/JsonBodyAttribute.cs | 14 +++ ln.http/Listener.cs | 32 ++++-- ln.http/QueryAttribute.cs | 11 ++ ln.http/RequiresAttribute.cs | 13 +++ ln.http/RouteAttribute.cs | 12 ++ ln.http/TlsListener.cs | 26 +++-- ln.http/client/HttpClient.cs | 32 +++++- ln.http/client/HttpClientResponse.cs | 17 +++ ln.http/ln.http.csproj | 6 +- 13 files changed, 271 insertions(+), 88 deletions(-) create mode 100644 ln.http/JsonBodyAttribute.cs create mode 100644 ln.http/QueryAttribute.cs create mode 100644 ln.http/RequiresAttribute.cs create mode 100644 ln.http/RouteAttribute.cs diff --git a/ln.http/HttpEndpointController.cs b/ln.http/HttpEndpointController.cs index 1177e6e..3c460ff 100644 --- a/ln.http/HttpEndpointController.cs +++ b/ln.http/HttpEndpointController.cs @@ -7,6 +7,8 @@ using System.Text.RegularExpressions; using ln.json; using ln.json.mapping; using ln.type; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace ln.http { @@ -61,6 +63,8 @@ namespace ln.http private HttpArgumentSourceAttribute[] _argumentSourceAttributes; private Type _returnType; + private RequiresAttribute[] _requiresAttributes; + public MappedEndpoint(HttpEndpointController endpointController, MapAttribute mapAttribute, MethodInfo methodInfo) { EndpointController = endpointController; @@ -71,10 +75,89 @@ namespace ln.http _returnType = MethodInfo.ReturnType; _argumentSourceAttributes = _parameterInfos .Select((pi) => pi.GetCustomAttribute()).ToArray(); + _requiresAttributes = methodInfo.GetCustomAttributes().ToArray(); } - public abstract bool Route(HttpRequestContext httpContext, string routePath); + public bool Route(HttpRequestContext httpContext, string routePath) + { + if (_requiresAttributes.Length > 0){ + if (httpContext.AuthenticatedPrincipal is HttpPrincipal) + { + foreach (var requiresAttribute in _requiresAttributes) + { + if (requiresAttribute.Roles.All((role) => + httpContext.AuthenticatedPrincipal.Roles.Contains(role))) + { + return Route2(httpContext, routePath); + } + } + } + httpContext.Response = HttpResponse.Unauthorized(); + return true; + } + return Route2(httpContext, routePath); + } + public abstract bool Route2(HttpRequestContext httpContext, string routePath); + + bool TryFindParameterValue(HttpRequestContext context, ParameterInfo parameterInfo, out object value) + { + if (parameterInfo.GetCustomAttribute() is JsonBodyAttribute jsonBodyAttribute) + { + if (jsonBodyAttribute.Property is null) + { + value = context.Request.Json(parameterInfo.ParameterType); + return true; + } + else if (context.Request.Json().TryGetValue(jsonBodyAttribute.Property, out JToken property)) + { + value = property.ToObject(parameterInfo.ParameterType); + return true; + } + } + + if ((parameterInfo.GetCustomAttribute() is RouteAttribute routeAttribute) && + context.TryGetParameter(routeAttribute.Name ?? parameterInfo.Name, out string pvalue)) + { + value = pvalue; + return true; + } + + if ((parameterInfo.GetCustomAttribute() is QueryAttribute queryAttribute) && + context.Request.Query.TryGetValue(queryAttribute.Name ?? parameterInfo.Name, out string svalue)) + { + value = svalue; + return true; + } + + if (parameterInfo.ParameterType.Equals(typeof(Stream))) + { + value = context.Request.ContentStream; + return true; + } + + if (parameterInfo.ParameterType.Equals(typeof(HttpRequestContext))) + { + value = context; + return true; + } + + if (parameterInfo.ParameterType.Equals(typeof(HttpRequest))) + { + value = context.Request; + return true; + } + + if (parameterInfo.ParameterType.Equals(typeof(HttpPrincipal))) + { + value = context.AuthenticatedPrincipal; + return true; + } + + value = null; + return false; + } + bool TryApplyParameters(HttpRequestContext requestContext, out object[] parameters) { parameters = new object[_parameterInfos.Length]; @@ -83,78 +166,26 @@ namespace ln.http { ParameterInfo parameterInfo = _parameterInfos[n]; - if (parameterInfo.ParameterType.Equals(typeof(HttpRequestContext))) - parameters[n] = requestContext; - else if (parameterInfo.ParameterType.Equals(typeof(HttpRequest))) - parameters[n] = requestContext.Request; - else if (parameterInfo.ParameterType.Equals(typeof(HttpPrincipal))) - parameters[n] = requestContext.AuthenticatedPrincipal; - else if (TryFindArgumentByName( - requestContext, - _argumentSourceAttributes[n]?.ArgumentSource ?? HttpArgumentSource.AUTO, - _argumentSourceAttributes[n]?.ArgumentName ?? _parameterInfos[n].Name, - out string parameterValue - )) + if (!TryFindParameterValue(requestContext, parameterInfo, out parameters[n])) + parameters[n] = parameterInfo.DefaultValue; + + if ((parameters[n] != null) && + (!parameterInfo.ParameterType.IsAssignableFrom(parameters[n].GetType()))) { - if (!Cast.To(parameterValue, _parameterInfos[n].ParameterType, out parameters[n])) + if (!Cast.To(parameters[n], _parameterInfos[n].ParameterType, out parameters[n])) { parameters[n] = TypeDescriptor .GetConverter(parameterInfo.ParameterType) - .ConvertFromInvariantString(parameterValue); + .ConvertFrom(parameters[n]); } } - else if ((_argumentSourceAttributes[n].ArgumentSource == HttpArgumentSource.CONTENT) && (requestContext.Request.Headers.Contains("content-length"))) - { - if (_parameterInfos[n].ParameterType.Equals(typeof(Stream))) - { - parameters[n] = requestContext.Request.ContentStream; - } else if (_parameterInfos[n].ParameterType.Equals(typeof(TextReader))) - { - parameters[n] = new StreamReader(requestContext.Request.ContentStream); - } else if (_parameterInfos[n].ParameterType.Equals(typeof(string))) - { - using (var r = new StreamReader(requestContext.Request.ContentStream)) - parameters[n] = r.ReadToEnd(); - } else if (requestContext.Request.Headers.TryGetValue("Content-Type", out string contentType) && - contentType.Equals("application/json")) - { - using (TextReader reader = new StreamReader(requestContext.Request.ContentStream)) - { - JSONValue jsonValue = JSONParser.Parse(reader); - - if (typeof(JSONValue).IsAssignableFrom(_parameterInfos[n].ParameterType)) - parameters[n] = jsonValue; - else - parameters[n] = JSONMapper.DefaultMapper.FromJson(jsonValue, - _parameterInfos[n].ParameterType); - } - } - } - else if (_parameterInfos[n].HasDefaultValue) - { - parameters[n] = _parameterInfos[n].DefaultValue; - } - else + + if ((parameters[n] == null) && (parameterInfo.ParameterType.IsValueType)) return false; + } - return true; } - - bool TryFindArgumentByName(HttpRequestContext httpContext, HttpArgumentSource argumentSource, string parameterName, out string parameterValue) - { - if (((argumentSource & HttpArgumentSource.PARAMETER) == HttpArgumentSource.PARAMETER) && httpContext.TryGetParameter(parameterName, out parameterValue)) - return true; - else if (((argumentSource & HttpArgumentSource.HEADER) == HttpArgumentSource.HEADER) && - (httpContext.Request.Headers.TryGetValue(parameterName, out parameterValue))) - return true; - else if (((argumentSource & HttpArgumentSource.QUERY) == HttpArgumentSource.QUERY) && - (httpContext.Request.Query.TryGetValue(parameterName, out parameterValue))) - return true; - - parameterValue = null; - return false; - } private object InvokeMethod(HttpRequestContext httpContext) { @@ -181,7 +212,7 @@ namespace ln.http { } - public override bool Route(HttpRequestContext httpContext, string routePath) + public override bool Route2(HttpRequestContext httpContext, string routePath) { object returnedValue = InvokeMethod(httpContext); @@ -204,7 +235,7 @@ namespace ln.http { } - public override bool Route(HttpRequestContext httpContext, string routePath) + public override bool Route2(HttpRequestContext httpContext, string routePath) { object returnedValue = InvokeMethod(httpContext); if (returnedValue is HttpResponse httpResponse) diff --git a/ln.http/HttpRequest.cs b/ln.http/HttpRequest.cs index 590967e..75344c7 100644 --- a/ln.http/HttpRequest.cs +++ b/ln.http/HttpRequest.cs @@ -2,9 +2,13 @@ using System.IO; using System.Collections.Generic; using System.Linq; +using System.Text; using System.Text.RegularExpressions; using ln.http.content; using ln.json; +using ln.type; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace ln.http { @@ -154,6 +158,28 @@ namespace ln.http } } + private string? _bodyText; + public string Text + { + get + { + if (_bodyText is null) + _bodyText = Encoding.UTF8.GetString(ContentStream.ReadToEnd()); + return _bodyText; + } + } + + private JObject? _jobject; + public JObject Json() + { + if (_jobject is null) + _jobject = JObject.Load(new JsonTextReader(new StringReader(Text))); + return _jobject; + } + + public T Json() => JsonConvert.DeserializeObject(Text); + public object Json(Type nativeType) => JsonConvert.DeserializeObject(Text, nativeType); + public void Dispose() => ContentStream?.Dispose(); } diff --git a/ln.http/HttpResponse.cs b/ln.http/HttpResponse.cs index 0e5cb89..2ba664a 100644 --- a/ln.http/HttpResponse.cs +++ b/ln.http/HttpResponse.cs @@ -2,6 +2,7 @@ using System.IO; using System.Collections.Generic; using ln.json; +using Newtonsoft.Json; namespace ln.http { @@ -133,6 +134,12 @@ namespace ln.http return this; } + public HttpResponse Json(T content) + { + HttpContent = new StringContent(JsonConvert.SerializeObject(content), "application/json"); + return this; + } + public void Dispose() { HttpContent?.Dispose(); diff --git a/ln.http/HttpRouter.cs b/ln.http/HttpRouter.cs index 6246367..7502aaa 100644 --- a/ln.http/HttpRouter.cs +++ b/ln.http/HttpRouter.cs @@ -46,7 +46,7 @@ namespace ln.http => Map(new HttpRoute(httpMethod, route, routerDelegate)); - public bool RouteRequest(HttpRequestContext httpRequestContext, string routePath) + public virtual bool RouteRequest(HttpRequestContext httpRequestContext, string routePath) { HttpFilters?.Invoke(httpRequestContext); diff --git a/ln.http/JsonBodyAttribute.cs b/ln.http/JsonBodyAttribute.cs new file mode 100644 index 0000000..9099419 --- /dev/null +++ b/ln.http/JsonBodyAttribute.cs @@ -0,0 +1,14 @@ +using System; + +namespace ln.http; + +[AttributeUsage(AttributeTargets.Parameter)] +public class JsonBodyAttribute : Attribute +{ + public string Property { get; set; } + public JsonBodyAttribute(){} + public JsonBodyAttribute(string property) + { + Property = property; + } +} \ No newline at end of file diff --git a/ln.http/Listener.cs b/ln.http/Listener.cs index 0bd214a..e5d2a57 100644 --- a/ln.http/Listener.cs +++ b/ln.http/Listener.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Net; using System.Net.Sockets; +using System.Threading; using ln.http.exceptions; using ln.threading; @@ -47,25 +48,36 @@ public class Listener : IDisposable Socket.Listen(); Socket.ReceiveTimeout = 10000; - DynamicThreadPool.DefaultPool.Enqueue(ConnectionHandler); + DynamicThreadPool.DefaultPool.Enqueue(ListenerLoop); + } + + private void ListenerLoop() + { + while (Socket.IsBound) + { + Socket clientSocket = Socket.Accept(); + ThreadPool.QueueUserWorkItem((state => ConnectionHandler(clientSocket))); +// DynamicThreadPool.DefaultPool.Enqueue(()=>ConnectionHandler(clientSocket)); + } } - private void ConnectionHandler() + private void ConnectionHandler(Socket clientSocket) { - Socket clientSocket = Socket.Accept(); - if (Socket.IsBound) - DynamicThreadPool.DefaultPool.Enqueue(ConnectionHandler); - - clientSocket.ReceiveTimeout = 10000; - +// clientSocket.ReceiveTimeout = 10000; + try { - using (NetworkStream networkStream = new NetworkStream(clientSocket)) + using (NetworkStream networkStream = new NetworkStream(clientSocket)) Accepted(clientSocket, networkStream); } + catch (SocketException se) + { + } + catch (IOException e) + { + } finally { - clientSocket.Close(); clientSocket.Dispose(); } } diff --git a/ln.http/QueryAttribute.cs b/ln.http/QueryAttribute.cs new file mode 100644 index 0000000..428c40b --- /dev/null +++ b/ln.http/QueryAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace ln.http; + +[AttributeUsage(AttributeTargets.Parameter)] +public class QueryAttribute : Attribute +{ + public string Name { get; set; } + + public QueryAttribute(){} +} \ No newline at end of file diff --git a/ln.http/RequiresAttribute.cs b/ln.http/RequiresAttribute.cs new file mode 100644 index 0000000..363dcbf --- /dev/null +++ b/ln.http/RequiresAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace ln.http; + +public class RequiresAttribute : Attribute +{ + public string[] Roles { get; set; } + + public RequiresAttribute(params string[] roles) + { + Roles = roles; + } +} \ No newline at end of file diff --git a/ln.http/RouteAttribute.cs b/ln.http/RouteAttribute.cs new file mode 100644 index 0000000..881a3b9 --- /dev/null +++ b/ln.http/RouteAttribute.cs @@ -0,0 +1,12 @@ +using System; + +namespace ln.http; + +[AttributeUsage(AttributeTargets.Parameter)] +public class RouteAttribute : Attribute +{ + public string Name { get; set; } + + public RouteAttribute(){} +} + diff --git a/ln.http/TlsListener.cs b/ln.http/TlsListener.cs index 87f8aa2..9d8852d 100644 --- a/ln.http/TlsListener.cs +++ b/ln.http/TlsListener.cs @@ -5,7 +5,6 @@ using System.Net.Security; using System.Net.Sockets; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; -using System.Threading; namespace ln.http; @@ -27,20 +26,33 @@ public class TlsListener : Listener { _certificateStore = certificateStore; DefaultCertificate = defaultCertificate; - + if (DefaultCertificate is null) - DefaultCertificate = buildSelfSignedServerCertificate(); + { + if (_certificateStore.TryGetCertificate("localhost", out defaultCertificate)) + DefaultCertificate = defaultCertificate; + } } protected override void Accepted(Socket connectedSocket, Stream connectionStream) { SslStream sslStream = new SslStream(connectionStream, false, null, CertificateSelectionCallback); - sslStream.AuthenticateAsServer(DefaultCertificate); + try + { + sslStream.AuthenticateAsServer(DefaultCertificate); - // ToDo: Check for correct ALPN protocol identifier - - base.Accepted(connectedSocket, sslStream); + // ToDo: Check for correct ALPN protocol identifier + + base.Accepted(connectedSocket, sslStream); + } + catch (Exception e) + { + } + finally + { + sslStream?.Dispose(); + } } diff --git a/ln.http/client/HttpClient.cs b/ln.http/client/HttpClient.cs index bc81a04..1e9b768 100644 --- a/ln.http/client/HttpClient.cs +++ b/ln.http/client/HttpClient.cs @@ -1,17 +1,16 @@ using System; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; using System.IO; using System.Net; using System.Net.Security; using System.Net.Sockets; -using System.Runtime.InteropServices.JavaScript; using System.Text; using ln.http.content; -using ln.http.exceptions; using ln.logging; using ln.patterns; using ln.type; +using Newtonsoft.Json; + namespace ln.http.client { public class HttpClient @@ -184,6 +183,33 @@ namespace ln.http.client public Optional Post(string uri, IEnumerable
headers, Stream body) => Request(HttpMethod.POST, new Uri(uri), headers, body); + public Optional Post(string uri, Newtonsoft.Json.JsonToken json) => Post(uri, null, json); + public Optional Post(string uri, IEnumerable
headers, + Newtonsoft.Json.JsonToken json) + { + using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(json.ToString()))) + return Request(HttpMethod.POST, new Uri(uri), headers, stream); + } + + public Optional Post(string uri, IEnumerable
headers, string body) + { + using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(body))) + return Request(HttpMethod.POST, new Uri(uri), headers, stream); + } + + + public Optional Post(string uri, IEnumerable
headers, T body) => + Post(new Uri(uri), headers, body); + public Optional Post(Uri uri, IEnumerable
headers, T body) + { + HeaderContainer headerContainer = new HeaderContainer(headers); + if (!headerContainer.Contains("Content-Type")) + headerContainer.Add("Content-Type", "application/json"); + + using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(body)))) + return Request(HttpMethod.POST, uri, headerContainer, stream); + } + public static bool ReadResponseLine(Stream stream, out HttpVersion httpVersion, out HttpStatusCode statusCode, out string reason) { diff --git a/ln.http/client/HttpClientResponse.cs b/ln.http/client/HttpClientResponse.cs index 01d8e58..7b5aff7 100644 --- a/ln.http/client/HttpClientResponse.cs +++ b/ln.http/client/HttpClientResponse.cs @@ -1,6 +1,9 @@ using System; using System.IO; +using System.Text; using ln.http.content; +using ln.type; +using Newtonsoft.Json; namespace ln.http.client { @@ -30,6 +33,20 @@ namespace ln.http.client _contentStream = contentStream; } + private string? _bodyText; + public string Text + { + get + { + if (_bodyText is null) + _bodyText = Encoding.UTF8.GetString(ContentStream.ReadToEnd()); + return _bodyText; + } + } + + public T Json() => JsonConvert.DeserializeObject(Text); + + public override string ToString() => String.Format("{0} {1}", (int)StatusCode, StatusCode); } } diff --git a/ln.http/ln.http.csproj b/ln.http/ln.http.csproj index 654b9b4..4945bc3 100644 --- a/ln.http/ln.http.csproj +++ b/ln.http/ln.http.csproj @@ -9,7 +9,7 @@ (c) 2020 Harald Wolff-Thobaben http server default - 0.9.9 + 0.9.10-preview0 0.6.2.0 net7.0 @@ -20,7 +20,9 @@ - + + +