diff --git a/fluxzy.core.sln b/fluxzy.core.sln index 660886c4c..c08d98913 100644 --- a/fluxzy.core.sln +++ b/fluxzy.core.sln @@ -39,11 +39,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "__global", "__global", "{00 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{30657256-C907-42C2-B7B2-DA4FD8D0E412}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{F5C5DD67-8A16-4561-8545-2EF0441A2015}" - ProjectSection(SolutionItems) = preProject - tools\scripts\NameAndCompress.csx = tools\scripts\NameAndCompress.csx - EndProjectSection -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fluxzy.Core", "src\Fluxzy.Core\Fluxzy.Core.csproj", "{C79EDDB1-E4FA-4AB3-BAE4-B60D4D4E0B5E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "fluxzy", "src\Fluxzy\fluxzy.csproj", "{7826F811-8525-456B-99AC-03FBB8D37484}" diff --git a/src/Fluxzy.Core/Core/ConnectionErrorHandler.cs b/src/Fluxzy.Core/Core/ConnectionErrorHandler.cs index f10faba9e..e522144f9 100644 --- a/src/Fluxzy.Core/Core/ConnectionErrorHandler.cs +++ b/src/Fluxzy.Core/Core/ConnectionErrorHandler.cs @@ -7,13 +7,20 @@ using System.Security.Authentication; using System.Text; using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; using Fluxzy.Clients; using Fluxzy.Clients.H2; +using Fluxzy.Misc.ResizableBuffers; +using Fluxzy.Rules; +using Fluxzy.Writers; namespace Fluxzy.Core { internal static class ConnectionErrorHandler { + private static readonly JsonSerializerOptions PrettyJsonOptions = new JsonSerializerOptions { WriteIndented = true }; + public static bool RequalifyOnResponseSendError( Exception ex, Exchange exchange, ITimingProvider timingProvider) @@ -82,6 +89,10 @@ public static bool RequalifyOnResponseSendError( if (ex.TryGetException(out var clientErrorException)) { exchange.ClientErrors.Add(clientErrorException.ClientError); } + + if (ex.TryGetException(out var ruleExecutionFailureException)) { + exchange.ClientErrors.Add(new ClientError(999, ruleExecutionFailureException.Message)); + } if (!exchange.ClientErrors.Any()) { exchange.ClientErrors.Add(new ClientError(0, "A generic error has occured") { @@ -93,6 +104,7 @@ public static bool RequalifyOnResponseSendError( ex is IOException || ex is H2Exception || ex is ClientErrorException || + ex is RuleExecutionFailureException || ex is AuthenticationException) { if (DebugContext.EnableDumpStackTraceOn502) { var message = "Fluxzy close connection due to server connection errors.\r\n\r\n"; @@ -107,11 +119,7 @@ ex is ClientErrorException || if (DebugContext.EnableDumpStackTraceOn502) { exchange.Metrics.ErrorInstant = DateTime.Now; - - message += "\r\n" + "\r\n" + JsonSerializer.Serialize(exchange.Metrics, - new JsonSerializerOptions { - WriteIndented = true - }); + message += "\r\n" + "\r\n" + JsonSerializer.Serialize(exchange.Metrics,PrettyJsonOptions); } var messageBinary = Encoding.UTF8.GetBytes(message); @@ -123,9 +131,6 @@ ex is ClientErrorException || header.AsMemory(), exchange.Authority.Secure, true); - //if (DebugContext.EnableDumpStackTraceOn502) - // Console.WriteLine(message); - exchange.Response.Body = new MemoryStream(messageBinary); if (!exchange.ExchangeCompletionSource.Task.IsCompleted) { @@ -156,5 +161,72 @@ ex is ClientErrorException || return false; } + + public static async Task HandleGenericException(Exception ex, + ExchangeSourceInitResult? exchangeInitResult, + Exchange? exchange, + RsBuffer buffer, + RealtimeArchiveWriter? archiveWriter, + ITimingProvider timingProvider, CancellationToken token) + { + if (exchange?.Connection == null || exchangeInitResult?.WriteStream == null) + return false; + + var message = "A configuration error has occured.\r\n"; + + if (ex is RuleExecutionFailureException ruleException) { + message = + "A rule execution failure has occured.\r\n\r\n" + ruleException.Message; + } + + if (DebugContext.EnableDumpStackTraceOn502) + { + message += $"\r\n" + + $"Stacktrace:\r\n{ex}"; + } + + var (header, body) = ConnectionErrorPageHelper.GetSimplePlainTextResponse( + exchange.Authority, + message, ex.GetType().Name); + + exchange.ClientErrors.Add(new ClientError(9999, message)); + + exchange.Response.Header = new ResponseHeader( + header.AsMemory(), + exchange.Authority.Secure, true); + + exchange.Response.Body = new MemoryStream(body); + + var responseHeaderLength = exchange.Response.Header! + .WriteHttp11(false, buffer, true, true, + true); + + exchange.Metrics.ResponseHeaderStart = timingProvider.Instant(); + + await exchangeInitResult.WriteStream + .WriteAsync(buffer.Buffer, 0, responseHeaderLength, token) + .ConfigureAwait(false); + + exchange.Metrics.ResponseHeaderEnd = timingProvider.Instant(); + exchange.Metrics.ResponseBodyStart = timingProvider.Instant(); + + await exchange.Response.Body.CopyToAsync(exchangeInitResult.WriteStream, token) + .ConfigureAwait(false); + + if (exchange.Metrics.ResponseBodyEnd == default) + { + exchange.Metrics.ResponseBodyEnd = timingProvider.Instant(); + } + + if (!exchange.ExchangeCompletionSource.Task.IsCompleted) + { + exchange.ExchangeCompletionSource.TrySetResult(true); + } + + archiveWriter?.Update(exchange, ArchiveUpdateType.AfterResponse, token); + + + return true; + } } } diff --git a/src/Fluxzy.Core/Core/ConnectionErrorPageHelper.cs b/src/Fluxzy.Core/Core/ConnectionErrorPageHelper.cs index 8fd988335..a257c1292 100644 --- a/src/Fluxzy.Core/Core/ConnectionErrorPageHelper.cs +++ b/src/Fluxzy.Core/Core/ConnectionErrorPageHelper.cs @@ -10,11 +10,19 @@ namespace Fluxzy.Core { internal static class ConnectionErrorPageHelper { - private static readonly string ErrorHeader = + private static readonly string ErrorHeaderHtml = "HTTP/1.1 {0}\r\n" + "x-fluxzy: Fluxzy error\r\n" + "Content-length: {1}\r\n" + - "Content-type: text/html\r\n" + + "Content-type: text/html; charset: utf-8\r\n" + + "Connection : close\r\n\r\n"; + + private static readonly string ErrorHeaderText = + "HTTP/1.1 {0}\r\n" + + "x-fluxzy: Fluxzy error\r\n" + + "x-fluxzy-error-type: {2}\r\n" + + "Content-length: {1}\r\n" + + "Content-type: text/plain; charset: utf-8\r\n" + "Connection : close\r\n\r\n"; private static string BodyTemplate { get; } @@ -60,7 +68,16 @@ public static (string FlatHeader, byte[] BodyContent) GetPrettyErrorPage( bodyTemplate = bodyTemplate.Replace("@@error-message@@", errorMessage); var body = Encoding.UTF8.GetBytes(bodyTemplate); - var header = string.Format(ErrorHeader, headerStatus, body.Length); + var header = string.Format(ErrorHeaderHtml, headerStatus, body.Length); + return (header, body); + } + + public static (string FlatHeader, byte[] BodyContent) GetSimplePlainTextResponse( + Authority authority, string messageText, string errorTypeText) + { + var statusLine = "502 Fluxzy Configuration Error"; + var header = string.Format(ErrorHeaderText, statusLine, messageText.Length, errorTypeText); + var body = Encoding.UTF8.GetBytes(messageText); return (header, body); } } diff --git a/src/Fluxzy.Core/Core/ProxyOrchestrator.cs b/src/Fluxzy.Core/Core/ProxyOrchestrator.cs index 8252d7bbc..6262a83df 100644 --- a/src/Fluxzy.Core/Core/ProxyOrchestrator.cs +++ b/src/Fluxzy.Core/Core/ProxyOrchestrator.cs @@ -43,7 +43,11 @@ public void Dispose() public async ValueTask Operate(TcpClient client, RsBuffer buffer, bool closeImmediately, CancellationToken token) { - try { + Exchange? exchange = null; + ExchangeSourceInitResult? exchangeSourceInitResult = null; + + try + { if (D.EnableTracing) { @@ -55,17 +59,19 @@ public async ValueTask Operate(TcpClient client, RsBuffer buffer, bool closeImme token = callerTokenSource.Token; - if (!token.IsCancellationRequested) { + if (!token.IsCancellationRequested) + { // READ initial state of connection, - ExchangeSourceInitResult? exchangeSourceInitResult = null; - try { + try + { exchangeSourceInitResult = await _exchangeSourceProvider.InitClientConnection( client.GetStream(), buffer, - _exchangeContextBuilder, (IPEndPoint) client.Client.LocalEndPoint!, (IPEndPoint) client.Client.RemoteEndPoint!, token) + _exchangeContextBuilder, (IPEndPoint)client.Client.LocalEndPoint!, (IPEndPoint)client.Client.RemoteEndPoint!, token) .ConfigureAwait(false); } - catch (Exception ex) { + catch (Exception ex) + { // Failure from the local connection if (D.EnableTracing) @@ -87,11 +93,11 @@ public async ValueTask Operate(TcpClient client, RsBuffer buffer, bool closeImme if (exchangeSourceInitResult == null) return; - var exchange = + exchange = exchangeSourceInitResult.ProvisionalExchange; - var endPoint = (IPEndPoint) client.Client.RemoteEndPoint!; - var localEndPoint = (IPEndPoint) client.Client.LocalEndPoint!; + var endPoint = (IPEndPoint)client.Client.RemoteEndPoint!; + var localEndPoint = (IPEndPoint)client.Client.LocalEndPoint!; exchange.Metrics.DownStreamClientPort = endPoint.Port; exchange.Metrics.DownStreamClientAddress = endPoint.Address.ToString(); @@ -102,26 +108,30 @@ public async ValueTask Operate(TcpClient client, RsBuffer buffer, bool closeImme var shouldClose = false; - do { + do + { var processMessage = !exchange.Unprocessed; - if (processMessage) { + if (processMessage) + { // Check whether the local browser ask for a connection close - if (D.EnableTracing) { + if (D.EnableTracing) + { var message = $"[#{exchange.Id}] Processing {exchange.Request.Header.Authority}"; D.TraceInfo(message); } shouldClose = exchange.ShouldClose() || closeImmediately; - if (_proxyRuntimeSetting.UserAgentProvider != null) { + if (_proxyRuntimeSetting.UserAgentProvider != null) + { var userAgentValue = exchange.GetRequestHeaderValue("User-Agent"); // Solve user agent exchange.Agent = Agent.Create(userAgentValue ?? string.Empty, - ((IPEndPoint) client.Client.LocalEndPoint!).Address, + ((IPEndPoint)client.Client.LocalEndPoint!).Address, _proxyRuntimeSetting.UserAgentProvider); } @@ -131,27 +141,32 @@ await _proxyRuntimeSetting.EnforceRules(exchange.Context, FilterScope.RequestHeaderReceivedFromClient, exchange.Connection, exchange).ConfigureAwait(false); - if (exchange.Context.Abort) { + if (exchange.Context.Abort) + { return; } - if (exchange.Context.BreakPointContext != null) { + if (exchange.Context.BreakPointContext != null) + { await exchange.Context.BreakPointContext.ConnectionSetupCompletion .WaitForEdit().ConfigureAwait(false); } // Run header alteration - foreach (var requestHeaderAlteration in exchange.Context.RequestHeaderAlterations) { + foreach (var requestHeaderAlteration in exchange.Context.RequestHeaderAlterations) + { requestHeaderAlteration.Apply(exchange.Request.Header); } IHttpConnectionPool connectionPool; - Stream? originalRequestBodyStream = null; + Stream? originalRequestBodyStream = null; Stream? originalResponseBodyStream = null; - try { - if (exchange.Context.BreakPointContext != null) { + try + { + if (exchange.Context.BreakPointContext != null) + { await exchange.Context.BreakPointContext.RequestHeaderCompletion .WaitForEdit().ConfigureAwait(false); } @@ -162,7 +177,8 @@ await exchange.Context.BreakPointContext.RequestHeaderCompletion exchange.Context.HasRequestBody = hasRequestBody; - if (_archiveWriter != null) { + if (_archiveWriter != null) + { _archiveWriter.Update( exchange, ArchiveUpdateType.BeforeRequestHeader, @@ -171,9 +187,9 @@ await exchange.Context.BreakPointContext.RequestHeaderCompletion if (exchange.Context.HasRequestBodySubstitution) { - originalRequestBodyStream = hasRequestBody? exchange.Request.Body : Stream.Null; + originalRequestBodyStream = hasRequestBody ? exchange.Request.Body : Stream.Null; exchange.Request.Body = await - exchange.Context.GetSubstitutedRequestBody(exchange.Request.Body!, + exchange.Context.GetSubstitutedRequestBody(exchange.Request.Body!, exchange).ConfigureAwait(false); exchange.Request.Header.ForceTransferChunked(); @@ -189,7 +205,8 @@ await exchange.Context.BreakPointContext.RequestHeaderCompletion } } - while (true) { + while (true) + { // get a connection pool for the current exchange connectionPool = await _poolBuilder.GetPool(exchange, _proxyRuntimeSetting, token).ConfigureAwait(false); @@ -212,9 +229,11 @@ await exchange.Context.BreakPointContext.RequestHeaderCompletion D.TraceInfo(message); } } - catch (Exception ex) { + catch (Exception ex) + { - if (ex is ConnectionCloseException || ex is TlsFatalAlert) { + if (ex is ConnectionCloseException || ex is TlsFatalAlert) + { // This connection was "goawayed" while current exchange // tries to use it. @@ -227,7 +246,8 @@ await exchange.Context.BreakPointContext.RequestHeaderCompletion throw; } - finally { + finally + { // We close the request body dispatchstream await SafeCloseRequestBody(exchange, originalRequestBodyStream).ConfigureAwait(false); } @@ -235,7 +255,8 @@ await exchange.Context.BreakPointContext.RequestHeaderCompletion break; } } - catch (Exception exception) { + catch (Exception exception) + { // The caller cancelled the task await SafeCloseRequestBody(exchange, originalRequestBodyStream).ConfigureAwait(false); @@ -254,7 +275,8 @@ await exchange.Context.BreakPointContext.RequestHeaderCompletion // We do not need to read websocket response if (!exchange.Request.Header.IsWebSocketRequest && !exchange.Context.BlindMode - && exchange.Response.Header != null) { + && exchange.Response.Header != null) + { // Request processed by IHttpConnectionPool returns before complete response body // Apply response alteration @@ -264,7 +286,8 @@ await _proxyRuntimeSetting.EnforceRules(exchange.Context, // Setup break point for response - if (exchange.Context.BreakPointContext != null) { + if (exchange.Context.BreakPointContext != null) + { await exchange.Context.BreakPointContext.ResponseHeaderCompletion .WaitForEdit().ConfigureAwait(false); } @@ -299,21 +322,24 @@ await exchange.Context.BreakPointContext.ResponseHeaderCompletion exchange.Response.Header.ForceTransferChunked(); - foreach (var responseHeaderAlteration in exchange.Context.ResponseHeaderAlterations) { + foreach (var responseHeaderAlteration in exchange.Context.ResponseHeaderAlterations) + { responseHeaderAlteration.Apply(exchange.Response.Header); } // Writing the received header to downstream - if (DebugContext.InsertFluxzyMetricsOnResponseHeader) { + if (DebugContext.InsertFluxzyMetricsOnResponseHeader) + { exchange.Response.Header?.AddExtraHeaderFieldToLocalConnection( exchange.GetMetricsSummaryAsHeader()); } - - var responseHeaderLength = exchange.Response.Header!.WriteHttp11(false,buffer, true, true, shouldClose); - if (_archiveWriter != null) { + var responseHeaderLength = exchange.Response.Header!.WriteHttp11(false, buffer, true, true, shouldClose); + + if (_archiveWriter != null) + { // Update the state of the exchange // _archiveWriter.Update(exchange, ArchiveUpdateType.AfterResponseHeader, @@ -321,7 +347,8 @@ await exchange.Context.BreakPointContext.ResponseHeaderCompletion ); if (responseBodyStream != null && - (!responseBodyStream.CanSeek || responseBodyStream.Length > 0)) { + (!responseBodyStream.CanSeek || responseBodyStream.Length > 0)) + { if (exchange.Context.HasResponseBodySubstitution) { @@ -354,9 +381,10 @@ await exchange.Context.BreakPointContext.ResponseHeaderCompletion }; exchange.Response.Body = dispatchStream; - responseBodyStream = dispatchStream; + responseBodyStream = dispatchStream; } - else { + else + { // No response body, we ensure the stream is done _archiveWriter.Update(exchange, @@ -367,13 +395,15 @@ await exchange.Context.BreakPointContext.ResponseHeaderCompletion } } - try { + try + { // Start sending response to browser await exchangeSourceInitResult.WriteStream.WriteAsync( new ReadOnlyMemory(buffer.Buffer, 0, responseHeaderLength), token).ConfigureAwait(false); } - catch (Exception ex) { + catch (Exception ex) + { await SafeCloseRequestBody(exchange, originalRequestBodyStream).ConfigureAwait(false); await SafeCloseResponseBody(exchange, originalResponseBodyStream).ConfigureAwait(false); @@ -386,16 +416,19 @@ await exchangeSourceInitResult.WriteStream.WriteAsync( } if (exchange.Response.Header.ContentLength != 0 && - responseBodyStream != null) { + responseBodyStream != null) + { var localConnectionWriteStream = exchangeSourceInitResult.WriteStream; - if (exchange.Response.Header.ChunkedBody && - exchange.Response.Header.HasResponseBody(exchange.Request.Header.Method.Span, out _)) { + if (exchange.Response.Header.ChunkedBody && + exchange.Response.Header.HasResponseBody(exchange.Request.Header.Method.Span, out _)) + { localConnectionWriteStream = new ChunkedTransferWriteStream(localConnectionWriteStream); } - try { + try + { await responseBodyStream.CopyDetailed( localConnectionWriteStream, buffer.Buffer, _ => { }, token).ConfigureAwait(false); @@ -403,14 +436,17 @@ await responseBodyStream.CopyDetailed( await exchangeSourceInitResult.WriteStream.FlushAsync(CancellationToken.None).ConfigureAwait(false); } - catch (Exception ex) { - if (ex is IOException || ex is OperationCanceledException) { + catch (Exception ex) + { + if (ex is IOException || ex is OperationCanceledException) + { // Local connection may close the underlying stream before // receiving the entire message. Particulary when waiting for the last 0\r\n\r\n on chunked stream. // In that case, we just leave // without any error - if (ex is IOException && ex.InnerException is SocketException sex) { + if (ex is IOException && ex.InnerException is SocketException sex) + { if (sex.SocketErrorCode == SocketError.ConnectionAborted) callerTokenSource.Cancel(); } @@ -420,13 +456,16 @@ await responseBodyStream.CopyDetailed( throw; } - finally { + finally + { await SafeCloseRequestBody(exchange, originalRequestBodyStream).ConfigureAwait(false); await SafeCloseResponseBody(exchange, originalResponseBodyStream).ConfigureAwait(false); } } - else { - if (responseBodyStream != null) { + else + { + if (responseBodyStream != null) + { await SafeCloseRequestBody(exchange, originalRequestBodyStream).ConfigureAwait(false); await SafeCloseResponseBody(exchange, originalResponseBodyStream).ConfigureAwait(false); } @@ -435,10 +474,12 @@ await responseBodyStream.CopyDetailed( // In case the down stream connection is persisted, // we wait for the current exchange to complete before reading further request - try { + try + { shouldClose = shouldClose || await exchange.Complete.ConfigureAwait(false); } - catch (ExchangeException) { + catch (ExchangeException) + { // Enhance your calm } } @@ -451,7 +492,8 @@ await responseBodyStream.CopyDetailed( if (shouldClose) break; - try { + try + { // Read the next HTTP message exchange = await _exchangeSourceProvider.ReadNextExchange( exchangeSourceInitResult.ReadStream, @@ -459,14 +501,16 @@ await responseBodyStream.CopyDetailed( buffer, _exchangeContextBuilder, token ).ConfigureAwait(false); - if (exchange != null) { - var ep2 = (IPEndPoint) client.Client.RemoteEndPoint!; + if (exchange != null) + { + var ep2 = (IPEndPoint)client.Client.RemoteEndPoint!; exchange.Metrics.DownStreamClientPort = ep2.Port; exchange.Metrics.DownStreamClientAddress = ep2.Address.ToString(); } } - catch (IOException ex) { + catch (IOException ex) + { // Downstream close the underlying connection if (D.EnableTracing) @@ -481,9 +525,11 @@ await responseBodyStream.CopyDetailed( while (exchange != null); } } - catch (Exception ex) { + catch (Exception ex) + { - if (ex is OperationCanceledException) { + if (ex is OperationCanceledException) + { if (D.EnableTracing) { @@ -494,54 +540,67 @@ await responseBodyStream.CopyDetailed( return; } - // FATAL exception only happens here - throw; + var handleResult = await + ConnectionErrorHandler.HandleGenericException(ex, exchangeSourceInitResult, + exchange, buffer, _archiveWriter, ITimingProvider.Default, token); + + if (!handleResult) + // + throw; + } } private ValueTask SafeCloseRequestBody(Exchange exchange, Stream? substitutionStream) { - if (exchange.Request.Body != null) { - try { + if (exchange.Request.Body != null) + { + try + { // Clean the pipe var body = exchange.Request.Body; - exchange.Request.Body = null; + exchange.Request.Body = null; return body.DisposeAsync(); } - catch { + catch + { // ignore errors when closing pipe } } SafeCloseExtraStream(substitutionStream); - return default; + return default; } private ValueTask SafeCloseResponseBody(Exchange exchange, Stream? substitutionStream) { - if (exchange.Response.Body != null) { - try { + if (exchange.Response.Body != null) + { + try + { // Clean the pipe var body = exchange.Response.Body; exchange.Response.Body = null; return body.DisposeAsync(); } - catch { + catch + { // ignore errors when closing pipe } } SafeCloseExtraStream(substitutionStream); - return default; + return default; } private ValueTask SafeCloseExtraStream(params Stream?[] streams) { - foreach (var stream in streams) { + foreach (var stream in streams) + { if (stream == null) continue; @@ -558,4 +617,4 @@ private ValueTask SafeCloseExtraStream(params Stream?[] streams) return default; } } -} +} \ No newline at end of file diff --git a/src/Fluxzy.Core/Core/ProxyRuntimeSetting.cs b/src/Fluxzy.Core/Core/ProxyRuntimeSetting.cs index ab982b745..1e6afb50d 100644 --- a/src/Fluxzy.Core/Core/ProxyRuntimeSetting.cs +++ b/src/Fluxzy.Core/Core/ProxyRuntimeSetting.cs @@ -1,5 +1,6 @@ // Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -130,25 +131,34 @@ public async ValueTask EnforceRules( ExchangeContext context, FilterScope filterScope, Connection? connection = null, Exchange? exchange = null) { - foreach (var rule in _effectiveRules!.Where(a => - a.Action.ActionScope == filterScope - || a.Action.ActionScope == FilterScope.OutOfScope - || (a.Action.ActionScope == FilterScope.CopySibling - && a.Action is MultipleScopeAction multipleScopeAction - && multipleScopeAction.RunScope == filterScope - ) - )) { - await rule.Enforce( - context, exchange, connection, filterScope, - ExecutionContext?.BreakPointManager!).ConfigureAwait(false); + try { + foreach (var rule in _effectiveRules!.Where(a => + a.Action.ActionScope == filterScope + || a.Action.ActionScope == FilterScope.OutOfScope + || (a.Action.ActionScope == FilterScope.CopySibling + && a.Action is MultipleScopeAction multipleScopeAction + && multipleScopeAction.RunScope == filterScope + ) + )) { + await rule.Enforce( + context, exchange, connection, filterScope, + ExecutionContext?.BreakPointManager!).ConfigureAwait(false); + } + + if (exchange?.RunInLiveEdit ?? false) { + var breakPointAction = new BreakPointAction(); + var rule = new Rule(breakPointAction, AnyFilter.Default); + + await rule.Enforce(context, exchange, connection, filterScope, + ExecutionContext?.BreakPointManager!).ConfigureAwait(false); + } } - - if (exchange?.RunInLiveEdit ?? false) { - var breakPointAction = new BreakPointAction(); - var rule = new Rule(breakPointAction, AnyFilter.Default); - - await rule.Enforce(context, exchange, connection, filterScope, - ExecutionContext?.BreakPointManager!).ConfigureAwait(false); + catch (Exception e) { + if (e is RuleExecutionFailureException) { + throw; + } + + throw new RuleExecutionFailureException("Error while evaluating rules: " + e.Message, e); } return context; diff --git a/src/Fluxzy.Core/Rules/Actions/AddResponseHeaderAction.cs b/src/Fluxzy.Core/Rules/Actions/AddResponseHeaderAction.cs index e6dd562a6..f7e06f38b 100644 --- a/src/Fluxzy.Core/Rules/Actions/AddResponseHeaderAction.cs +++ b/src/Fluxzy.Core/Rules/Actions/AddResponseHeaderAction.cs @@ -45,7 +45,7 @@ public override ValueTask InternalAlter( BreakPointManager breakPointManager) { if (string.IsNullOrWhiteSpace(HeaderName)) - throw new RuleExecutionFailureException("Header name cannot be empty"); + throw new RuleExecutionFailureException("Header name cannot be empty", this); context.ResponseHeaderAlterations.Add(new HeaderAlterationAdd(HeaderName.EvaluateVariable(context) ?? string.Empty, HeaderValue.EvaluateVariable(context) ?? string.Empty)); diff --git a/src/Fluxzy.Core/Rules/Actions/ForceRemotePortAction.cs b/src/Fluxzy.Core/Rules/Actions/ForceRemotePortAction.cs index 85159c42a..ca0a92d5f 100644 --- a/src/Fluxzy.Core/Rules/Actions/ForceRemotePortAction.cs +++ b/src/Fluxzy.Core/Rules/Actions/ForceRemotePortAction.cs @@ -33,7 +33,7 @@ public override ValueTask InternalAlter( BreakPointManager breakPointManager) { if (Port <= 0 || Port > 65535) - throw new RuleExecutionFailureException("Port must be between 1 and 65535"); + throw new RuleExecutionFailureException("Port must be between 1 and 65535", this); context.RemoteHostPort = Port; diff --git a/src/Fluxzy.Core/Rules/Actions/HighLevelActions/InjectHtmlTagAction.cs b/src/Fluxzy.Core/Rules/Actions/HighLevelActions/InjectHtmlTagAction.cs index 877e06634..102fcad1b 100644 --- a/src/Fluxzy.Core/Rules/Actions/HighLevelActions/InjectHtmlTagAction.cs +++ b/src/Fluxzy.Core/Rules/Actions/HighLevelActions/InjectHtmlTagAction.cs @@ -73,11 +73,11 @@ public override ValueTask InternalAlter( } if (!FromFile && string.IsNullOrEmpty(HtmlContent)) { - throw new RuleExecutionFailureException("Text is null or empty"); + throw new RuleExecutionFailureException("Text is null or empty", this); } if (FromFile && string.IsNullOrEmpty(FileName)) { - throw new RuleExecutionFailureException("FileName is null or empty"); + throw new RuleExecutionFailureException("FileName is null or empty", this); } if (RestrictToHtml) { diff --git a/src/Fluxzy.Core/Rules/Actions/HighLevelActions/ServeDirectoryAction.cs b/src/Fluxzy.Core/Rules/Actions/HighLevelActions/ServeDirectoryAction.cs index 1ea4ba985..d127862fe 100644 --- a/src/Fluxzy.Core/Rules/Actions/HighLevelActions/ServeDirectoryAction.cs +++ b/src/Fluxzy.Core/Rules/Actions/HighLevelActions/ServeDirectoryAction.cs @@ -32,7 +32,7 @@ public override ValueTask InternalAlter( BreakPointManager breakPointManager) { if (!System.IO.Directory.Exists(Directory)) { - throw new RuleExecutionFailureException($"Directory {Directory} does not exist"); + throw new RuleExecutionFailureException($"Directory {Directory} does not exist", this); } if (exchange == null) { diff --git a/src/Fluxzy.Core/Rules/Actions/HighLevelActions/SetRequestCookieAction.cs b/src/Fluxzy.Core/Rules/Actions/HighLevelActions/SetRequestCookieAction.cs index e7fcc94f1..f6274d2a7 100644 --- a/src/Fluxzy.Core/Rules/Actions/HighLevelActions/SetRequestCookieAction.cs +++ b/src/Fluxzy.Core/Rules/Actions/HighLevelActions/SetRequestCookieAction.cs @@ -42,7 +42,7 @@ public override ValueTask InternalAlter( if (Name == null!) throw new RuleExecutionFailureException( - $"{nameof(Name)} is mandatory for {nameof(SetRequestCookieAction)}"); + $"{nameof(Name)} is mandatory for {nameof(SetRequestCookieAction)}", this); var cookieHeaders = exchange.GetRequestHeaders().Where( c => c.Name.Span.Equals("cookie", StringComparison.OrdinalIgnoreCase)) diff --git a/src/Fluxzy.Core/Rules/Actions/HighLevelActions/SetResponseCookieAction.cs b/src/Fluxzy.Core/Rules/Actions/HighLevelActions/SetResponseCookieAction.cs index 1d1b09c73..a827abd59 100644 --- a/src/Fluxzy.Core/Rules/Actions/HighLevelActions/SetResponseCookieAction.cs +++ b/src/Fluxzy.Core/Rules/Actions/HighLevelActions/SetResponseCookieAction.cs @@ -61,11 +61,11 @@ public override ValueTask InternalAlter( { if (Name == null!) throw new RuleExecutionFailureException( - $"{nameof(Name)} is mandatory for {nameof(SetResponseCookieAction)}"); + $"{nameof(Name)} is mandatory for {nameof(SetResponseCookieAction)}", this); if (Value == null!) throw new RuleExecutionFailureException( - $"{nameof(Value)} is mandatory for {nameof(SetResponseCookieAction)}"); + $"{nameof(Value)} is mandatory for {nameof(SetResponseCookieAction)}", this); if (exchange == null) return default; diff --git a/src/Fluxzy.Core/Rules/Actions/RemoveCacheAction.cs b/src/Fluxzy.Core/Rules/Actions/RemoveCacheAction.cs index f6262e07c..5df2a37fa 100644 --- a/src/Fluxzy.Core/Rules/Actions/RemoveCacheAction.cs +++ b/src/Fluxzy.Core/Rules/Actions/RemoveCacheAction.cs @@ -43,4 +43,4 @@ public override ValueTask InternalAlter( return default; } } -} +} diff --git a/src/Fluxzy.Core/Rules/Actions/SpoofDnsAction.cs b/src/Fluxzy.Core/Rules/Actions/SpoofDnsAction.cs index c5bb11c8e..684466c7e 100644 --- a/src/Fluxzy.Core/Rules/Actions/SpoofDnsAction.cs +++ b/src/Fluxzy.Core/Rules/Actions/SpoofDnsAction.cs @@ -54,7 +54,7 @@ public override ValueTask InternalAlter( if (!string.IsNullOrEmpty(remoteHostIp)) { if (!IPAddress.TryParse(remoteHostIp, out var ip)) - throw new RuleExecutionFailureException($"{remoteHostIp} is not a valid IP address"); + throw new RuleExecutionFailureException($"{remoteHostIp} is not a valid IP address", this); context.RemoteHostIp = ip; } @@ -62,7 +62,7 @@ public override ValueTask InternalAlter( if (RemoteHostPort != null && RemoteHostPort != 0) { if (RemoteHostPort < 0 || RemoteHostPort > 65535) throw new RuleExecutionFailureException( - $"{RemoteHostPort} is not a valid port. Port must be between 0 and 65536 exclusive."); + $"{RemoteHostPort} is not a valid port. Port must be between 0 and 65536 exclusive.", this); context.RemoteHostPort = RemoteHostPort; } diff --git a/src/Fluxzy.Core/Rules/Filters/ExecFilter.cs b/src/Fluxzy.Core/Rules/Filters/ExecFilter.cs index b2c16ae2b..7e13b18a2 100644 --- a/src/Fluxzy.Core/Rules/Filters/ExecFilter.cs +++ b/src/Fluxzy.Core/Rules/Filters/ExecFilter.cs @@ -42,7 +42,7 @@ protected override bool InternalApply( var fileName = Filename.EvaluateVariable(exchangeContext); if (string.IsNullOrWhiteSpace(fileName)) { - throw new RuleExecutionFailureException($"{nameof(Filename)} cannot be null or empty"); + throw new RuleExecutionFailureException($"{nameof(Filename)} cannot be null or empty", this); } var arguments = Arguments.EvaluateVariable(exchangeContext); @@ -75,7 +75,7 @@ protected override bool InternalApply( catch (Exception e) { Console.WriteLine(e); throw new RuleExecutionFailureException($"An error occurs while running process:" + - $"{nameof(Filename)}", e); + $"{nameof(Filename)}", this, e); } } diff --git a/src/Fluxzy.Core/Rules/Filters/RequestFilters/IsSelfFilter.cs b/src/Fluxzy.Core/Rules/Filters/RequestFilters/IsSelfFilter.cs index f99adb665..0273a847e 100644 --- a/src/Fluxzy.Core/Rules/Filters/RequestFilters/IsSelfFilter.cs +++ b/src/Fluxzy.Core/Rules/Filters/RequestFilters/IsSelfFilter.cs @@ -1,39 +1,39 @@ -// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak - -using System.Collections.Generic; +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + +using System.Collections.Generic; using Fluxzy.Core; -using Fluxzy.Misc; - -namespace Fluxzy.Rules.Filters.RequestFilters -{ - [FilterMetaData( - LongDescription = "Check if incoming request considers fluxzy as a web server", - NotSelectable = true - )] - public class IsSelfFilter : Filter - { - public override FilterScope FilterScope => FilterScope.DnsSolveDone; - - protected override bool InternalApply( - ExchangeContext? exchangeContext, IAuthority authority, IExchange? exchange, IFilteringContext? filteringContext) - { - if (exchangeContext == null || !(exchange is Exchange internalExchange)) - return false; - - if (internalExchange.Metrics.DownStreamLocalPort == exchangeContext.RemoteHostPort - && - exchangeContext.RemoteHostIp != null && - IpUtility.LocalAddresses.Contains(exchangeContext.RemoteHostIp)) { - return true; - } - - return false; - } - - public override IEnumerable GetExamples() - { - yield return GetDefaultSample()!; - - } - } -} +using Fluxzy.Misc; + +namespace Fluxzy.Rules.Filters.RequestFilters +{ + [FilterMetaData( + LongDescription = "Check if incoming request considers fluxzy as a web server", + NotSelectable = true + )] + public class IsSelfFilter : Filter + { + public override FilterScope FilterScope => FilterScope.DnsSolveDone; + + protected override bool InternalApply( + ExchangeContext? exchangeContext, IAuthority authority, IExchange? exchange, IFilteringContext? filteringContext) + { + if (exchangeContext == null || !(exchange is Exchange internalExchange)) + return false; + + if (internalExchange.Metrics.DownStreamLocalPort == exchangeContext.RemoteHostPort + && + exchangeContext.RemoteHostIp != null && + IpUtility.LocalAddresses.Contains(exchangeContext.RemoteHostIp)) { + return true; + } + + return false; + } + + public override IEnumerable GetExamples() + { + yield return GetDefaultSample()!; + + } + } +} diff --git a/src/Fluxzy.Core/Rules/Filters/RequestFilters/MethodFilter.cs b/src/Fluxzy.Core/Rules/Filters/RequestFilters/MethodFilter.cs index a5ee6e72e..480fee6be 100644 --- a/src/Fluxzy.Core/Rules/Filters/RequestFilters/MethodFilter.cs +++ b/src/Fluxzy.Core/Rules/Filters/RequestFilters/MethodFilter.cs @@ -74,4 +74,4 @@ public static IConfigureActionBuilder WhenMethodIsPut(this IConfigureFilterBuild return builder.WhenMethodIs("PUT"); } } -} +} diff --git a/src/Fluxzy.Core/Rules/Filters/RequestFilters/RequestHeaderFilter.cs b/src/Fluxzy.Core/Rules/Filters/RequestFilters/RequestHeaderFilter.cs index 5e30984f6..5d28918b8 100644 --- a/src/Fluxzy.Core/Rules/Filters/RequestFilters/RequestHeaderFilter.cs +++ b/src/Fluxzy.Core/Rules/Filters/RequestFilters/RequestHeaderFilter.cs @@ -68,4 +68,4 @@ public static IConfigureActionBuilder WhenRequestHeaderExists( return builder.When(new RequestHeaderFilter("", StringSelectorOperation.Contains, headerName)); } } -} +} diff --git a/src/Fluxzy.Core/Rules/Filters/StringFilter.cs b/src/Fluxzy.Core/Rules/Filters/StringFilter.cs index f3f786f23..448e585be 100644 --- a/src/Fluxzy.Core/Rules/Filters/StringFilter.cs +++ b/src/Fluxzy.Core/Rules/Filters/StringFilter.cs @@ -115,7 +115,7 @@ protected override bool InternalApply( continue; default: - throw new RuleExecutionFailureException($"Unimplemented string operation {Operation}"); + throw new RuleExecutionFailureException($"Unimplemented string operation {Operation}", this); } } diff --git a/src/Fluxzy.Core/Rules/Rule.cs b/src/Fluxzy.Core/Rules/Rule.cs index ee0225c4f..f56028605 100644 --- a/src/Fluxzy.Core/Rules/Rule.cs +++ b/src/Fluxzy.Core/Rules/Rule.cs @@ -42,12 +42,27 @@ public ValueTask Enforce( if (!context.FilterEvaluationResult.TryGetValue(Filter, out var result)) { - result = Filter.Apply(context, context.Authority, exchange, null); - context.FilterEvaluationResult[Filter] = result; + try { + result = Filter.Apply(context, context.Authority, exchange, null); + context.FilterEvaluationResult[Filter] = result; + } + catch (Exception e) { + if (e is RuleExecutionFailureException) + throw; + throw new RuleExecutionFailureException(e.Message, Filter, e); + } } - if (result) - return Action.Alter(context, exchange, connection, filterScope, breakPointManager); + try { + if (result) + return Action.Alter(context, exchange, connection, filterScope, breakPointManager); + } + catch (Exception e) { + if (e is RuleExecutionFailureException) + throw; + + throw new RuleExecutionFailureException(e.Message, Action, e); + } return default; } diff --git a/src/Fluxzy.Core/Rules/RuleDefinitionMismatchException.cs b/src/Fluxzy.Core/Rules/RuleDefinitionMismatchException.cs deleted file mode 100644 index df529202e..000000000 --- a/src/Fluxzy.Core/Rules/RuleDefinitionMismatchException.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak - -using System; - -namespace Fluxzy.Rules -{ - public class RuleExecutionFailureException : Exception - { - public RuleExecutionFailureException(string message) - : base(message) - { - } - - public RuleExecutionFailureException(string message, Exception innerException) - : base(message, innerException) - { - } - } -} diff --git a/src/Fluxzy.Core/Rules/RuleExecutionFailureException.cs b/src/Fluxzy.Core/Rules/RuleExecutionFailureException.cs new file mode 100644 index 000000000..ffc1be2e2 --- /dev/null +++ b/src/Fluxzy.Core/Rules/RuleExecutionFailureException.cs @@ -0,0 +1,75 @@ +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + +using System; +using System.Text; +using Fluxzy.Rules.Filters; +using static System.Collections.Specialized.BitVector32; + +namespace Fluxzy.Rules +{ + public class RuleExecutionFailureException : Exception + { + public RuleExecutionFailureException(string message, Exception ex) + : base(message, ex) + { + } + + public RuleExecutionFailureException(string message, Filter filter) + : base(FormatMessage(message, filter, null)) + { + } + + public RuleExecutionFailureException(string message, Action action) + : base(FormatMessage(message, action, null)) + { + } + + public RuleExecutionFailureException(string message, Filter filter, Exception innerException) + : base(FormatMessage(message, filter, innerException)) + { + } + + public RuleExecutionFailureException(string message, Action action, Exception innerException) + : base(FormatMessage(message, action, innerException)) + { + } + + private static string FormatMessage(string originalMessage, Filter filter, Exception? exception) + { + var builder = new StringBuilder(originalMessage); + + builder.AppendLine(); + builder.AppendLine(); + builder.AppendLine($"Origin: [{filter.GetType().Name}] “{filter.AutoGeneratedName}” " + + $"(Friendly Name: {filter.FriendlyName}) ({filter.Identifier})"); + + if (exception != null) + { + builder.AppendLine(); + builder.AppendLine("Exception:"); + builder.AppendLine(exception.ToString()); + } + + return builder.ToString(); + } + + private static string FormatMessage(string originalMessage, Action action, Exception? exception) + { + var builder = new StringBuilder(originalMessage); + + builder.AppendLine(); + builder.AppendLine(); + builder.AppendLine($"Origin: [{action.GetType().Name}] “{action.DefaultDescription}” " + + $"(Friendly Name: {action.FriendlyName}) ({action.Identifier})"); + + if (exception != null) + { + builder.AppendLine(); + builder.AppendLine("Exception:"); + builder.AppendLine(exception.ToString()); + } + + return builder.ToString(); + } + } +} diff --git a/test/Fluxzy.Tests/Cases/BadRuleHandlingTests.cs b/test/Fluxzy.Tests/Cases/BadRuleHandlingTests.cs new file mode 100644 index 000000000..b800a793f --- /dev/null +++ b/test/Fluxzy.Tests/Cases/BadRuleHandlingTests.cs @@ -0,0 +1,60 @@ +// Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak + +using System; +using System.Linq; +using System.Threading.Tasks; +using Fluxzy.Rules; +using Fluxzy.Rules.Actions; +using Fluxzy.Rules.Extensions; +using Fluxzy.Rules.Filters; +using Fluxzy.Rules.Filters.RequestFilters; +using Fluxzy.Tests._Fixtures; +using Xunit; + +namespace Fluxzy.Tests.Cases +{ + public class BadRuleHandlingTests + { + [Fact] + public async Task HandleBadFilter1Exception() + { + await HandleGeneric(c => + c.When(new HostFilter("*", StringSelectorOperation.Regex)) + .Do(new AddResponseHeaderAction("yes", "no"))); + } + + [Fact] + public async Task HandleBadFilter2Exception() + { + await HandleGeneric(c => + c.When(new AbsoluteUriFilter("*", StringSelectorOperation.Regex)) + .Do(new AddResponseHeaderAction("yes", "no"))); + } + + [Fact] + public async Task HandleBadActionException() + { + await HandleGeneric(c => c.WhenAny().Do(new DelayAction(-10))); + } + + private async Task HandleGeneric(Action builder) + { + var setting = FluxzySetting.CreateLocalRandomPort(); + + builder(setting.ConfigureRule()); + + await using var proxy = new Proxy(setting); + + using var client = HttpClientUtility.CreateHttpClient(proxy.Run(), setting); + + var response = await client.GetAsync(TestConstants.Http2Host); + + var content = await response.Content.ReadAsStringAsync(); + var hasHeader = response.Headers.TryGetValues("x-fluxzy-error-type", out var values); + + Assert.True(hasHeader); + Assert.NotNull(values); + Assert.Equal(nameof(RuleExecutionFailureException), values.First()); + } + } +} diff --git a/test/Fluxzy.Tests/Cases/Expect100ContinueTests.cs b/test/Fluxzy.Tests/Cases/Expect100ContinueTests.cs index c2e8d50b9..a57777790 100644 --- a/test/Fluxzy.Tests/Cases/Expect100ContinueTests.cs +++ b/test/Fluxzy.Tests/Cases/Expect100ContinueTests.cs @@ -1,5 +1,6 @@ // Copyright 2021 - Haga Rakotoharivelo - https://github.com/haga-rak +using System; using System.Net.Http; using System.Text; using System.Threading.Tasks; diff --git a/test/Fluxzy.Tests/UnitTests/Handlers/Http2ConcurrentCall.cs b/test/Fluxzy.Tests/UnitTests/Handlers/Http2ConcurrentCall.cs index c08afecbc..251fa7ab2 100644 --- a/test/Fluxzy.Tests/UnitTests/Handlers/Http2ConcurrentCall.cs +++ b/test/Fluxzy.Tests/UnitTests/Handlers/Http2ConcurrentCall.cs @@ -47,7 +47,8 @@ private async Task CallSimple( await AssertionHelper.ValidateCheck(requestMessage, null, response, token); } - [Fact] + + [Fact(Timeout = 1000 * 60)] public async Task Post_Random_Data_And_Validate_Content() { using var handler = new FluxzyHttp2Handler(); @@ -95,7 +96,7 @@ public async Task Post_Multi_Header_Dynamic_Table_Evict_Simple() /// The goal of this test is to challenge the dynamic table content /// /// - [Theory] + [Theory(Timeout = 1000 * 60)] [InlineData(1024)] [InlineData(16394)] public async Task Post_Dynamic_Table_Evict_Simple_Large_Object(int bufferSize) @@ -128,7 +129,7 @@ public async Task Post_Dynamic_Table_Evict_Simple_Large_Object(int bufferSize) await Task.WhenAll(tasks); } - [Fact] + [Fact(Timeout = 1000 * 60)] public async Task Headers_Multiple_Reception() { using var handler = new FluxzyHttp2Handler(); @@ -189,4 +190,4 @@ private static async Task Receiving_Multiple_Repeating_Header_Value_Call() await Task.WhenAll(tasks); } } -} +} diff --git a/tools/scripts/NameAndCompress.csx b/tools/scripts/NameAndCompress.csx deleted file mode 100644 index 6ba5c414f..000000000 --- a/tools/scripts/NameAndCompress.csx +++ /dev/null @@ -1,27 +0,0 @@ -using System.Runtime.InteropServices; - -var version = Console.In.ReadToEnd().Trim('\r', '\n', ' ', '\t'); - -var shortIdentifier = Operating​System.IsWindows() ? - "windows" : (OperatingSystem.IsMacOS() ? "macos" : - (OperatingSystem.IsLinux() ? "linux" : "custom")); - -shortIdentifier += $"-{RuntimeInformation.ProcessArchitecture}"; -shortIdentifier = shortIdentifier.ToLowerInvariant(); - -var fileName = $"fluxzy-{version}-{shortIdentifier}.zip"; - -var fullFileName = Path.Combine(Args[0], fileName); - -if (System.IO.File.Exists(fullFileName)) { - System.IO.File.Delete(fullFileName); -} - -System.IO.Compression.ZipFile.CreateFromDirectory( - Args[1], - fullFileName, - System.IO.Compression.CompressionLevel.Optimal, - false - ); - -Console.Write($"{fileName}"); \ No newline at end of file