diff --git a/.nuget/README.md b/.nuget/README.md index 8fca84b..91b76ec 100644 --- a/.nuget/README.md +++ b/.nuget/README.md @@ -1,4 +1,4 @@ -**Sisk** is a **web development framework** that is lightweight, agnostic, easy, simple, and robust. The perfect choice for your next project. +**Sisk** is a **web development framework** that is lightweight, agnostic, easy, simple, and robust. The perfect choice for your next project. - [Discover Sisk](https://www.sisk-framework.org/) - [Documentation](https://docs.sisk-framework.org/) @@ -19,23 +19,19 @@ It can handle multiple requests asynchronously, provides useful tools to manage ```c# using Sisk.Core.Http; -using Sisk.Core.Routing; - -namespace myProgram; class Program { - static void Main(string[] args) + static async Task Main(string[] args) { - var app = HttpServer.CreateBuilder(); + using var app = HttpServer.CreateBuilder(5555).Build(); - app.Router += new Route(RouteMethod.Get, "/", request => + app.Router.MapGet("/", request => { - return new HttpResponse(200) - .WithContent("Hello, world!"); + return new HttpResponse("Hello, world!"); }); - app.Start(); + await app.StartAsync(); // 🚀 app is listening on http://localhost:5555/ } } ``` diff --git a/README.md b/README.md index 818e5c0..1bbb805 100644 --- a/README.md +++ b/README.md @@ -54,20 +54,19 @@ It can handle multiple requests asynchronously, provides useful tools to manage ```c# using Sisk.Core.Http; -using Sisk.Core.Routing; class Program { - static void Main(string[] args) + static async Task Main(string[] args) { - var app = HttpServer.CreateBuilder(5000); + using var app = HttpServer.CreateBuilder(5555).Build(); - app.Router.SetRoute(RouteMethod.Get, "/", request => + app.Router.MapGet("/", request => { return new HttpResponse("Hello, world!"); }); - app.Start(); // 🚀 app is listening on http://localhost:5000/ + await app.StartAsync(); // 🚀 app is listening on http://localhost:5555/ } } ``` diff --git a/src/Http/HttpRequest.cs b/src/Http/HttpRequest.cs index 786b95e..243872b 100644 --- a/src/Http/HttpRequest.cs +++ b/src/Http/HttpRequest.cs @@ -470,6 +470,10 @@ public object SendTo(RouteAction otherCallback) /// /// Closes this HTTP request and their connection with the remote client without sending any response. /// + /// + /// This method is now obsolete and will be removed in next Sisk versions. Please, use instead. + /// + [Obsolete("This method is now obsolete and will be removed in next Sisk versions. Please, use HttpResponse.Refuse() instead.")] public HttpResponse Close() { return new HttpResponse(HttpResponse.HTTPRESPONSE_SERVER_REFUSE); diff --git a/src/Http/HttpResponse.cs b/src/Http/HttpResponse.cs index be8ca98..b4509b4 100644 --- a/src/Http/HttpResponse.cs +++ b/src/Http/HttpResponse.cs @@ -9,7 +9,6 @@ using Sisk.Core.Entity; using Sisk.Core.Routing; -using System.Collections.Specialized; using System.Net; using System.Text; @@ -20,17 +19,30 @@ namespace Sisk.Core.Http /// public class HttpResponse : CookieHelper { - internal const byte HTTPRESPONSE_EMPTY = 2; + internal const byte HTTPRESPONSE_EMPTY = 2; // <- theres no reason for this to exist internal const byte HTTPRESPONSE_SERVER_REFUSE = 4; internal const byte HTTPRESPONSE_SERVER_CLOSE = 6; internal const byte HTTPRESPONSE_CLIENT_CLOSE = 32; - internal const byte HTTPRESPONSE_ERROR = 8; + internal const byte HTTPRESPONSE_UNHANDLED_EXCEPTION = 8; + internal long CalculedLength = -1; /// - /// Creates an new empty with no status code or contents. This will cause to the HTTP server to close the - /// connection between the server and the client and don't deliver any response. + /// Creates an object which closes the connection with the client immediately (ECONNRESET). + /// + /// + public static HttpResponse Refuse() + { + return new HttpResponse(HTTPRESPONSE_SERVER_REFUSE); + } + + /// + /// Creates an object which closes the connection with the client immediately (ECONNRESET). /// + /// + /// This method is obsolete and replaced by . + /// + [Obsolete("This method should be avoided and will be removed in next Sisk versions.")] public static HttpResponse CreateEmptyResponse() { return new HttpResponse(HTTPRESPONSE_EMPTY); diff --git a/src/Http/HttpServer.cs b/src/Http/HttpServer.cs index 20a5ce5..81980f2 100644 --- a/src/Http/HttpServer.cs +++ b/src/Http/HttpServer.cs @@ -92,15 +92,44 @@ public static HttpServerHostContextBuilder CreateBuilder() } /// - /// Outputs an non-listening HTTP server with configuration, listening host, and router. + /// Gets an listening and running HTTP server in an random port. + /// + public static HttpServer CreateListener() => CreateListener(ListeningPort.GetRandomPort().Port, out _, out _, out _); + + /// + /// Gets an listening and running HTTP server in the specified port. + /// + /// The listening port of the HTTP server. + public static HttpServer CreateListener(ushort port) => CreateListener(port, out _, out _, out _); + + /// + /// Gets an listening and running HTTP server in the specified port. + /// + /// The insecure port where the HTTP server will listen. + /// The object issued from this method. + /// The object issued from this method. + /// The object issued from this method. + public static HttpServer CreateListener( + ushort insecureHttpPort, + out HttpServerConfiguration configuration, + out ListeningHost host, + out Router router + ) + { + var s = Emit(insecureHttpPort, out configuration, out host, out router); + s.Start(); + return s; + } + + /// + /// Gets an non-listening HTTP server with configuration, listening host, and router. /// - /// This method is not appropriate to running production servers. /// The insecure port where the HTTP server will listen. /// The object issued from this method. /// The object issued from this method. /// The object issued from this method. public static HttpServer Emit( - in ushort insecureHttpPort, + ushort insecureHttpPort, out HttpServerConfiguration configuration, out ListeningHost host, out Router router diff --git a/src/Http/HttpServer__Core.cs b/src/Http/HttpServer__Core.cs index 705eb6f..ab4633a 100644 --- a/src/Http/HttpServer__Core.cs +++ b/src/Http/HttpServer__Core.cs @@ -13,6 +13,7 @@ using System.Diagnostics; using System.Net; using System.Runtime.CompilerServices; +using System.Text; namespace Sisk.Core.Http; @@ -50,29 +51,44 @@ internal static string HumanReadableSize(in float size) internal static void SetCorsHeaders(HttpServerFlags serverFlags, HttpListenerRequest baseRequest, CrossOriginResourceSharingHeaders cors, HttpListenerResponse baseResponse) { - if (!serverFlags.SendCorsHeaders) return; - if (cors.AllowHeaders.Length > 0) baseResponse.Headers.Set("Access-Control-Allow-Headers", string.Join(", ", cors.AllowHeaders)); - if (cors.AllowMethods.Length > 0) baseResponse.Headers.Set("Access-Control-Allow-Methods", string.Join(", ", cors.AllowMethods)); - if (cors.AllowOrigin != null) baseResponse.Headers.Set("Access-Control-Allow-Origin", cors.AllowOrigin); + if (!serverFlags.SendCorsHeaders) + return; + + if (cors.AllowHeaders.Length > 0) + baseResponse.Headers.Set(HttpKnownHeaderNames.AccessControlAllowHeaders, string.Join(", ", cors.AllowHeaders)); + + if (cors.AllowMethods.Length > 0) + baseResponse.Headers.Set(HttpKnownHeaderNames.AccessControlAllowMethods, string.Join(", ", cors.AllowMethods)); + + if (cors.AllowOrigin is not null) + baseResponse.Headers.Set(HttpKnownHeaderNames.AccessControlAllowOrigin, cors.AllowOrigin); + if (cors.AllowOrigins?.Length > 0) { - string? origin = baseRequest.Headers["Origin"]; - if (origin != null) + string? origin = baseRequest.Headers[HttpKnownHeaderNames.Origin]; + + if (origin is not null) { for (int i = 0; i < cors.AllowOrigins.Length; i++) { string definedOrigin = cors.AllowOrigins[i]; if (string.Compare(definedOrigin, origin, true) == 0) { - baseResponse.Headers.Set("Access-Control-Allow-Origin", origin); + baseResponse.Headers.Set(HttpKnownHeaderNames.AccessControlAllowOrigin, origin); break; } } } } - if (cors.AllowCredentials != null) baseResponse.Headers.Set("Access-Control-Allow-Credentials", cors.AllowCredentials.ToString()!.ToLower()); - if (cors.ExposeHeaders.Length > 0) baseResponse.Headers.Set("Access-Control-Expose-Headers", string.Join(", ", cors.ExposeHeaders)); - if (cors.MaxAge.TotalSeconds > 0) baseResponse.Headers.Set("Access-Control-Max-Age", cors.MaxAge.TotalSeconds.ToString()); + + if (cors.AllowCredentials == true) + baseResponse.Headers.Set(HttpKnownHeaderNames.AccessControlAllowCredentials, "true"); + + if (cors.ExposeHeaders.Length > 0) + baseResponse.Headers.Set(HttpKnownHeaderNames.AccessControlExposeHeaders, string.Join(", ", cors.ExposeHeaders)); + + if (cors.MaxAge.TotalSeconds > 0) + baseResponse.Headers.Set(HttpKnownHeaderNames.AccessControlMaxAge, cors.MaxAge.TotalSeconds.ToString()); } private void UnbindRouters() @@ -123,8 +139,8 @@ private void ProcessRequest(HttpListenerContext context) long outcomingSize = 0; bool closeStream = true; bool useCors = false; - bool hasAccessLogging = ServerConfiguration.AccessLogsStream != null; - bool hasErrorLogging = ServerConfiguration.ErrorsLogsStream != null; + bool hasAccessLogging = ServerConfiguration.AccessLogsStream is not null; + bool hasErrorLogging = ServerConfiguration.ErrorsLogsStream is not null; IPAddress otherParty = baseRequest.RemoteEndPoint.Address; Uri? connectingUri = baseRequest.Url; Router.RouterExecutionResult? routerResult = null; @@ -134,7 +150,7 @@ private void ProcessRequest(HttpListenerContext context) string _debugState = "begin"; #pragma warning restore CS0219 - if (ServerConfiguration.DefaultCultureInfo != null) + if (ServerConfiguration.DefaultCultureInfo is not null) { Thread.CurrentThread.CurrentCulture = ServerConfiguration.DefaultCultureInfo; Thread.CurrentThread.CurrentUICulture = ServerConfiguration.DefaultCultureInfo; @@ -277,11 +293,11 @@ private void ProcessRequest(HttpListenerContext context) executionResult.Status = HttpServerExecutionStatus.NoResponse; goto finishSending; } - else if (response.internalStatus == HttpResponse.HTTPRESPONSE_ERROR) + else if (response.internalStatus == HttpResponse.HTTPRESPONSE_UNHANDLED_EXCEPTION) { executionResult.Status = HttpServerExecutionStatus.UncaughtExceptionThrown; baseResponse.StatusCode = 500; - if (routerResult.Exception != null) + if (routerResult.Exception is not null) throw routerResult.Exception; goto finishSending; } @@ -431,6 +447,7 @@ response.Content is StreamContent || executionResult.Status = HttpServerExecutionStatus.ConnectionClosed; executionResult.ServerException = netException; hasErrorLogging = false; + hasAccessLogging = false; } catch (HttpRequestException requestException) { @@ -472,7 +489,7 @@ response.Content is StreamContent || handler.HttpRequestClose(executionResult); - if (executionResult.ServerException != null) + if (executionResult.ServerException is not null) handler.Exception(executionResult.ServerException); LogOutput logMode; @@ -490,9 +507,26 @@ response.Content is StreamContent || bool canAccessLog = logMode.HasFlag(LogOutput.AccessLog) && hasAccessLogging; bool canErrorLog = logMode.HasFlag(LogOutput.ErrorLog) && hasErrorLogging; - if (executionResult.ServerException != null && canErrorLog) + if (executionResult.ServerException is { } ex && canErrorLog) { - ServerConfiguration.ErrorsLogsStream?.WriteException(executionResult.ServerException); + StringBuilder errLineBuilder = new StringBuilder(); + errLineBuilder.Append("["); + errLineBuilder.Append(DateTime.Now.ToString("G")); + errLineBuilder.Append("] "); + errLineBuilder.Append(baseRequest.HttpMethod); + errLineBuilder.Append(" "); + errLineBuilder.Append(baseRequest.RawUrl); + errLineBuilder.AppendLine(":"); + errLineBuilder.AppendLine(ex.ToString()); + if (ex.InnerException is { } iex) + { + errLineBuilder.AppendLine("[inner exception]"); + errLineBuilder.AppendLine(iex.ToString()); + } + + errLineBuilder.AppendLine(); + + ServerConfiguration.ErrorsLogsStream?.WriteLine(errLineBuilder); } if (canAccessLog) diff --git a/src/Internal/SR.cs b/src/Internal/SR.cs index e3b6b27..682e495 100644 --- a/src/Internal/SR.cs +++ b/src/Internal/SR.cs @@ -68,6 +68,8 @@ static partial class SR public const string Router_NoRouteActionDefined = "No route action was defined to the route {0}."; public const string Router_ReadOnlyException = "It's not possible to modify the routes or handlers for this router, as it is read-only."; + public const string RequestHandler_ActivationException = "Couldn't activate an instance of the IRequestHandler {0} with the {1} arguments."; + public const string Route_Action_ValueTypeSet = "Defining actions which their return type is an value type is not supported. Encapsulate it with ValueResult."; public const string Route_Action_AsyncMissingGenericType = "Async route {0} action must return an object in addition to Task."; diff --git a/src/Routing/RequestHandledAttribute.cs b/src/Routing/RequestHandledAttribute.cs index ac3c9ef..4f475ce 100644 --- a/src/Routing/RequestHandledAttribute.cs +++ b/src/Routing/RequestHandledAttribute.cs @@ -45,7 +45,7 @@ public RequestHandlerAttribute(params object?[] constructorArguments) : base(typ /// /// Specifies that the method, when used on this attribute, will instantiate the type and call the with given parameters. /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)] public class RequestHandlerAttribute : Attribute { /// @@ -83,7 +83,17 @@ public RequestHandlerAttribute([DynamicallyAccessedMembers( )] Type handledBy) { RequestHandlerType = handledBy; - ConstructorArguments = new object?[] { }; + ConstructorArguments = Array.Empty(); + } + + internal IRequestHandler Activate() + { + IRequestHandler? rhandler = Activator.CreateInstance(RequestHandlerType, ConstructorArguments) as IRequestHandler; + if (rhandler is null) + { + throw new ArgumentException(SR.Format(SR.RequestHandler_ActivationException, RequestHandlerType.FullName, ConstructorArguments.Length)); + } + return rhandler; } } } diff --git a/src/Routing/Route.cs b/src/Routing/Route.cs index 6ddbfda..6bbfba7 100644 --- a/src/Routing/Route.cs +++ b/src/Routing/Route.cs @@ -123,12 +123,12 @@ public RouteAction? Action /// /// Gets or sets the request handlers instances to run before the route's Action. /// - public IRequestHandler[]? RequestHandlers { get; set; } + public IRequestHandler[] RequestHandlers { get; set; } = Array.Empty(); /// /// Gets or sets the global request handlers instances that will not run on this route. /// - public IRequestHandler[]? BypassGlobalRequestHandlers { get; set; } + public IRequestHandler[] BypassGlobalRequestHandlers { get; set; } = Array.Empty(); /// /// Creates an new instance with given parameters. @@ -157,7 +157,7 @@ public Route(RouteMethod method, string path, string? name, RouteAction action, this.path = path; Name = name; Action = action; - RequestHandlers = beforeCallback; + RequestHandlers = beforeCallback ?? Array.Empty(); } /// diff --git a/src/Routing/Router.cs b/src/Routing/Router.cs index 5f44ffc..90fdd48 100644 --- a/src/Routing/Router.cs +++ b/src/Routing/Router.cs @@ -15,6 +15,7 @@ record struct RouteDictItem(System.Type type, Delegate lambda); + namespace Sisk.Core.Routing { /// diff --git a/src/Routing/Router__CoreInvoker.cs b/src/Routing/Router__CoreInvoker.cs index d88cae9..5721822 100644 --- a/src/Routing/Router__CoreInvoker.cs +++ b/src/Routing/Router__CoreInvoker.cs @@ -56,12 +56,11 @@ private Internal.HttpStringInternals.PathMatchResult TestRouteMatchUsingRegex(Ro } } - internal bool InvokeRequestHandlerGroup(in RequestHandlerExecutionMode mode, IRequestHandler[] baseLists, IRequestHandler[]? bypassList, HttpRequest request, HttpContext context, out HttpResponse? result, out Exception? exception) + internal bool InvokeRequestHandlerGroup(in RequestHandlerExecutionMode mode, Span baseLists, Span bypassList, HttpRequest request, HttpContext context, out HttpResponse? result, out Exception? exception) { - ref IRequestHandler pointer = ref MemoryMarshal.GetArrayDataReference(baseLists); for (int i = 0; i < baseLists.Length; i++) { - var rh = Unsafe.Add(ref pointer, i); + var rh = baseLists[i]; if (rh.ExecutionMode == mode) { HttpResponse? response = InvokeHandler(rh, request, context, bypassList, out exception); @@ -77,12 +76,15 @@ internal bool InvokeRequestHandlerGroup(in RequestHandlerExecutionMode mode, IRe return false; } - internal HttpResponse? InvokeHandler(IRequestHandler handler, HttpRequest request, HttpContext context, IRequestHandler[]? bypass, out Exception? exception) + internal HttpResponse? InvokeHandler(IRequestHandler handler, HttpRequest request, HttpContext context, Span bypass, out Exception? exception) { - if (bypass is not null && bypass.Contains(handler)) + for (int i = 0; i < bypass.Length; i++) { - exception = null; - return null; + if (ReferenceEquals(handler, bypass[i])) + { + exception = null; + return null; + } } HttpResponse? result = null; @@ -268,7 +270,7 @@ internal RouterExecutionResult Execute(HttpContext context) } catch (Exception ex) { - if (!parentServer!.ServerConfiguration.ThrowExceptions && (ex is not HttpListenerException)) + if (parentServer!.ServerConfiguration.ThrowExceptions == false && (ex is not HttpListenerException)) { if (CallbackErrorHandler is not null) { @@ -276,7 +278,7 @@ internal RouterExecutionResult Execute(HttpContext context) } else { - result = new HttpResponse(HttpResponse.HTTPRESPONSE_ERROR); + result = new HttpResponse(HttpResponse.HTTPRESPONSE_UNHANDLED_EXCEPTION); return new RouterExecutionResult(result, matchedRoute, matchResult, ex); } } diff --git a/src/Routing/Router__CoreSetters.cs b/src/Routing/Router__CoreSetters.cs index 8e4b87c..01775fb 100644 --- a/src/Routing/Router__CoreSetters.cs +++ b/src/Routing/Router__CoreSetters.cs @@ -291,35 +291,46 @@ public void SetObject([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes private void SetInternal(MethodInfo[] methods, Type callerType, object? instance) { RouterModule? rmodule = instance as RouterModule; - string? prefix; - if (rmodule?.Prefix is null) - { - RoutePrefixAttribute? rPrefix = callerType.GetCustomAttribute(); - prefix = rPrefix?.Prefix; - } - else + // get caller prefix from RoutePrefix first, or router module + object[] callerTypeLevelHandlers = callerType.GetCustomAttributes(true); + List callerAttrReqHandlers = new List(callerTypeLevelHandlers.Length); + string? prefix = rmodule?.Prefix; + + // search for an RoutePrefix handler + for (int i = 0; i < callerTypeLevelHandlers.Length; i++) { - prefix = rmodule.Prefix; + object attr = callerTypeLevelHandlers[i]; + + if (attr is RoutePrefixAttribute rprefix) + { + prefix = rprefix.Prefix; + } + else if (attr is RequestHandlerAttribute rhattr) + { + callerAttrReqHandlers.Add(rhattr.Activate()); + } } for (int imethod = 0; imethod < methods.Length; imethod++) { - MethodInfo? method = methods[imethod]; + MethodInfo method = methods[imethod]; RouteAttribute? routeAttribute = null; object[] methodAttributes = method.GetCustomAttributes(true); List methodAttrReqHandlers = new List(methodAttributes.Length); + methodAttrReqHandlers.AddRange(callerAttrReqHandlers); + if (rmodule is not null) + methodAttrReqHandlers.AddRange(rmodule.RequestHandlers); + for (int imethodAttribute = 0; imethodAttribute < methodAttributes.Length; imethodAttribute++) { object attrInstance = methodAttributes[imethodAttribute]; if (attrInstance is RequestHandlerAttribute reqHandlerAttr) { - IRequestHandler? rhandler = (IRequestHandler?)Activator.CreateInstance(reqHandlerAttr.RequestHandlerType, reqHandlerAttr.ConstructorArguments); - if (rhandler is not null) - methodAttrReqHandlers.Add(rhandler); + methodAttrReqHandlers.Add(reqHandlerAttr.Activate()); } else if (attrInstance is RouteAttribute routeAttributeItem) { @@ -329,14 +340,6 @@ private void SetInternal(MethodInfo[] methods, Type callerType, object? instance if (routeAttribute is not null) { - if (rmodule?.RequestHandlers.Count > 0) - { - for (int imodReqHandler = 0; imodReqHandler < rmodule.RequestHandlers.Count; imodReqHandler++) - { - IRequestHandler handler = rmodule.RequestHandlers[imodReqHandler]; - methodAttrReqHandlers.Add(handler); - } - } try { RouteAction r;