diff --git a/src/Entity/MultipartObject.cs b/src/Entity/MultipartObject.cs index 1c93b55..b3c422b 100644 --- a/src/Entity/MultipartObject.cs +++ b/src/Entity/MultipartObject.cs @@ -303,7 +303,7 @@ bool Equals(byte[] source, byte[] separator, int index) } } - if (fieldName == null) + if (fieldName is null) { throw new InvalidOperationException(string.Format(SR.MultipartObject_EmptyFieldName, i)); } diff --git a/src/Entity/StringValueCollection.cs b/src/Entity/StringValueCollection.cs index 7247623..66bab38 100644 --- a/src/Entity/StringValueCollection.cs +++ b/src/Entity/StringValueCollection.cs @@ -39,7 +39,7 @@ internal static StringValueCollection FromNameValueCollection(string paramName, { value = col[i]; if (value is null) continue; - vcol.items.Add(value, ""); + vcol.items.Add(value, string.Empty); } else { diff --git a/src/Http/Hosting/InitializationParameterCollection.cs b/src/Http/Hosting/InitializationParameterCollection.cs index ec6000d..fa2bfb6 100644 --- a/src/Http/Hosting/InitializationParameterCollection.cs +++ b/src/Http/Hosting/InitializationParameterCollection.cs @@ -45,7 +45,7 @@ public sealed class InitializationParameterCollection : IDictionary /// Gets the X-Powered-By Sisk header value. /// - public static string PoweredBy { get; private set; } = ""; + public static string PoweredBy { get; private set; } = string.Empty; /// /// Gets the current Sisk version. diff --git a/src/Http/ListeningHost.cs b/src/Http/ListeningHost.cs index 2d58af6..0e0968a 100644 --- a/src/Http/ListeningHost.cs +++ b/src/Http/ListeningHost.cs @@ -28,7 +28,7 @@ public override bool Equals(object? obj) { if (obj is ListeningHost other) { - if (other == null) return false; + if (other is null) return false; if (other._ports.Length != _ports.Length) return false; for (int i = 0; i < _ports.Length; i++) diff --git a/src/Http/ListeningPort.cs b/src/Http/ListeningPort.cs index 65743f1..24cfdb0 100644 --- a/src/Http/ListeningPort.cs +++ b/src/Http/ListeningPort.cs @@ -7,6 +7,7 @@ // File name: ListeningPort.cs // Repository: https://github.com/sisk-http/core +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Sockets; @@ -39,6 +40,9 @@ namespace Sisk.Core.Http /// /// public readonly struct ListeningPort : IEquatable +#if NET7_0_OR_GREATER + , IParsable +#endif { /// /// Gets or sets the DNS hostname pattern where this listening port will refer. @@ -186,5 +190,40 @@ public override string ToString() { return $"{(Secure ? "https" : "http")}://{Hostname}:{Port}/"; } + + /// + /// Parses a string into a . + /// + /// The string to parse. + /// An object that provides culture-specific formatting information about s. + public static ListeningPort Parse(string s, IFormatProvider? provider) + { + return new ListeningPort(s); + } + + /// + /// Tries to parse a string into a . + /// + /// The string to parse. + /// An object that provides culture-specific formatting information about s. + /// When this method returns, contains the result of successfully parsing s or an undefined value on failure. + public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out ListeningPort result) + { + if (s is null) + { + result = default; + return false; + } + try + { + result = Parse(s, provider); + return true; + } + catch + { + result = default; + return false; + } + } } } diff --git a/src/Http/LogStream.cs b/src/Http/LogStream.cs index f516b4c..5314965 100644 --- a/src/Http/LogStream.cs +++ b/src/Http/LogStream.cs @@ -8,8 +8,8 @@ // Repository: https://github.com/sisk-http/core using Sisk.Core.Entity; -using System.Collections.Concurrent; using System.Text; +using System.Threading.Channels; namespace Sisk.Core.Http { @@ -18,19 +18,18 @@ namespace Sisk.Core.Http /// public class LogStream : IDisposable { - readonly BlockingCollection logQueue = new BlockingCollection(); - string? filePath; - private bool isDisposed; - Thread consumerThread; - AutoResetEvent logQueueEmptyEvent = new AutoResetEvent(true); - CircularBuffer? _bufferingContent = null; - + private readonly Channel channel = Channel.CreateUnbounded(new UnboundedChannelOptions() { SingleReader = true }); + private readonly Thread consumerThread; + internal readonly ManualResetEvent writeEvent = new ManualResetEvent(false); + internal readonly ManualResetEvent rotatingPolicyLocker = new ManualResetEvent(true); internal RotatingLogPolicy? rotatingLogPolicy = null; - internal ManualResetEvent rotatingPolicyLocker = new ManualResetEvent(true); - internal SemaphoreSlim sm = new SemaphoreSlim(1); + + private string? filePath; + private bool isDisposed; + private CircularBuffer? _bufferingContent = null; /// - /// Represents a LogStream that writes its output to the stream. + /// Represents a that writes its output to the stream. /// public static readonly LogStream ConsoleOutput = new LogStream(Console.Out); @@ -41,7 +40,7 @@ public RotatingLogPolicy RotatingPolicy { get { - if (rotatingLogPolicy == null) + if (rotatingLogPolicy is null) { rotatingLogPolicy = new RotatingLogPolicy(this); } @@ -55,6 +54,18 @@ public RotatingLogPolicy RotatingPolicy /// public bool IsBuffering { get => _bufferingContent is not null; } + + /// + /// Gets an boolean indicating if this was disposed. + /// + public bool Disposed { get => isDisposed; } + + /// + /// Gets or sets a boolean that indicates that every input must be trimmed and normalized before + /// being written to some output stream. + /// + public bool NormalizeEntries { get; set; } = true; + /// /// Gets or sets the absolute path to the file where the log is being written to. /// @@ -97,7 +108,6 @@ public string? FilePath public LogStream() { consumerThread = new Thread(new ThreadStart(ProcessQueue)); - consumerThread.IsBackground = true; consumerThread.Start(); } @@ -135,9 +145,7 @@ public LogStream(string? filename, TextWriter? tw) : this() /// public void Flush() { - logQueueEmptyEvent.Reset(); - logQueueEmptyEvent.WaitOne(); - ; + writeEvent.WaitOne(); } /// @@ -155,7 +163,7 @@ public string Peek() lock (_bufferingContent) { string[] lines = _bufferingContent.ToArray(); - return string.Join("", lines); + return string.Join(Environment.NewLine, lines); } } @@ -177,39 +185,65 @@ public void StopBuffering() _bufferingContent = null; } - private void ProcessQueue() + private async void ProcessQueue() { - while (!isDisposed) + var reader = channel.Reader; + while (!isDisposed && await reader.WaitToReadAsync()) { - if (!rotatingPolicyLocker.WaitOne(2500)) - { - continue; - } - if (!logQueue.TryTake(out object? data, 2500)) - { - continue; - } - - string? dataStr = data?.ToString(); - if (dataStr is null) - continue; + writeEvent.Reset(); - if (TextWriter is not null) - { - TextWriter.WriteLine(dataStr); - TextWriter.Flush(); - } - if (filePath is not null) + while (reader.TryRead(out var item)) { - File.AppendAllLines(filePath, contents: new string[] { dataStr }); + rotatingPolicyLocker.WaitOne(); + + bool gotAnyError = false; + string? dataStr = item?.ToString(); + + if (dataStr is null) + continue; + + try + { + TextWriter?.WriteLine(dataStr); + } + catch + { + if (!gotAnyError) + { + await channel.Writer.WriteAsync(item); + gotAnyError = true; + } + } + + try + { + if (filePath is not null) + File.AppendAllText(filePath, dataStr + Environment.NewLine, Encoding); + } + catch + { + if (!gotAnyError) + { + await channel.Writer.WriteAsync(item); + gotAnyError = true; + } + } + + try + { + _bufferingContent?.Add(dataStr); + } + catch + { + if (!gotAnyError) + { + await channel.Writer.WriteAsync(item); + gotAnyError = true; + } + } } - _bufferingContent?.Add(dataStr); - - if (logQueue.Count == 0) - { - logQueueEmptyEvent.Set(); - } + writeEvent.Set(); } } @@ -234,7 +268,7 @@ public virtual void WriteException(Exception exp) /// public void WriteLine() { - WriteLineInternal(""); + WriteLineInternal(string.Empty); } /// @@ -243,7 +277,7 @@ public void WriteLine() /// The text that will be written in the output. public void WriteLine(object? message) { - WriteLineInternal(message?.ToString() ?? ""); + WriteLineInternal(message?.ToString() ?? string.Empty); } /// @@ -271,7 +305,14 @@ public void WriteLine(string format, params object?[] args) /// The line which will be written to the log stream. protected virtual void WriteLineInternal(string line) { - EnqueueMessageLine(line.Normalize()); + if (NormalizeEntries) + { + EnqueueMessageLine(line.Normalize().Trim().ReplaceLineEndings()); + } + else + { + EnqueueMessageLine(line); + } } /// @@ -293,10 +334,7 @@ public LogStream ConfigureRotatingPolicy(long maximumSize, TimeSpan dueTime) void EnqueueMessageLine(string message) { ArgumentNullException.ThrowIfNull(message, nameof(message)); - lock (logQueue) - { - logQueue.Add(message); - } + _ = channel.Writer.WriteAsync(message); } void WriteExceptionInternal(StringBuilder exceptionSbuilder, Exception exp, int currentDepth = 0) @@ -328,9 +366,8 @@ protected virtual void Dispose(bool disposing) if (disposing) { Flush(); + channel.Writer.Complete(); TextWriter?.Dispose(); - rotatingPolicyLocker?.Dispose(); - logQueueEmptyEvent?.Dispose(); rotatingLogPolicy?.Dispose(); consumerThread.Join(); } diff --git a/src/Http/RotatingLogPolicy.cs b/src/Http/RotatingLogPolicy.cs index cdf70e4..62a5d16 100644 --- a/src/Http/RotatingLogPolicy.cs +++ b/src/Http/RotatingLogPolicy.cs @@ -18,7 +18,6 @@ namespace Sisk.Core.Http public sealed class RotatingLogPolicy : IDisposable { private System.Timers.Timer? checkTimer = null; - private bool isTerminating = false; internal LogStream? _logStream; private bool disposedValue; @@ -79,13 +78,13 @@ public void Configure(long maximumSize, TimeSpan due) checkTimer.Interval = Due.TotalMilliseconds; checkTimer.Elapsed += Check; - checkTimer?.Start(); + checkTimer.Start(); } private void Check(object? state, ElapsedEventArgs e) { - if (isTerminating) return; - if (_logStream is null) return; + if (disposedValue) return; + if (_logStream is null || _logStream.Disposed) return; if (checkTimer is null) return; string file = _logStream.FilePath!; @@ -104,14 +103,16 @@ private void Check(object? state, ElapsedEventArgs e) try { _logStream.rotatingPolicyLocker.Reset(); - _logStream.Flush(); using (FileStream logSs = fileInfo.Open(FileMode.OpenOrCreate)) using (FileStream gzFileSs = new FileInfo(gzippedFilename).Create()) using (GZipStream gzSs = new GZipStream(gzFileSs, CompressionMode.Compress)) { - logSs.CopyTo(gzSs); - logSs.SetLength(0); + if (logSs.CanRead) + logSs.CopyTo(gzSs); + + if (logSs.CanWrite) + logSs.SetLength(0); } } catch diff --git a/src/Http/Streams/HttpEventSourceCollection.cs b/src/Http/Streams/HttpEventSourceCollection.cs index 508ffe6..8f2fce1 100644 --- a/src/Http/Streams/HttpEventSourceCollection.cs +++ b/src/Http/Streams/HttpEventSourceCollection.cs @@ -87,7 +87,7 @@ public HttpRequestEventSource[] Find(Func predicate) { return _eventSources.Where(e => { - if (!e.IsActive || e.Identifier == null) return false; + if (!e.IsActive || e.Identifier is null) return false; return predicate(e.Identifier); }).ToArray(); } diff --git a/src/Internal/CookieParser.cs b/src/Internal/CookieParser.cs index 29dd3c3..486c667 100644 --- a/src/Internal/CookieParser.cs +++ b/src/Internal/CookieParser.cs @@ -36,7 +36,7 @@ public static NameValueCollection ParseCookieString(string? cookieHeader) int eqPos = cookieExpression.IndexOf(SharedChars.Equal); if (eqPos < 0) { - cookies[cookieExpression] = ""; + cookies[cookieExpression] = string.Empty; continue; } else diff --git a/src/Internal/HttpStringInternals.cs b/src/Internal/HttpStringInternals.cs index 3e373cb..14d6d91 100644 --- a/src/Internal/HttpStringInternals.cs +++ b/src/Internal/HttpStringInternals.cs @@ -106,7 +106,7 @@ public static PathMatchResult IsPathMatch(ReadOnlySpan pathPattern, ReadOn if (pathPtt.StartsWith(ROUTE_GROUP_START) && pathPtt.EndsWith(ROUTE_GROUP_END)) { - if (query == null) query = new NameValueCollection(); + if (query is null) query = new NameValueCollection(); string queryValueName = new string(pathPtt[new Range(1, pathPtt.Length - 1)]); query.Add(queryValueName, new string(reqsPtt)); } @@ -184,7 +184,7 @@ public static PathMatchResult IsPathMatch(in string pathPattern, in string reque if (pathPtt.StartsWith('<') && pathPtt.EndsWith('>')) { - if (query == null) query = new NameValueCollection(); + if (query is null) query = new NameValueCollection(); string queryValueName = pathPtt.Substring(1, pathPtt.Length - 2); query.Add(queryValueName, reqsPtt); } @@ -224,7 +224,7 @@ public static bool IsDnsMatch(in string wildcardPattern, string subject) } else if (wildcardCount == 1) { - string newWildcardPattern = wildcardPattern.Replace("*", ""); + string newWildcardPattern = wildcardPattern.Replace("*", string.Empty); if (wildcardPattern.StartsWith("*")) { diff --git a/src/Routing/RouteDefinition.cs b/src/Routing/RouteDefinition.cs index 3e22d8d..ef4fe17 100644 --- a/src/Routing/RouteDefinition.cs +++ b/src/Routing/RouteDefinition.cs @@ -26,7 +26,7 @@ public RouteDefinition(RouteMethod method, String path) public static RouteDefinition GetFromCallback(RouteAction action) { RouteAttribute? callbackType = action.GetMethodInfo().GetCustomAttribute(true); - if (callbackType == null) + if (callbackType is null) { throw new InvalidOperationException(SR.Router_RouteDefinitionNotFound); } diff --git a/src/Routing/Router__CoreInvoker.cs b/src/Routing/Router__CoreInvoker.cs index 98bf301..d88cae9 100644 --- a/src/Routing/Router__CoreInvoker.cs +++ b/src/Routing/Router__CoreInvoker.cs @@ -111,7 +111,7 @@ internal bool InvokeRequestHandlerGroup(in RequestHandlerExecutionMode mode, IRe [MethodImpl(MethodImplOptions.AggressiveOptimization)] internal RouterExecutionResult Execute(HttpContext context) { - if (parentServer == null) throw new InvalidOperationException(SR.Router_NotBinded); + if (parentServer is null) throw new InvalidOperationException(SR.Router_NotBinded); context.Router = this; HttpRequest request = context.Request; @@ -220,7 +220,7 @@ internal RouterExecutionResult Execute(HttpContext context) { HttpResponse res = new HttpResponse(); res.Status = HttpStatusCode.TemporaryRedirect; - res.Headers.Add("Location", request.Path + "/" + (request.QueryString ?? "")); + res.Headers.Add("Location", request.Path + "/" + (request.QueryString ?? string.Empty)); return new RouterExecutionResult(res, matchedRoute, matchResult, null); } diff --git a/src/Routing/Router__CoreSetters.cs b/src/Routing/Router__CoreSetters.cs index b81da7b..8e4b87c 100644 --- a/src/Routing/Router__CoreSetters.cs +++ b/src/Routing/Router__CoreSetters.cs @@ -7,9 +7,12 @@ // File name: Router__CoreSetters.cs // Repository: https://github.com/sisk-http/core +using Sisk.Core.Entity; +using Sisk.Core.Http; using Sisk.Core.Internal; using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Web; namespace Sisk.Core.Routing; @@ -186,6 +189,17 @@ public void MapPatch(string path, RouteAction action) public void MapAny(string path, RouteAction action) => SetRoute(RouteMethod.Any, path, action); + /// + /// Maps a rewrite route, which redirects all requests that match the given path to another path, + /// keeping the body and headers of the original request. + /// + /// The incoming HTTP request path. + /// The rewrited URL. + public void Rewrite(string rewritePath, string rewriteInto) + { + SetRoute(RouteMethod.Any, rewritePath, request => RewriteHandler(rewriteInto, request)); + } + /// /// Defines an route with their method, path and action function. /// @@ -327,7 +341,7 @@ private void SetInternal(MethodInfo[] methods, Type callerType, object? instance { RouteAction r; - if (instance == null) + if (instance is null) { r = (RouteAction)Delegate.CreateDelegate(typeof(RouteAction), method); } @@ -373,6 +387,19 @@ private void SetInternal(MethodInfo[] methods, Type callerType, object? instance } #endregion + private HttpResponse RewriteHandler(string rewriteInto, HttpRequest request) + { + string newPath = rewriteInto; + foreach (StringValue item in request.Query) + { + newPath = newPath.Replace($"<{item.Name}>", HttpUtility.UrlEncode(item.Value)); + } + + return new HttpResponse() + .WithStatus(System.Net.HttpStatusCode.Found) + .WithHeader("Location", newPath); + } + private Route? GetCollisionRoute(RouteMethod method, string path) { if (!path.StartsWith('/')) diff --git a/src/Routing/ValueResult.cs b/src/Routing/ValueResult.cs index 780ab15..1be425b 100644 --- a/src/Routing/ValueResult.cs +++ b/src/Routing/ValueResult.cs @@ -42,7 +42,7 @@ public static implicit operator T(ValueResult box) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static implicit operator ValueResult(T value) { - if (value == null) throw new NullReferenceException(SR.ValueResult_Null); + if (value is null) throw new NullReferenceException(SR.ValueResult_Null); return Unsafe.As>(value)!; } } \ No newline at end of file diff --git a/src/Sisk.Core.csproj b/src/Sisk.Core.csproj index 831bac6..c8eb8f5 100644 --- a/src/Sisk.Core.csproj +++ b/src/Sisk.Core.csproj @@ -31,7 +31,7 @@ 1.0.0.0 1.0.0.0 - 1.0.0.0-rc6 + 1.0.0.0-rc8 LICENSE.txt False