From 256b9c4d362c41f783345e39f967893306ceaf4f Mon Sep 17 00:00:00 2001
From: Stephan van Rooij <1292510+svrooij@users.noreply.github.com>
Date: Fri, 8 Aug 2025 14:34:58 +0200
Subject: [PATCH 01/14] Initial migration attempt
---
.editorconfig | 48 ++-
.../DevProxy.Abstractions.csproj | 2 +-
.../Extensions/FuncExtensions.cs | 2 +-
DevProxy.Abstractions/Plugins/PluginEvents.cs | 2 +-
DevProxy.Abstractions/Proxy/IProxyLogger.cs | 2 +-
DevProxy.Abstractions/Proxy/ProxyEvents.cs | 2 +-
DevProxy.Abstractions/Utils/ProxyUtils.cs | 2 +-
DevProxy.Abstractions/packages.lock.json | 19 +-
.../Behavior/GenericRandomErrorPlugin.cs | 4 +-
.../Behavior/GraphRandomErrorPlugin.cs | 4 +-
.../LanguageModelRateLimitingPlugin.cs | 4 +-
.../Behavior/RateLimitingPlugin.cs | 4 +-
DevProxy.Plugins/Behavior/RetryAfterPlugin.cs | 4 +-
DevProxy.Plugins/DevProxy.Plugins.csproj | 4 -
.../Generation/MockGeneratorPlugin.cs | 2 +-
.../Generation/OpenApiSpecGeneratorPlugin.cs | 4 +-
.../Generation/TypeSpecGeneratorPlugin.cs | 2 +-
.../Guidance/CachingGuidancePlugin.cs | 2 +-
.../GraphClientRequestIdGuidancePlugin.cs | 2 +-
.../Guidance/GraphSdkGuidancePlugin.cs | 2 +-
.../Guidance/GraphSelectGuidancePlugin.cs | 2 +-
.../Guidance/ODSPSearchGuidancePlugin.cs | 2 +-
DevProxy.Plugins/Inspection/DevToolsPlugin.cs | 2 +-
.../Inspection/OpenAITelemetryPlugin.cs | 2 +-
DevProxy.Plugins/Mocking/AuthPlugin.cs | 6 +-
DevProxy.Plugins/Mocking/CrudApiPlugin.cs | 6 +-
.../Mocking/GraphMockResponsePlugin.cs | 2 +-
.../Mocking/MockResponsePlugin.cs | 4 +-
.../Mocking/OpenAIMockResponsePlugin.cs | 2 +-
DevProxy.Plugins/Utils/GraphUtils.cs | 2 +-
DevProxy.Plugins/packages.lock.json | 32 +-
DevProxy.sln | 1 +
DevProxy/ApiControllers/ProxyController.cs | 5 +-
DevProxy/Commands/CertCommand.cs | 12 +-
DevProxy/DevProxy.csproj | 17 +-
.../Extensions/ILoggingBuilderExtensions.cs | 12 +-
.../IServiceCollectionExtensions.cs | 80 ++++-
DevProxy/Logging/ILoggerExtensions.cs | 3 +
DevProxy/Properties/launchSettings.json | 11 +-
DevProxy/Proxy/CertificateDiskCache.cs | 4 +-
.../Proxy/EfficientProxyHttpClientFactory.cs | 15 +
DevProxy/Proxy/ProxyEngine.cs | 184 ++++++++----
DevProxy/Proxy/ProxyStateController.cs | 2 +-
DevProxy/packages.lock.json | 275 ++++++++++--------
44 files changed, 531 insertions(+), 269 deletions(-)
create mode 100644 DevProxy/Proxy/EfficientProxyHttpClientFactory.cs
diff --git a/.editorconfig b/.editorconfig
index 0524a605..2254dff0 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -81,14 +81,14 @@ csharp_style_var_for_built_in_types = true
csharp_style_var_when_type_is_apparent = true
# Expression-bodied members
-csharp_style_expression_bodied_accessors = true
-csharp_style_expression_bodied_constructors = false
-csharp_style_expression_bodied_indexers = true
-csharp_style_expression_bodied_lambdas = true
-csharp_style_expression_bodied_local_functions = false
-csharp_style_expression_bodied_methods = when_on_single_line
-csharp_style_expression_bodied_operators = false
-csharp_style_expression_bodied_properties = true
+csharp_style_expression_bodied_accessors = true:silent
+csharp_style_expression_bodied_constructors = false:silent
+csharp_style_expression_bodied_indexers = true:silent
+csharp_style_expression_bodied_lambdas = true:silent
+csharp_style_expression_bodied_local_functions = false:silent
+csharp_style_expression_bodied_methods = when_on_single_line:silent
+csharp_style_expression_bodied_operators = false:silent
+csharp_style_expression_bodied_properties = true:silent
# Pattern matching preferences
csharp_style_pattern_matching_over_as_with_null_check = true
@@ -106,11 +106,11 @@ csharp_prefer_static_local_function = true
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async
# Code-block preferences
-csharp_prefer_braces = true
-csharp_prefer_simple_using_statement = true
-csharp_style_namespace_declarations = file_scoped
-csharp_style_prefer_method_group_conversion = true
-csharp_style_prefer_top_level_statements = true
+csharp_prefer_braces = true:silent
+csharp_prefer_simple_using_statement = true:suggestion
+csharp_style_namespace_declarations = file_scoped:silent
+csharp_style_prefer_method_group_conversion = true:silent
+csharp_style_prefer_top_level_statements = true:silent
# Expression-level preferences
csharp_prefer_simple_default_expression = true
@@ -128,7 +128,7 @@ csharp_style_unused_value_assignment_preference = discard_variable
csharp_style_unused_value_expression_statement_preference = discard_variable
# 'using' directive preferences
-csharp_using_directive_placement = outside_namespace
+csharp_using_directive_placement = outside_namespace:silent
# New line preferences
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
@@ -263,4 +263,22 @@ dotnet_diagnostic.VSTHRD104.severity = none
# Add .ConfigureAwait(bool) to your await expression
dotnet_diagnostic.VSTHRD111.severity = none
-dotnet_analyzer_diagnostic.severity = warning
\ No newline at end of file
+dotnet_analyzer_diagnostic.severity = warning
+csharp_style_prefer_primary_constructors = true:suggestion
+csharp_prefer_system_threading_lock = true:suggestion
+
+# CS0618: Type or member is obsolete
+dotnet_diagnostic.CS0618.severity = warning
+
+[*.{cs,vb}]
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
+dotnet_style_prefer_auto_properties = true:silent
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+tab_width = 4
+indent_size = 4
+end_of_line = crlf
+dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
\ No newline at end of file
diff --git a/DevProxy.Abstractions/DevProxy.Abstractions.csproj b/DevProxy.Abstractions/DevProxy.Abstractions.csproj
index cc898d59..eaa9d82f 100644
--- a/DevProxy.Abstractions/DevProxy.Abstractions.csproj
+++ b/DevProxy.Abstractions/DevProxy.Abstractions.csproj
@@ -23,7 +23,7 @@
-
+
diff --git a/DevProxy.Abstractions/Extensions/FuncExtensions.cs b/DevProxy.Abstractions/Extensions/FuncExtensions.cs
index 4f0565db..6740b0b3 100644
--- a/DevProxy.Abstractions/Extensions/FuncExtensions.cs
+++ b/DevProxy.Abstractions/Extensions/FuncExtensions.cs
@@ -5,7 +5,7 @@
// from: https://github.com/justcoding121/titanium-web-proxy/blob/902504a324425e4e49fc5ba604c2b7fa172e68ce/src/Titanium.Web.Proxy/Extensions/FuncExtensions.cs
#pragma warning disable IDE0130
-namespace Titanium.Web.Proxy.EventArguments;
+namespace Unobtanium.Web.Proxy.EventArguments;
#pragma warning restore IDE0130
public static class FuncExtensions
diff --git a/DevProxy.Abstractions/Plugins/PluginEvents.cs b/DevProxy.Abstractions/Plugins/PluginEvents.cs
index de9298f0..fb6333a1 100644
--- a/DevProxy.Abstractions/Plugins/PluginEvents.cs
+++ b/DevProxy.Abstractions/Plugins/PluginEvents.cs
@@ -2,7 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using Titanium.Web.Proxy.Http;
+using Unobtanium.Web.Proxy.Http;
namespace DevProxy.Abstractions.Plugins;
diff --git a/DevProxy.Abstractions/Proxy/IProxyLogger.cs b/DevProxy.Abstractions/Proxy/IProxyLogger.cs
index 47106bbf..16d42e3d 100644
--- a/DevProxy.Abstractions/Proxy/IProxyLogger.cs
+++ b/DevProxy.Abstractions/Proxy/IProxyLogger.cs
@@ -2,7 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using Titanium.Web.Proxy.EventArguments;
+using Unobtanium.Web.Proxy.EventArguments;
namespace DevProxy.Abstractions.Proxy;
diff --git a/DevProxy.Abstractions/Proxy/ProxyEvents.cs b/DevProxy.Abstractions/Proxy/ProxyEvents.cs
index 810387d3..55c5fb20 100644
--- a/DevProxy.Abstractions/Proxy/ProxyEvents.cs
+++ b/DevProxy.Abstractions/Proxy/ProxyEvents.cs
@@ -5,7 +5,7 @@
using DevProxy.Abstractions.Utils;
using System.CommandLine;
using System.Text.Json.Serialization;
-using Titanium.Web.Proxy.EventArguments;
+using Unobtanium.Web.Proxy.EventArguments;
namespace DevProxy.Abstractions.Proxy;
diff --git a/DevProxy.Abstractions/Utils/ProxyUtils.cs b/DevProxy.Abstractions/Utils/ProxyUtils.cs
index ec21a389..56b8da9b 100644
--- a/DevProxy.Abstractions/Utils/ProxyUtils.cs
+++ b/DevProxy.Abstractions/Utils/ProxyUtils.cs
@@ -12,7 +12,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
-using Titanium.Web.Proxy.Http;
+using Unobtanium.Web.Proxy.Http;
namespace DevProxy.Abstractions.Utils;
diff --git a/DevProxy.Abstractions/packages.lock.json b/DevProxy.Abstractions/packages.lock.json
index af377dc9..ba78b533 100644
--- a/DevProxy.Abstractions/packages.lock.json
+++ b/DevProxy.Abstractions/packages.lock.json
@@ -97,13 +97,14 @@
},
"Unobtanium.Web.Proxy": {
"type": "Direct",
- "requested": "[0.1.5, )",
- "resolved": "0.1.5",
- "contentHash": "HiICGm0e44+i4aVHpLn+aphmSC2eQnDvlTttw1rE0hntOZKoLGRy37sydqqbRP1ZokMf3Mt0GEgSWxDwnucKGg==",
+ "requested": "[0.9.0-beta.1, )",
+ "resolved": "0.9.0-beta.1",
+ "contentHash": "Ae/2f7v3T3NQkRknBhAHqBrY85nJHQr8lxRD/j8B3rMK4KbIlbGe3A6Hd71ponq+sNHnLxa3BPSg8pdldc9RHg==",
"dependencies": {
"BouncyCastle.Cryptography": "2.4.0",
- "Microsoft.Extensions.Logging.Abstractions": "8.0.1",
- "System.Runtime.CompilerServices.Unsafe": "6.0.0"
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.3",
+ "System.Runtime.CompilerServices.Unsafe": "6.0.0",
+ "Unobtanium.Web.Proxy.Events": "0.9.0-beta.1"
}
},
"YamlDotNet": {
@@ -336,6 +337,14 @@
"type": "Transitive",
"resolved": "9.0.4",
"contentHash": "pYtmpcO6R3Ef1XilZEHgXP2xBPVORbYEzRP7dl0IAAbN8Dm+kfwio8aCKle97rAWXOExr292MuxWYurIuwN62g=="
+ },
+ "Unobtanium.Web.Proxy.Events": {
+ "type": "Transitive",
+ "resolved": "0.9.0-beta.1",
+ "contentHash": "ckgncJ95Tr1GCx3Kv3yJK3sANfJznKIHOqZebqR0w3JblHrNF2VLJ5ALA5YZ5CcO7QuqqXVnfkusapy7QJRFwA==",
+ "dependencies": {
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.1"
+ }
}
}
}
diff --git a/DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs b/DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs
index 96c37658..ccda0309 100644
--- a/DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs
+++ b/DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs
@@ -15,8 +15,8 @@
using System.Net;
using System.Text.Json;
using System.Text.RegularExpressions;
-using Titanium.Web.Proxy.Http;
-using Titanium.Web.Proxy.Models;
+using Unobtanium.Web.Proxy.Http;
+using Unobtanium.Web.Proxy.Models;
namespace DevProxy.Plugins.Behavior;
diff --git a/DevProxy.Plugins/Behavior/GraphRandomErrorPlugin.cs b/DevProxy.Plugins/Behavior/GraphRandomErrorPlugin.cs
index cc12fe67..7ec347a8 100644
--- a/DevProxy.Plugins/Behavior/GraphRandomErrorPlugin.cs
+++ b/DevProxy.Plugins/Behavior/GraphRandomErrorPlugin.cs
@@ -15,8 +15,8 @@
using System.Net;
using System.Text.Json;
using System.Text.RegularExpressions;
-using Titanium.Web.Proxy.Http;
-using Titanium.Web.Proxy.Models;
+using Unobtanium.Web.Proxy.Http;
+using Unobtanium.Web.Proxy.Models;
using DevProxy.Plugins.Models;
namespace DevProxy.Plugins.Behavior;
diff --git a/DevProxy.Plugins/Behavior/LanguageModelRateLimitingPlugin.cs b/DevProxy.Plugins/Behavior/LanguageModelRateLimitingPlugin.cs
index 5dcc2121..25d63f2e 100644
--- a/DevProxy.Plugins/Behavior/LanguageModelRateLimitingPlugin.cs
+++ b/DevProxy.Plugins/Behavior/LanguageModelRateLimitingPlugin.cs
@@ -13,8 +13,8 @@
using System.Globalization;
using System.Net;
using System.Text.Json;
-using Titanium.Web.Proxy.Http;
-using Titanium.Web.Proxy.Models;
+using Unobtanium.Web.Proxy.Http;
+using Unobtanium.Web.Proxy.Models;
namespace DevProxy.Plugins.Behavior;
diff --git a/DevProxy.Plugins/Behavior/RateLimitingPlugin.cs b/DevProxy.Plugins/Behavior/RateLimitingPlugin.cs
index 44864c19..824a60d0 100644
--- a/DevProxy.Plugins/Behavior/RateLimitingPlugin.cs
+++ b/DevProxy.Plugins/Behavior/RateLimitingPlugin.cs
@@ -15,8 +15,8 @@
using System.Net;
using System.Text.Json;
using System.Text.RegularExpressions;
-using Titanium.Web.Proxy.Http;
-using Titanium.Web.Proxy.Models;
+using Unobtanium.Web.Proxy.Http;
+using Unobtanium.Web.Proxy.Models;
namespace DevProxy.Plugins.Behavior;
diff --git a/DevProxy.Plugins/Behavior/RetryAfterPlugin.cs b/DevProxy.Plugins/Behavior/RetryAfterPlugin.cs
index 25e58efc..56f0562e 100644
--- a/DevProxy.Plugins/Behavior/RetryAfterPlugin.cs
+++ b/DevProxy.Plugins/Behavior/RetryAfterPlugin.cs
@@ -13,8 +13,8 @@
using System.Net;
using System.Text.Json;
using System.Text.RegularExpressions;
-using Titanium.Web.Proxy.Http;
-using Titanium.Web.Proxy.Models;
+using Unobtanium.Web.Proxy.Http;
+using Unobtanium.Web.Proxy.Models;
namespace DevProxy.Plugins.Behavior;
diff --git a/DevProxy.Plugins/DevProxy.Plugins.csproj b/DevProxy.Plugins/DevProxy.Plugins.csproj
index 79a8bcde..7e331911 100644
--- a/DevProxy.Plugins/DevProxy.Plugins.csproj
+++ b/DevProxy.Plugins/DevProxy.Plugins.csproj
@@ -60,10 +60,6 @@
false
runtime
-
- false
- runtime
-
diff --git a/DevProxy.Plugins/Generation/MockGeneratorPlugin.cs b/DevProxy.Plugins/Generation/MockGeneratorPlugin.cs
index 957e6564..24d1a0cb 100644
--- a/DevProxy.Plugins/Generation/MockGeneratorPlugin.cs
+++ b/DevProxy.Plugins/Generation/MockGeneratorPlugin.cs
@@ -10,7 +10,7 @@
using DevProxy.Plugins.Utils;
using Microsoft.Extensions.Logging;
using System.Text.Json;
-using Titanium.Web.Proxy.EventArguments;
+using Unobtanium.Web.Proxy.EventArguments;
namespace DevProxy.Plugins.Generation;
diff --git a/DevProxy.Plugins/Generation/OpenApiSpecGeneratorPlugin.cs b/DevProxy.Plugins/Generation/OpenApiSpecGeneratorPlugin.cs
index 255b2a5d..66031153 100644
--- a/DevProxy.Plugins/Generation/OpenApiSpecGeneratorPlugin.cs
+++ b/DevProxy.Plugins/Generation/OpenApiSpecGeneratorPlugin.cs
@@ -19,8 +19,8 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Web;
-using Titanium.Web.Proxy.EventArguments;
-using Titanium.Web.Proxy.Http;
+using Unobtanium.Web.Proxy.EventArguments;
+using Unobtanium.Web.Proxy.Http;
namespace DevProxy.Plugins.Generation;
diff --git a/DevProxy.Plugins/Generation/TypeSpecGeneratorPlugin.cs b/DevProxy.Plugins/Generation/TypeSpecGeneratorPlugin.cs
index b95cee82..d166a27b 100644
--- a/DevProxy.Plugins/Generation/TypeSpecGeneratorPlugin.cs
+++ b/DevProxy.Plugins/Generation/TypeSpecGeneratorPlugin.cs
@@ -14,7 +14,7 @@
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Web;
-using Titanium.Web.Proxy.Http;
+using Unobtanium.Web.Proxy.Http;
namespace DevProxy.Plugins.Generation;
diff --git a/DevProxy.Plugins/Guidance/CachingGuidancePlugin.cs b/DevProxy.Plugins/Guidance/CachingGuidancePlugin.cs
index dc4fefe8..a9f08dad 100644
--- a/DevProxy.Plugins/Guidance/CachingGuidancePlugin.cs
+++ b/DevProxy.Plugins/Guidance/CachingGuidancePlugin.cs
@@ -6,7 +6,7 @@
using DevProxy.Abstractions.Plugins;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
-using Titanium.Web.Proxy.Http;
+using Unobtanium.Web.Proxy.Http;
namespace DevProxy.Plugins.Guidance;
diff --git a/DevProxy.Plugins/Guidance/GraphClientRequestIdGuidancePlugin.cs b/DevProxy.Plugins/Guidance/GraphClientRequestIdGuidancePlugin.cs
index 5e610009..b241f210 100644
--- a/DevProxy.Plugins/Guidance/GraphClientRequestIdGuidancePlugin.cs
+++ b/DevProxy.Plugins/Guidance/GraphClientRequestIdGuidancePlugin.cs
@@ -7,7 +7,7 @@
using DevProxy.Abstractions.Utils;
using DevProxy.Plugins.Utils;
using Microsoft.Extensions.Logging;
-using Titanium.Web.Proxy.Http;
+using Unobtanium.Web.Proxy.Http;
namespace DevProxy.Plugins.Guidance;
diff --git a/DevProxy.Plugins/Guidance/GraphSdkGuidancePlugin.cs b/DevProxy.Plugins/Guidance/GraphSdkGuidancePlugin.cs
index fae1070d..73d5b5bc 100644
--- a/DevProxy.Plugins/Guidance/GraphSdkGuidancePlugin.cs
+++ b/DevProxy.Plugins/Guidance/GraphSdkGuidancePlugin.cs
@@ -7,7 +7,7 @@
using DevProxy.Abstractions.Utils;
using DevProxy.Plugins.Utils;
using Microsoft.Extensions.Logging;
-using Titanium.Web.Proxy.Http;
+using Unobtanium.Web.Proxy.Http;
namespace DevProxy.Plugins.Guidance;
diff --git a/DevProxy.Plugins/Guidance/GraphSelectGuidancePlugin.cs b/DevProxy.Plugins/Guidance/GraphSelectGuidancePlugin.cs
index 9f41574e..e27d7528 100644
--- a/DevProxy.Plugins/Guidance/GraphSelectGuidancePlugin.cs
+++ b/DevProxy.Plugins/Guidance/GraphSelectGuidancePlugin.cs
@@ -8,7 +8,7 @@
using DevProxy.Abstractions.Utils;
using Microsoft.Extensions.Logging;
using System.Globalization;
-using Titanium.Web.Proxy.EventArguments;
+using Unobtanium.Web.Proxy.EventArguments;
namespace DevProxy.Plugins.Guidance;
diff --git a/DevProxy.Plugins/Guidance/ODSPSearchGuidancePlugin.cs b/DevProxy.Plugins/Guidance/ODSPSearchGuidancePlugin.cs
index 7cdb52b2..a4d282ff 100644
--- a/DevProxy.Plugins/Guidance/ODSPSearchGuidancePlugin.cs
+++ b/DevProxy.Plugins/Guidance/ODSPSearchGuidancePlugin.cs
@@ -6,7 +6,7 @@
using DevProxy.Abstractions.Plugins;
using DevProxy.Abstractions.Utils;
using Microsoft.Extensions.Logging;
-using Titanium.Web.Proxy.EventArguments;
+using Unobtanium.Web.Proxy.EventArguments;
namespace DevProxy.Plugins.Guidance;
diff --git a/DevProxy.Plugins/Inspection/DevToolsPlugin.cs b/DevProxy.Plugins/Inspection/DevToolsPlugin.cs
index fbd50491..a1feb04a 100644
--- a/DevProxy.Plugins/Inspection/DevToolsPlugin.cs
+++ b/DevProxy.Plugins/Inspection/DevToolsPlugin.cs
@@ -447,7 +447,7 @@ private static int GetFreePort()
return port;
}
- private static string GetRequestId(Titanium.Web.Proxy.Http.Request? request)
+ private static string GetRequestId(Unobtanium.Web.Proxy.Http.Request? request)
{
if (request is null)
{
diff --git a/DevProxy.Plugins/Inspection/OpenAITelemetryPlugin.cs b/DevProxy.Plugins/Inspection/OpenAITelemetryPlugin.cs
index 7707cdf5..2b7edc01 100644
--- a/DevProxy.Plugins/Inspection/OpenAITelemetryPlugin.cs
+++ b/DevProxy.Plugins/Inspection/OpenAITelemetryPlugin.cs
@@ -19,7 +19,7 @@
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Text.Json;
-using Titanium.Web.Proxy.Http;
+using Unobtanium.Web.Proxy.Http;
namespace DevProxy.Plugins.Inspection;
diff --git a/DevProxy.Plugins/Mocking/AuthPlugin.cs b/DevProxy.Plugins/Mocking/AuthPlugin.cs
index d688cd1f..5d7b927d 100644
--- a/DevProxy.Plugins/Mocking/AuthPlugin.cs
+++ b/DevProxy.Plugins/Mocking/AuthPlugin.cs
@@ -17,9 +17,9 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Web;
-using Titanium.Web.Proxy.EventArguments;
-using Titanium.Web.Proxy.Http;
-using Titanium.Web.Proxy.Models;
+using Unobtanium.Web.Proxy.EventArguments;
+using Unobtanium.Web.Proxy.Http;
+using Unobtanium.Web.Proxy.Models;
namespace DevProxy.Plugins.Mocking;
diff --git a/DevProxy.Plugins/Mocking/CrudApiPlugin.cs b/DevProxy.Plugins/Mocking/CrudApiPlugin.cs
index 979b14e6..6e87087a 100644
--- a/DevProxy.Plugins/Mocking/CrudApiPlugin.cs
+++ b/DevProxy.Plugins/Mocking/CrudApiPlugin.cs
@@ -19,9 +19,9 @@
using System.Security.Claims;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
-using Titanium.Web.Proxy.EventArguments;
-using Titanium.Web.Proxy.Http;
-using Titanium.Web.Proxy.Models;
+using Unobtanium.Web.Proxy.EventArguments;
+using Unobtanium.Web.Proxy.Http;
+using Unobtanium.Web.Proxy.Models;
namespace DevProxy.Plugins.Mocking;
diff --git a/DevProxy.Plugins/Mocking/GraphMockResponsePlugin.cs b/DevProxy.Plugins/Mocking/GraphMockResponsePlugin.cs
index d3d0f717..0bfccb0e 100644
--- a/DevProxy.Plugins/Mocking/GraphMockResponsePlugin.cs
+++ b/DevProxy.Plugins/Mocking/GraphMockResponsePlugin.cs
@@ -12,7 +12,7 @@
using System.Net;
using System.Text.Json;
using System.Text.RegularExpressions;
-using Titanium.Web.Proxy.Models;
+using Unobtanium.Web.Proxy.Models;
namespace DevProxy.Plugins.Mocking;
diff --git a/DevProxy.Plugins/Mocking/MockResponsePlugin.cs b/DevProxy.Plugins/Mocking/MockResponsePlugin.cs
index 033203aa..0a7f3dc3 100644
--- a/DevProxy.Plugins/Mocking/MockResponsePlugin.cs
+++ b/DevProxy.Plugins/Mocking/MockResponsePlugin.cs
@@ -21,8 +21,8 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
-using Titanium.Web.Proxy.Http;
-using Titanium.Web.Proxy.Models;
+using Unobtanium.Web.Proxy.Http;
+using Unobtanium.Web.Proxy.Models;
namespace DevProxy.Plugins.Mocking;
diff --git a/DevProxy.Plugins/Mocking/OpenAIMockResponsePlugin.cs b/DevProxy.Plugins/Mocking/OpenAIMockResponsePlugin.cs
index 464c5ac9..210935a6 100644
--- a/DevProxy.Plugins/Mocking/OpenAIMockResponsePlugin.cs
+++ b/DevProxy.Plugins/Mocking/OpenAIMockResponsePlugin.cs
@@ -9,7 +9,7 @@
using Microsoft.Extensions.Logging;
using System.Net;
using System.Text.Json;
-using Titanium.Web.Proxy.Models;
+using Unobtanium.Web.Proxy.Models;
namespace DevProxy.Plugins.Mocking;
diff --git a/DevProxy.Plugins/Utils/GraphUtils.cs b/DevProxy.Plugins/Utils/GraphUtils.cs
index 27ef95b6..ccd2ecb2 100644
--- a/DevProxy.Plugins/Utils/GraphUtils.cs
+++ b/DevProxy.Plugins/Utils/GraphUtils.cs
@@ -5,7 +5,7 @@
using DevProxy.Plugins.Models;
using Microsoft.Extensions.Logging;
using System.Net.Http.Json;
-using Titanium.Web.Proxy.Http;
+using Unobtanium.Web.Proxy.Http;
namespace DevProxy.Plugins.Utils;
diff --git a/DevProxy.Plugins/packages.lock.json b/DevProxy.Plugins/packages.lock.json
index 10eb0fc6..1180752a 100644
--- a/DevProxy.Plugins/packages.lock.json
+++ b/DevProxy.Plugins/packages.lock.json
@@ -105,17 +105,6 @@
"Microsoft.IdentityModel.Tokens": "8.9.0"
}
},
- "Unobtanium.Web.Proxy": {
- "type": "Direct",
- "requested": "[0.1.5, )",
- "resolved": "0.1.5",
- "contentHash": "HiICGm0e44+i4aVHpLn+aphmSC2eQnDvlTttw1rE0hntOZKoLGRy37sydqqbRP1ZokMf3Mt0GEgSWxDwnucKGg==",
- "dependencies": {
- "BouncyCastle.Cryptography": "2.4.0",
- "Microsoft.Extensions.Logging.Abstractions": "8.0.1",
- "System.Runtime.CompilerServices.Unsafe": "6.0.0"
- }
- },
"Azure.Core": {
"type": "Transitive",
"resolved": "1.44.1",
@@ -554,6 +543,25 @@
"resolved": "4.5.4",
"contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg=="
},
+ "Unobtanium.Web.Proxy": {
+ "type": "Transitive",
+ "resolved": "0.9.0-beta.1",
+ "contentHash": "Ae/2f7v3T3NQkRknBhAHqBrY85nJHQr8lxRD/j8B3rMK4KbIlbGe3A6Hd71ponq+sNHnLxa3BPSg8pdldc9RHg==",
+ "dependencies": {
+ "BouncyCastle.Cryptography": "2.4.0",
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.3",
+ "System.Runtime.CompilerServices.Unsafe": "6.0.0",
+ "Unobtanium.Web.Proxy.Events": "0.9.0-beta.1"
+ }
+ },
+ "Unobtanium.Web.Proxy.Events": {
+ "type": "Transitive",
+ "resolved": "0.9.0-beta.1",
+ "contentHash": "ckgncJ95Tr1GCx3Kv3yJK3sANfJznKIHOqZebqR0w3JblHrNF2VLJ5ALA5YZ5CcO7QuqqXVnfkusapy7QJRFwA==",
+ "dependencies": {
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.1"
+ }
+ },
"YamlDotNet": {
"type": "Transitive",
"resolved": "16.3.0",
@@ -572,7 +580,7 @@
"Newtonsoft.Json.Schema": "[4.0.1, )",
"Scriban": "[6.2.1, )",
"System.CommandLine": "[2.0.0-beta5.25306.1, )",
- "Unobtanium.Web.Proxy": "[0.1.5, )",
+ "Unobtanium.Web.Proxy": "[0.9.0-beta.1, )",
"YamlDotNet": "[16.3.0, )"
}
}
diff --git a/DevProxy.sln b/DevProxy.sln
index 9a4c219d..b21d9fa7 100644
--- a/DevProxy.sln
+++ b/DevProxy.sln
@@ -7,6 +7,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevProxy", "DevProxy\DevPro
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1CC1EC7F-4839-43C8-9980-9BCB19609FA9}"
ProjectSection(SolutionItems) = preProject
+ .editorconfig = .editorconfig
LICENSE = LICENSE
README.md = README.md
settings.editorconfig = settings.editorconfig
diff --git a/DevProxy/ApiControllers/ProxyController.cs b/DevProxy/ApiControllers/ProxyController.cs
index cec82532..1b0c88ec 100644
--- a/DevProxy/ApiControllers/ProxyController.cs
+++ b/DevProxy/ApiControllers/ProxyController.cs
@@ -8,13 +8,14 @@
using System.ComponentModel.DataAnnotations;
using DevProxy.Proxy;
using DevProxy.Abstractions.Proxy;
+using Unobtanium.Web.Proxy;
namespace DevProxy.ApiControllers;
[ApiController]
[Route("[controller]")]
#pragma warning disable CA1515 // required for the API controller
-public sealed class ProxyController(IProxyStateController proxyStateController, IProxyConfiguration proxyConfiguration) : ControllerBase
+public sealed class ProxyController(IProxyStateController proxyStateController, IProxyConfiguration proxyConfiguration, ProxyServer proxyServer) : ControllerBase
#pragma warning restore CA1515
{
private readonly IProxyStateController _proxyStateController = proxyStateController;
@@ -114,7 +115,7 @@ public IActionResult GetRootCertificate([FromQuery][Required] string format)
return ValidationProblem(ModelState);
}
- var certificate = ProxyEngine.ProxyServer.CertificateManager.RootCertificate;
+ var certificate = proxyServer.CertificateManager.RootCertificate;
if (certificate == null)
{
var problemDetails = new ProblemDetails
diff --git a/DevProxy/Commands/CertCommand.cs b/DevProxy/Commands/CertCommand.cs
index 06c05240..0e745037 100644
--- a/DevProxy/Commands/CertCommand.cs
+++ b/DevProxy/Commands/CertCommand.cs
@@ -3,28 +3,30 @@
// See the LICENSE file in the project root for more information.
using DevProxy.Abstractions.Utils;
-using DevProxy.Proxy;
using System.CommandLine;
using System.CommandLine.Parsing;
using System.Diagnostics;
-using Titanium.Web.Proxy.Helpers;
+using Unobtanium.Web.Proxy;
+using Unobtanium.Web.Proxy.Helpers;
namespace DevProxy.Commands;
sealed class CertCommand : Command
{
private readonly ILogger _logger;
+ private readonly ProxyServer _server;
private readonly Option _forceOption = new("--force", "-f")
{
Description = "Don't prompt for confirmation when removing the certificate"
};
- public CertCommand(ILogger logger) :
+ public CertCommand(ILogger logger, ProxyServer server) :
base("cert", "Manage the Dev Proxy certificate")
{
_logger = logger;
ConfigureCommand();
+ _server = server;
}
private void ConfigureCommand()
@@ -50,7 +52,7 @@ private async Task EnsureCertAsync()
try
{
_logger.LogInformation("Ensuring certificate exists and is trusted...");
- await ProxyEngine.ProxyServer.CertificateManager.EnsureRootCertificateAsync();
+ await _server.CertificateManager.EnsureRootCertificateAsync();
_logger.LogInformation("DONE");
}
catch (Exception ex)
@@ -80,7 +82,7 @@ public void RemoveCert(ParseResult parseResult)
_logger.LogInformation("Uninstalling the root certificate...");
RemoveTrustedCertificateOnMac();
- ProxyEngine.ProxyServer.CertificateManager.RemoveTrustedRootCertificate(machineTrusted: false);
+ _server.CertificateManager.RemoveTrustedRootCertificate(machineTrusted: false);
_logger.LogInformation("DONE");
}
diff --git a/DevProxy/DevProxy.csproj b/DevProxy/DevProxy.csproj
index d3b5c297..52df788f 100644
--- a/DevProxy/DevProxy.csproj
+++ b/DevProxy/DevProxy.csproj
@@ -1,4 +1,4 @@
-
+
Exe
@@ -15,7 +15,7 @@
README.md
false
true
- true
+ false
AllEnabledByDefault
@@ -32,23 +32,24 @@
-
+
-
-
+
-
-
+
+
+
+
-
+
diff --git a/DevProxy/Extensions/ILoggingBuilderExtensions.cs b/DevProxy/Extensions/ILoggingBuilderExtensions.cs
index 24b40eb4..6bee3454 100644
--- a/DevProxy/Extensions/ILoggingBuilderExtensions.cs
+++ b/DevProxy/Extensions/ILoggingBuilderExtensions.cs
@@ -4,6 +4,8 @@
using DevProxy.Commands;
using DevProxy.Logging;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Logging.Console;
#pragma warning disable IDE0130
namespace Microsoft.Extensions.Logging;
@@ -13,7 +15,7 @@ static class ILoggingBuilderExtensions
{
public static ILoggingBuilder AddRequestLogger(this ILoggingBuilder builder)
{
- _ = builder.Services.AddSingleton();
+ builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton());
return builder;
}
@@ -27,6 +29,11 @@ public static ILoggingBuilder ConfigureDevProxyLogging(
configuration.GetValue("logLevel", LogLevel.Information);
_ = builder
+ .AddOpenTelemetry(config =>
+ {
+ config.IncludeFormattedMessage = true;
+ config.IncludeScopes = true;
+ })
.AddFilter("Microsoft.Hosting.*", LogLevel.Error)
.AddFilter("Microsoft.AspNetCore.*", LogLevel.Error)
.AddFilter("Microsoft.Extensions.*", LogLevel.Error)
@@ -49,7 +56,8 @@ public static ILoggingBuilder ConfigureDevProxyLogging(
}
)
.AddRequestLogger()
- .SetMinimumLevel(configuredLogLevel);
+ .SetMinimumLevel(configuredLogLevel)
+ ;
return builder;
}
diff --git a/DevProxy/Extensions/IServiceCollectionExtensions.cs b/DevProxy/Extensions/IServiceCollectionExtensions.cs
index 98bed44a..1c07d7ce 100644
--- a/DevProxy/Extensions/IServiceCollectionExtensions.cs
+++ b/DevProxy/Extensions/IServiceCollectionExtensions.cs
@@ -8,6 +8,11 @@
using DevProxy.Abstractions.Proxy;
using DevProxy.Commands;
using DevProxy.Proxy;
+using OpenTelemetry;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Trace;
+using Unobtanium.Web.Proxy;
+using Unobtanium.Web.Proxy.Models;
#pragma warning disable IDE0130
namespace Microsoft.Extensions.DependencyInjection;
@@ -22,6 +27,7 @@ public static IServiceCollection ConfigureDevProxyServices(
{
_ = services.AddControllers();
_ = services
+ .AddOpenTelemetryConfig(configuration)
.AddApplicationServices(configuration, options)
.AddHostedService()
.AddEndpointsApiExplorer()
@@ -37,11 +43,16 @@ static IServiceCollection AddApplicationServices(
DevProxyConfigOptions options)
{
_ = services
+ .AddProxyHttpClientFactory()
+ .AddProxyConfiguration(configuration)
+ .AddSingleton() // ProxyServer has to be injected
.AddSingleton((IConfigurationRoot)configuration)
.AddSingleton()
.AddSingleton()
.AddSingleton()
- .AddSingleton(sp => ProxyEngine.Certificate!)
+ // TODO: Removed the injected certificate
+ //.AddSingleton(sp => ProxyEngine.Certificate!) // Why is this injected?
+ .AddSingleton(sp => sp.GetRequiredService().CertificateManager.RootCertificate!)
.AddSingleton(sp => LanguageModelClientFactory.Create(sp, configuration))
.AddSingleton()
.AddSingleton()
@@ -53,4 +64,71 @@ static IServiceCollection AddApplicationServices(
return services;
}
+
+ static IServiceCollection AddProxyHttpClientFactory(this IServiceCollection services)
+ {
+ _ = services.AddHttpClient(EfficientProxyHttpClientFactory.HTTP_CLIENT_NAME, client =>
+ {
+ // Configure the HttpClient as needed, e.g., set base address, default headers, etc.
+ //client.BaseAddress = new Uri("https://graph.microsoft.com/");
+ })
+ .ConfigurePrimaryHttpMessageHandler(() =>
+ {
+ // Configure HttpClientHandler to explicitly bypass system proxy settings
+ return new HttpClientHandler()
+ {
+ UseProxy = false, // Explicitly disable proxy usage
+ Proxy = null // Ensure no proxy is set
+ };
+ });
+ _ = services.AddTransient();
+ return services;
+ }
+
+ static IServiceCollection AddProxyConfiguration(this IServiceCollection services, IConfigurationRoot configuration)
+ {
+ var ipAddressString = configuration.GetValue("IPAddress");
+ var ipAddress = string.IsNullOrEmpty(ipAddressString) ? System.Net.IPAddress.Any : System.Net.IPAddress.Parse(ipAddressString);
+ var proxyConfig = new ProxyServerConfiguration
+ {
+ TcpTimeWaitSeconds = 10,
+ ConnectionTimeOutSeconds = 10,
+ ReuseSocket = false,
+ EnableConnectionPool = true,
+ ForwardToUpstreamGateway = true,
+ CertificateTrustMode = ProxyCertificateTrustMode.UserTrust,
+ // Default endpoint? Load port from config?
+ EndPoints = [new ExplicitProxyEndPoint(ipAddress, configuration.GetValue("Port", 8000))],
+ CertificateCacheFolder = configuration.GetValue("DEV_PROXY_CERT_PATH"), // By default the configuration also has environment variables. Loading this from config makes it more flexible.
+ RootCertificateName = "Dev Proxy CA",
+ };
+ _ = services.AddSingleton(proxyConfig);
+ return services;
+ }
+
+ static IServiceCollection AddOpenTelemetryConfig(this IServiceCollection services, IConfigurationRoot configuration)
+ {
+ var openTelemetryBuilder = services
+ .AddOpenTelemetry()
+ .WithMetrics(metrics =>
+ {
+ _ = metrics
+ .AddHttpClientInstrumentation()
+ .AddRuntimeInstrumentation();
+
+ }).WithTracing(tracing =>
+ {
+ _ = tracing
+ .AddSource(ProxyServerDefaults.ActivitySourceName)
+ .AddSource(ProxyEngine.ACTIVITY_SOURCE_NAME)
+ .AddHttpClientInstrumentation()
+ ;
+ });
+ var endpoint = configuration.GetValue("OTEL_EXPORTER_OTLP_ENDPOINT");
+ if (!string.IsNullOrEmpty(endpoint))
+ {
+ _ = openTelemetryBuilder.UseOtlpExporter();
+ }
+ return services;
+ }
}
\ No newline at end of file
diff --git a/DevProxy/Logging/ILoggerExtensions.cs b/DevProxy/Logging/ILoggerExtensions.cs
index 53f1870c..96505787 100644
--- a/DevProxy/Logging/ILoggerExtensions.cs
+++ b/DevProxy/Logging/ILoggerExtensions.cs
@@ -15,4 +15,7 @@ static class ILoggerExtensions
{ nameof(url), url },
{ nameof(requestId), requestId }
});
+
+ public static IDisposable? BeginRequestScope(this ILogger logger, HttpMethod method, Uri url, int requestId) => logger.BeginScope(method.Method!, url.ToString(), requestId);
+
}
\ No newline at end of file
diff --git a/DevProxy/Properties/launchSettings.json b/DevProxy/Properties/launchSettings.json
index 30111fdf..2937063a 100755
--- a/DevProxy/Properties/launchSettings.json
+++ b/DevProxy/Properties/launchSettings.json
@@ -2,7 +2,9 @@
"profiles": {
"No args": {
"commandName": "Project",
- "commandLineArgs": "",
+ "environmentVariables": {
+ "AsSystemProxy": "false"
+ },
"hotReloadEnabled": true
},
"No chaos with mock responses": {
@@ -32,7 +34,12 @@
},
"Default": {
"commandName": "Project",
- "hotReloadEnabled": true
+ "hotReloadEnabled": true,
+ "environmentVariables": {
+ "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317",
+ "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
+ "AsSystemProxy": "false"
+ }
},
"Missing arg": {
"commandName": "Project",
diff --git a/DevProxy/Proxy/CertificateDiskCache.cs b/DevProxy/Proxy/CertificateDiskCache.cs
index e7442a63..efa91085 100644
--- a/DevProxy/Proxy/CertificateDiskCache.cs
+++ b/DevProxy/Proxy/CertificateDiskCache.cs
@@ -3,8 +3,8 @@
// See the LICENSE file in the project root for more information.
using System.Security.Cryptography.X509Certificates;
-using Titanium.Web.Proxy.Certificates.Cache;
-using Titanium.Web.Proxy.Helpers;
+using Unobtanium.Web.Proxy.Certificates.Cache;
+using Unobtanium.Web.Proxy.Helpers;
namespace DevProxy.Proxy;
diff --git a/DevProxy/Proxy/EfficientProxyHttpClientFactory.cs b/DevProxy/Proxy/EfficientProxyHttpClientFactory.cs
new file mode 100644
index 00000000..7f70364e
--- /dev/null
+++ b/DevProxy/Proxy/EfficientProxyHttpClientFactory.cs
@@ -0,0 +1,15 @@
+using Unobtanium.Web.Proxy;
+
+namespace DevProxy.Proxy;
+
+///
+/// for efficient re-use of available ports
+///
+/// Is added to Dependency Injection
+internal sealed class EfficientProxyHttpClientFactory(IHttpClientFactory httpClientFactory) : IProxyServerHttpClientFactory
+{
+ internal const string HTTP_CLIENT_NAME = "DevProxy.Proxy.EfficientHttpClient";
+ private readonly IHttpClientFactory _httpClientFactory = httpClientFactory;
+
+ public HttpClient CreateHttpClient() => _httpClientFactory.CreateClient(HTTP_CLIENT_NAME);
+}
diff --git a/DevProxy/Proxy/ProxyEngine.cs b/DevProxy/Proxy/ProxyEngine.cs
index 04c1498c..9d7258ab 100755
--- a/DevProxy/Proxy/ProxyEngine.cs
+++ b/DevProxy/Proxy/ProxyEngine.cs
@@ -5,17 +5,16 @@
using DevProxy.Abstractions.Plugins;
using DevProxy.Abstractions.Proxy;
using DevProxy.Abstractions.Utils;
-using Microsoft.VisualStudio.Threading;
using System.Collections.Concurrent;
using System.Diagnostics;
-using System.Net;
-using System.Security.Cryptography.X509Certificates;
using System.Text.RegularExpressions;
-using Titanium.Web.Proxy;
-using Titanium.Web.Proxy.EventArguments;
-using Titanium.Web.Proxy.Helpers;
-using Titanium.Web.Proxy.Http;
-using Titanium.Web.Proxy.Models;
+using Unobtanium.Web.Proxy;
+using Unobtanium.Web.Proxy.Events;
+using Unobtanium.Web.Proxy.EventArguments;
+using Unobtanium.Web.Proxy.Helpers;
+using Unobtanium.Web.Proxy.Http;
+using Unobtanium.Web.Proxy.Models;
+using Org.BouncyCastle.Asn1.X509.Qualified;
namespace DevProxy.Proxy;
@@ -30,14 +29,18 @@ sealed class ProxyEngine(
IProxyConfiguration proxyConfiguration,
ISet urlsToWatch,
IProxyStateController proxyController,
- ILogger logger) : BackgroundService, IDisposable
+ ILogger logger,
+ ProxyServer proxyServer,
+ ProxyServerConfiguration proxyServerConfiguration) : BackgroundService, IDisposable
{
+ internal const string ACTIVITY_SOURCE_NAME = "DevProxy.Proxy.ProxyEngine";
+ public static readonly ActivitySource ActivitySource = new(ACTIVITY_SOURCE_NAME);
private readonly IEnumerable _plugins = plugins;
private readonly ILogger _logger = logger;
private readonly IProxyConfiguration _config = proxyConfiguration;
- internal static ProxyServer ProxyServer { get; private set; }
- private ExplicitProxyEndPoint? _explicitEndPoint;
+ //internal static ProxyServer ProxyServer { get; private set; }
+ //private ExplicitProxyEndPoint? _explicitEndPoint;
// lists of URLs to watch, used for intercepting requests
private readonly ISet _urlsToWatch = urlsToWatch;
// lists of hosts to watch extracted from urlsToWatch,
@@ -50,30 +53,30 @@ sealed class ProxyEngine(
private InactivityTimer? _inactivityTimer;
private CancellationToken? _cancellationToken;
- public static X509Certificate2? Certificate => ProxyServer?.CertificateManager.RootCertificate;
+ //public static X509Certificate2? Certificate => proxyServer?.CertificateManager.RootCertificate;
private ExceptionHandler ExceptionHandler => ex => _logger.LogError(ex, "An error occurred in a plugin");
- static ProxyEngine()
- {
- ProxyServer = new();
- ProxyServer.CertificateManager.PfxFilePath = Environment.GetEnvironmentVariable("DEV_PROXY_CERT_PATH") ?? string.Empty;
- ProxyServer.CertificateManager.RootCertificateName = "Dev Proxy CA";
- ProxyServer.CertificateManager.CertificateStorage = new CertificateDiskCache();
- // we need to change this to a value lower than 397
- // to avoid the ERR_CERT_VALIDITY_TOO_LONG error in Edge
- ProxyServer.CertificateManager.CertificateValidDays = 365;
-
- using var joinableTaskContext = new JoinableTaskContext();
- var joinableTaskFactory = new JoinableTaskFactory(joinableTaskContext);
- _ = joinableTaskFactory.Run(async () => await ProxyServer.CertificateManager.LoadOrCreateRootCertificateAsync());
- }
+ //static ProxyEngine()
+ //{
+ // ProxyServer = new();
+ // ProxyServer.CertificateManager.PfxFilePath = Environment.GetEnvironmentVariable("DEV_PROXY_CERT_PATH") ?? string.Empty;
+ // ProxyServer.CertificateManager.RootCertificateName = "Dev Proxy CA";
+ // ProxyServer.CertificateManager.CertificateStorage = new CertificateDiskCache();
+ // // we need to change this to a value lower than 397
+ // // to avoid the ERR_CERT_VALIDITY_TOO_LONG error in Edge
+ // ProxyServer.CertificateManager.CertificateValidDays = 365;
+
+ // using var joinableTaskContext = new JoinableTaskContext();
+ // var joinableTaskFactory = new JoinableTaskFactory(joinableTaskContext);
+ // _ = joinableTaskFactory.Run(async () => await ProxyServer.CertificateManager.LoadOrCreateRootCertificateAsync());
+ //}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_cancellationToken = stoppingToken;
- Debug.Assert(ProxyServer is not null, "Proxy server is not initialized");
+ Debug.Assert(proxyServer is not null, "Proxy server is not initialized");
if (!_urlsToWatch.Any())
{
@@ -83,44 +86,64 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
LoadHostNamesFromUrls();
- ProxyServer.BeforeRequest += OnRequestAsync;
- ProxyServer.BeforeResponse += OnBeforeResponseAsync;
- ProxyServer.AfterResponse += OnAfterResponseAsync;
- ProxyServer.ServerCertificateValidationCallback += OnCertificateValidationAsync;
- ProxyServer.ClientCertificateSelectionCallback += OnCertificateSelectionAsync;
+ // TODO: Handle replacement of BeforeRequest
+ //proxyServer.BeforeRequest += OnRequestAsync;
+ proxyServerConfiguration.Events.OnRequest += OnRequestAsync;
+
+ // TODO: Handle removal of BeforeResponse
+ //proxyServer.BeforeResponse += OnBeforeResponseAsync;
+
+ // TODO: Handle replacement of AfterResponse
+ //proxyServer.AfterResponse += OnAfterResponseAsync;
+ proxyServerConfiguration.Events.OnResponse += OnResponseAsync;
- var ipAddress = string.IsNullOrEmpty(_config.IPAddress) ? IPAddress.Any : IPAddress.Parse(_config.IPAddress);
- _explicitEndPoint = new(ipAddress, _config.Port, true);
+ proxyServer.ServerCertificateValidationCallback += OnCertificateValidationAsync;
+ proxyServer.ClientCertificateSelectionCallback += OnCertificateSelectionAsync;
+
+ // Endpoint is configured in IServiceCollectionExtensions.AddProxyConfiguration
+ //var ipAddress = string.IsNullOrEmpty(_config.IPAddress) ? IPAddress.Any : IPAddress.Parse(_config.IPAddress);
+ //_explicitEndPoint = new(ipAddress, _config.Port, true);
+
+ // TODO: Implement process validation
// Fired when a CONNECT request is received
- _explicitEndPoint.BeforeTunnelConnectRequest += OnBeforeTunnelConnectRequestAsync;
+ //_explicitEndPoint.BeforeTunnelConnectRequest += OnBeforeTunnelConnectRequestAsync;
+ // This is superceeded by:
+ proxyServerConfiguration.Events.ShouldDecryptNewConnection = (host, cts) => Task.FromResult(IsProxiedHost(host));// || IsProxiedProcess(...));
if (_config.InstallCert)
{
- await ProxyServer.CertificateManager.EnsureRootCertificateAsync(stoppingToken);
+ await proxyServer.CertificateManager.EnsureRootCertificateAsync(stoppingToken);
}
else
{
- _explicitEndPoint.GenericCertificate = await ProxyServer
- .CertificateManager
- .LoadRootCertificateAsync(stoppingToken);
+ // TODO: Remove this code, happens automatically
+ //_explicitEndPoint.GenericCertificate = await proxyServer
+ // .CertificateManager
+ // .LoadRootCertificateAsync(stoppingToken);
}
- ProxyServer.AddEndPoint(_explicitEndPoint);
- await ProxyServer.StartAsync(cancellationToken: stoppingToken);
+ //proxyServer.AddEndPoint(_explicitEndPoint);
+ await proxyServer.StartAsync(cancellationToken: stoppingToken);
// run first-run setup on macOS
FirstRunSetup();
- foreach (var endPoint in ProxyServer.ProxyEndPoints)
+ ExplicitProxyEndPoint? explicitProxyEndPoint = null;
+
+ foreach (var endPoint in proxyServer.ProxyEndPoints)
{
_logger.LogInformation("Dev Proxy listening on {IPAddress}:{Port}...", endPoint.IpAddress, endPoint.Port);
+ if (explicitProxyEndPoint is null && endPoint is ExplicitProxyEndPoint explicitProxyEnd)
+ {
+ explicitProxyEndPoint = explicitProxyEnd;
+ }
}
if (_config.AsSystemProxy)
{
if (RunTime.IsWindows)
{
- ProxyServer.SetAsSystemHttpProxy(_explicitEndPoint);
- ProxyServer.SetAsSystemHttpsProxy(_explicitEndPoint);
+ proxyServer.SetAsSystemHttpProxy(explicitProxyEndPoint!);
+ proxyServer.SetAsSystemHttpsProxy(explicitProxyEndPoint!);
}
else if (RunTime.IsMac)
{
@@ -162,7 +185,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
try
{
- while (!stoppingToken.IsCancellationRequested && ProxyServer.ProxyRunning)
+ while (!stoppingToken.IsCancellationRequested && proxyServer.ProxyRunning)
{
while (!Console.KeyAvailable)
{
@@ -294,22 +317,22 @@ private void StopProxy()
// Unsubscribe & Quit
try
{
- if (_explicitEndPoint != null)
- {
- _explicitEndPoint.BeforeTunnelConnectRequest -= OnBeforeTunnelConnectRequestAsync;
- }
+ //if (_explicitEndPoint != null)
+ //{
+ // _explicitEndPoint.BeforeTunnelConnectRequest -= OnBeforeTunnelConnectRequestAsync;
+ //}
- if (ProxyServer is not null)
+ if (proxyServer is not null) // Irrelevant is always defined
{
- ProxyServer.BeforeRequest -= OnRequestAsync;
- ProxyServer.BeforeResponse -= OnBeforeResponseAsync;
- ProxyServer.AfterResponse -= OnAfterResponseAsync;
- ProxyServer.ServerCertificateValidationCallback -= OnCertificateValidationAsync;
- ProxyServer.ClientCertificateSelectionCallback -= OnCertificateSelectionAsync;
+ //ProxyServer.BeforeRequest -= OnRequestAsync;
+ //ProxyServer.BeforeResponse -= OnBeforeResponseAsync;
+ //ProxyServer.AfterResponse -= OnAfterResponseAsync;
+ proxyServer.ServerCertificateValidationCallback -= OnCertificateValidationAsync;
+ proxyServer.ClientCertificateSelectionCallback -= OnCertificateSelectionAsync;
- if (ProxyServer.ProxyRunning)
+ if (proxyServer.ProxyRunning)
{
- ProxyServer.Stop();
+ proxyServer.Stop();
}
}
@@ -419,6 +442,37 @@ async Task OnRequestAsync(object sender, SessionEventArgs e)
}
}
+ // Unobtanium Request handler
+ private async Task OnRequestAsync(object sender, RequestEventArguments e, CancellationToken cancellationToken)
+ {
+ // Distributed tracing
+ using var activity = ActivitySource.StartActivity(nameof(OnRequestAsync), ActivityKind.Server, e.RequestActivity?.Context ?? default);
+ var uri = e.Request.RequestUri!;
+ var hashCode = e.RequestActivity?.GetHashCode() ?? e.Request.GetHashCode();
+ if (IsProxiedHost(uri.Host)) // && IsIncludedByHeaders??
+ {
+
+ if (!_pluginData.TryAdd(hashCode, []))
+ {
+ // Throwing here will break the request....
+ throw new InvalidOperationException($"Unable to initialize the plugin data storage for hash key {hashCode}");
+ }
+
+ // If I start this scope I no longer see logs appear
+ using var scope = _logger.BeginRequestScope(e.Request.Method, uri, hashCode);
+
+ _logger.LogInformation("OnRequestAsync {method} {url}", e.Request.Method, uri);
+
+ // This is definity not using structured logging...
+ _logger.LogRequest($"{e.Request.Method} {uri}", MessageType.InterceptedRequest, e.Request.Method.Method, uri.ToString()); //Logging context?
+
+ // Execute the required plugins here and decide what to return.
+ //return RequestEventResponse.EarlyResponse(new HttpResponseMessage(System.Net.HttpStatusCode.UnavailableForLegalReasons));
+ }
+ _logger.LogRequest("Done", MessageType.FinishedProcessingRequest);
+ return RequestEventResponse.ContinueResponse();
+ }
+
private async Task HandleRequestAsync(SessionEventArgs e, ProxyRequestArgs proxyRequestArgs)
{
foreach (var plugin in _plugins.Where(p => p.Enabled))
@@ -445,6 +499,7 @@ private async Task HandleRequestAsync(SessionEventArgs e, ProxyRequestArgs proxy
private bool IsProxiedHost(string hostName)
{
+ return true;
var urlMatch = _hostsToWatch.FirstOrDefault(h => h.Url.IsMatch(hostName));
return urlMatch is not null && !urlMatch.Exclude;
}
@@ -488,6 +543,7 @@ private bool IsIncludedByHeaders(HeaderCollection requestHeaders)
}
// Modify response
+ // OnBeforeResponseAsync is no longer supported, where was this used for?
async Task OnBeforeResponseAsync(object sender, SessionEventArgs e)
{
// read response headers
@@ -527,6 +583,7 @@ async Task OnBeforeResponseAsync(object sender, SessionEventArgs e)
}
}
}
+
async Task OnAfterResponseAsync(object sender, SessionEventArgs e)
{
// read response headers
@@ -574,6 +631,19 @@ async Task OnAfterResponseAsync(object sender, SessionEventArgs e)
_ = _pluginData.Remove(e.GetHashCode(), out _);
}
}
+
+ // Unobtanium ResponseHandler
+ private async Task OnResponseAsync(object sender, ResponseEventArguments e, CancellationToken cancellationToken)
+ {
+ // Distributed tracing
+ using var activity = ActivitySource.StartActivity(nameof(OnResponseAsync), ActivityKind.Consumer, e.RequestActivity?.Context ?? default);
+ var uri = e.Request.RequestUri!;
+ if (IsProxiedHost(uri.Host)) // This is already checked but lets mimic the existing
+ {
+
+ }
+ return ResponseEventResponse.ContinueResponse();
+ }
// Allows overriding default certificate validation logic
Task OnCertificateValidationAsync(object sender, CertificateValidationEventArgs e)
diff --git a/DevProxy/Proxy/ProxyStateController.cs b/DevProxy/Proxy/ProxyStateController.cs
index 1f418abe..afe5481b 100644
--- a/DevProxy/Proxy/ProxyStateController.cs
+++ b/DevProxy/Proxy/ProxyStateController.cs
@@ -4,7 +4,7 @@
using DevProxy.Abstractions.Plugins;
using DevProxy.Abstractions.Proxy;
-using Titanium.Web.Proxy;
+using Unobtanium.Web.Proxy;
namespace DevProxy.Proxy;
diff --git a/DevProxy/packages.lock.json b/DevProxy/packages.lock.json
index 69c28690..8601bc87 100644
--- a/DevProxy/packages.lock.json
+++ b/DevProxy/packages.lock.json
@@ -4,15 +4,14 @@
"net9.0": {
"Azure.Identity": {
"type": "Direct",
- "requested": "[1.13.2, )",
- "resolved": "1.13.2",
- "contentHash": "CngQVQELdzFmsGSWyGIPIUOCrII7nApMVWxVmJCKQQrWxRXcNquCsZ+njRJRnhFUfD+KMAhpjyRCaceE4EOL6A==",
+ "requested": "[1.14.2, )",
+ "resolved": "1.14.2",
+ "contentHash": "YhNMwOTwT+I2wIcJKSdP0ADyB2aK+JaYWZxO8LSRDm5w77LFr0ykR9xmt2ZV5T1gaI7xU6iNFIh/yW1dAlpddQ==",
"dependencies": {
- "Azure.Core": "1.44.1",
- "Microsoft.Identity.Client": "4.67.2",
- "Microsoft.Identity.Client.Extensions.Msal": "4.67.2",
- "System.Memory": "4.5.5",
- "System.Threading.Tasks.Extensions": "4.5.4"
+ "Azure.Core": "1.46.1",
+ "Microsoft.Identity.Client": "4.73.1",
+ "Microsoft.Identity.Client.Extensions.Msal": "4.73.1",
+ "System.Memory": "4.5.5"
}
},
"Microsoft.EntityFrameworkCore.Sqlite": {
@@ -31,25 +30,6 @@
"System.Text.Json": "9.0.4"
}
},
- "Microsoft.Extensions.Configuration": {
- "type": "Direct",
- "requested": "[9.0.4, )",
- "resolved": "9.0.4",
- "contentHash": "KIVBrMbItnCJDd1RF4KEaE8jZwDJcDUJW5zXpbwQ05HNYTK1GveHxHK0B3SjgDJuR48GRACXAO+BLhL8h34S7g==",
- "dependencies": {
- "Microsoft.Extensions.Configuration.Abstractions": "9.0.4",
- "Microsoft.Extensions.Primitives": "9.0.4"
- }
- },
- "Microsoft.Extensions.Configuration.Binder": {
- "type": "Direct",
- "requested": "[9.0.4, )",
- "resolved": "9.0.4",
- "contentHash": "cdrjcl9RIcwt3ECbnpP0Gt1+pkjdW90mq5yFYy8D9qRj2NqFFcv3yDp141iEamsd9E218sGxK8WHaIOcrqgDJg==",
- "dependencies": {
- "Microsoft.Extensions.Configuration.Abstractions": "9.0.4"
- }
- },
"Microsoft.Extensions.Configuration.Json": {
"type": "Direct",
"requested": "[9.0.4, )",
@@ -68,6 +48,20 @@
"resolved": "9.0.6",
"contentHash": "1HJCAbwukNEoYbHgHbKHmenU0V/0huw8+i7Qtf5rLUG1E+3kEwRJQxpwD3wbTEagIgPSQisNgJTvmUX9yYVc6g=="
},
+ "Microsoft.Extensions.Http": {
+ "type": "Direct",
+ "requested": "[9.0.8, )",
+ "resolved": "9.0.8",
+ "contentHash": "jDj+4aDByk47oESlDDTtk6LWzlXlmoCsjCn6ihd+i9OntN885aPLszUII5+w0B/7wYSZcS3KdjqLAIhKLSiBXQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "9.0.8",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8",
+ "Microsoft.Extensions.Diagnostics": "9.0.8",
+ "Microsoft.Extensions.Logging": "9.0.8",
+ "Microsoft.Extensions.Logging.Abstractions": "9.0.8",
+ "Microsoft.Extensions.Options": "9.0.8"
+ }
+ },
"Microsoft.Extensions.Logging.Console": {
"type": "Direct",
"requested": "[9.0.4, )",
@@ -118,26 +112,45 @@
"resolved": "13.0.3",
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
},
- "OpenTelemetry": {
+ "OpenTelemetry.Exporter.OpenTelemetryProtocol": {
"type": "Direct",
"requested": "[1.12.0, )",
"resolved": "1.12.0",
- "contentHash": "aIEu2O3xFOdwIVH0AJsIHPIMH1YuX18nzu7BHyaDNQ6NWSk4Zyrs9Pp6y8SATuSbvdtmvue4mj/QZ3838srbwA==",
+ "contentHash": "7LzQSPhz5pNaL4xZgT3wkZODA1NLrEq3bet8KDHgtaJ9q+VNP7wmiZky8gQfMkB4FXuI/pevT8ZurL4p5997WA==",
"dependencies": {
- "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.0",
- "Microsoft.Extensions.Logging.Configuration": "9.0.0",
- "OpenTelemetry.Api.ProviderBuilderExtensions": "1.12.0"
+ "OpenTelemetry": "1.12.0"
}
},
- "OpenTelemetry.Exporter.OpenTelemetryProtocol": {
+ "OpenTelemetry.Extensions.Hosting": {
"type": "Direct",
"requested": "[1.12.0, )",
"resolved": "1.12.0",
- "contentHash": "7LzQSPhz5pNaL4xZgT3wkZODA1NLrEq3bet8KDHgtaJ9q+VNP7wmiZky8gQfMkB4FXuI/pevT8ZurL4p5997WA==",
+ "contentHash": "6/8O6rsJRwslg5/Fm3bscBelw4Yh9T9CN24p7cAsuEFkrmmeSO9gkYUCK02Qi+CmPM2KHYTLjKi0lJaCsDMWQA==",
"dependencies": {
+ "Microsoft.Extensions.Hosting.Abstractions": "9.0.0",
"OpenTelemetry": "1.12.0"
}
},
+ "OpenTelemetry.Instrumentation.Http": {
+ "type": "Direct",
+ "requested": "[1.12.0, )",
+ "resolved": "1.12.0",
+ "contentHash": "0rW+MbHgUQAdbvBtRxPYoQBosbNdWegL7cYkRlxq+KQ/VFyU8itt4pWTccmu1/FWmTgqJyT3LaujyDZoRrm8Yg==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "9.0.0",
+ "Microsoft.Extensions.Options": "9.0.0",
+ "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.12.0, 2.0.0)"
+ }
+ },
+ "OpenTelemetry.Instrumentation.Runtime": {
+ "type": "Direct",
+ "requested": "[1.12.0, )",
+ "resolved": "1.12.0",
+ "contentHash": "xmd0TAm2x+T3ztdf5BolIwLPh+Uy6osaBeIQtCXv611PN7h/Pnhsjg5lU2hkAWj7M7ns74U5wtVpS8DXmJ+94w==",
+ "dependencies": {
+ "OpenTelemetry.Api": "[1.12.0, 2.0.0)"
+ }
+ },
"Swashbuckle.AspNetCore": {
"type": "Direct",
"requested": "[8.1.1, )",
@@ -168,28 +181,24 @@
},
"Unobtanium.Web.Proxy": {
"type": "Direct",
- "requested": "[0.1.5, )",
- "resolved": "0.1.5",
- "contentHash": "HiICGm0e44+i4aVHpLn+aphmSC2eQnDvlTttw1rE0hntOZKoLGRy37sydqqbRP1ZokMf3Mt0GEgSWxDwnucKGg==",
+ "requested": "[0.9.0-beta.1, )",
+ "resolved": "0.9.0-beta.1",
+ "contentHash": "Ae/2f7v3T3NQkRknBhAHqBrY85nJHQr8lxRD/j8B3rMK4KbIlbGe3A6Hd71ponq+sNHnLxa3BPSg8pdldc9RHg==",
"dependencies": {
"BouncyCastle.Cryptography": "2.4.0",
- "Microsoft.Extensions.Logging.Abstractions": "8.0.1",
- "System.Runtime.CompilerServices.Unsafe": "6.0.0"
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.3",
+ "System.Runtime.CompilerServices.Unsafe": "6.0.0",
+ "Unobtanium.Web.Proxy.Events": "0.9.0-beta.1"
}
},
"Azure.Core": {
"type": "Transitive",
- "resolved": "1.44.1",
- "contentHash": "YyznXLQZCregzHvioip07/BkzjuWNXogJEVz9T5W6TwjNr17ax41YGzYMptlo2G10oLCuVPoyva62y0SIRDixg==",
+ "resolved": "1.46.1",
+ "contentHash": "iE5DPOlGsN5kCkF4gN+vasN1RihO0Ypie92oQ5tohQYiokmnrrhLnee+3zcE8n7vB6ZAzhPTfUGAEXX/qHGkYA==",
"dependencies": {
- "Microsoft.Bcl.AsyncInterfaces": "6.0.0",
- "System.ClientModel": "1.1.0",
- "System.Diagnostics.DiagnosticSource": "6.0.1",
- "System.Memory.Data": "6.0.0",
- "System.Numerics.Vectors": "4.5.0",
- "System.Text.Encodings.Web": "6.0.0",
- "System.Text.Json": "6.0.10",
- "System.Threading.Tasks.Extensions": "4.5.4"
+ "Microsoft.Bcl.AsyncInterfaces": "8.0.0",
+ "System.ClientModel": "1.4.1",
+ "System.Memory.Data": "6.0.1"
}
},
"BouncyCastle.Cryptography": {
@@ -204,8 +213,8 @@
},
"Microsoft.Bcl.AsyncInterfaces": {
"type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg=="
+ "resolved": "8.0.0",
+ "contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw=="
},
"Microsoft.Data.Sqlite.Core": {
"type": "Transitive",
@@ -287,12 +296,29 @@
"Microsoft.Extensions.Primitives": "9.0.4"
}
},
+ "Microsoft.Extensions.Configuration": {
+ "type": "Transitive",
+ "resolved": "9.0.8",
+ "contentHash": "6m+8Xgmf8UWL0p/oGqBM+0KbHE5/ePXbV1hKXgC59zEv0aa0DW5oiiyxDbK5kH5j4gIvyD5uWL0+HadKBJngvQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "9.0.8",
+ "Microsoft.Extensions.Primitives": "9.0.8"
+ }
+ },
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "0LN/DiIKvBrkqp7gkF3qhGIeZk6/B63PthAHjQsxymJfIBcz0kbf4/p/t4lMgggVxZ+flRi5xvTwlpPOoZk8fg==",
+ "resolved": "9.0.8",
+ "contentHash": "yNou2KM35RvzOh4vUFtl2l33rWPvOCoba+nzEDJ+BgD8aOL/jew4WPCibQvntRfOJ2pJU8ARygSMD+pdjvDHuA==",
"dependencies": {
- "Microsoft.Extensions.Primitives": "9.0.4"
+ "Microsoft.Extensions.Primitives": "9.0.8"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Binder": {
+ "type": "Transitive",
+ "resolved": "9.0.8",
+ "contentHash": "0vK9DnYrYChdiH3yRZWkkp4x4LbrfkWEdBc5HOsQ8t/0CLOWKXKkkhOE8A1shlex0hGydbGrhObeypxz/QTm+w==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "9.0.8"
}
},
"Microsoft.Extensions.Configuration.FileExtensions": {
@@ -309,29 +335,39 @@
},
"Microsoft.Extensions.DependencyInjection": {
"type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "f2MTUaS2EQ3lX4325ytPAISZqgBfXmY0WvgD80ji6Z20AoDNiCESxsqo6mFRwHJD/jfVKRw9FsW6+86gNre3ug==",
+ "resolved": "9.0.8",
+ "contentHash": "JJjI2Fa+QtZcUyuNjbKn04OjIUX5IgFGFu/Xc+qvzh1rXdZHLcnqqVXhR4093bGirTwacRlHiVg1XYI9xum6QQ==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8"
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "UI0TQPVkS78bFdjkTodmkH0Fe8lXv9LnhGFKgKrsgUJ5a5FVdFRcgjIkBVLbGgdRhxWirxH/8IXUtEyYJx6GQg=="
+ "resolved": "9.0.8",
+ "contentHash": "xY3lTjj4+ZYmiKIkyWitddrp1uL5uYiweQjqo4BKBw01ZC4HhcfgLghDpPZcUlppgWAFqFy9SgkiYWOMx365pw=="
},
"Microsoft.Extensions.DependencyModel": {
"type": "Transitive",
"resolved": "9.0.4",
"contentHash": "ACtnvl3H3M/f8Z42980JxsNu7V9PPbzys4vBs83ZewnsgKd7JeYK18OMPo0g+MxAHrpgMrjmlinXDiaSRPcVnA=="
},
+ "Microsoft.Extensions.Diagnostics": {
+ "type": "Transitive",
+ "resolved": "9.0.8",
+ "contentHash": "BKkLCFXzJvNmdngeYBf72VXoZqTJSb1orvjdzDLaGobicoGFBPW8ug2ru1nnEewMEwJzMgnsjHQY8EaKWmVhKg==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "9.0.8",
+ "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.8",
+ "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.8"
+ }
+ },
"Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive",
- "resolved": "9.0.0",
- "contentHash": "1K8P7XzuzX8W8pmXcZjcrqS6x5eSSdvhQohmcpgiQNY/HlDAlnrhR9dvlURfFz428A+RTCJpUyB+aKTA6AgVcQ==",
+ "resolved": "9.0.8",
+ "contentHash": "UDY7blv4DCyIJ/8CkNrQKLaAZFypXQavRZ2DWf/2zi1mxYYKKw2t8AOCBWxNntyPZHPGhtEmL3snFM98ADZqTw==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
- "Microsoft.Extensions.Options": "9.0.0"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8",
+ "Microsoft.Extensions.Options": "9.0.8"
}
},
"Microsoft.Extensions.FileProviders.Abstractions": {
@@ -352,22 +388,34 @@
"Microsoft.Extensions.Primitives": "9.0.4"
}
},
+ "Microsoft.Extensions.Hosting.Abstractions": {
+ "type": "Transitive",
+ "resolved": "9.0.0",
+ "contentHash": "yUKJgu81ExjvqbNWqZKshBbLntZMbMVz/P7Way2SBx7bMqA08Mfdc9O7hWDKAiSp+zPUGT6LKcSCQIPeDK+CCw==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "9.0.0",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
+ "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.0",
+ "Microsoft.Extensions.FileProviders.Abstractions": "9.0.0",
+ "Microsoft.Extensions.Logging.Abstractions": "9.0.0"
+ }
+ },
"Microsoft.Extensions.Logging": {
"type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "xW6QPYsqhbuWBO9/1oA43g/XPKbohJx+7G8FLQgQXIriYvY7s+gxr2wjQJfRoPO900dvvv2vVH7wZovG+M1m6w==",
+ "resolved": "9.0.8",
+ "contentHash": "Z/7ze+0iheT7FJeZPqJKARYvyC2bmwu3whbm/48BJjdlGVvgDguoCqJIkI/67NkroTYobd5geai1WheNQvWrgA==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection": "9.0.4",
- "Microsoft.Extensions.Logging.Abstractions": "9.0.4",
- "Microsoft.Extensions.Options": "9.0.4"
+ "Microsoft.Extensions.DependencyInjection": "9.0.8",
+ "Microsoft.Extensions.Logging.Abstractions": "9.0.8",
+ "Microsoft.Extensions.Options": "9.0.8"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "0MXlimU4Dud6t+iNi5NEz3dO2w1HXdhoOLaYFuLPCjAsvlPQGwOT6V2KZRMLEhCAm/stSZt1AUv0XmDdkjvtbw==",
+ "resolved": "9.0.8",
+ "contentHash": "pYnAffJL7ARD/HCnnPvnFKSIHnTSmWz84WIlT9tPeQ4lHNiu0Az7N/8itihWvcF8sT+VVD5lq8V+ckMzu4SbOw==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8"
}
},
"Microsoft.Extensions.Logging.Configuration": {
@@ -387,34 +435,34 @@
},
"Microsoft.Extensions.Options": {
"type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "fiFI2+58kicqVZyt/6obqoFwHiab7LC4FkQ3mmiBJ28Yy4fAvy2+v9MRnSvvlOO8chTOjKsdafFl/K9veCPo5g==",
+ "resolved": "9.0.8",
+ "contentHash": "OmTaQ0v4gxGQkehpwWIqPoEiwsPuG/u4HUsbOFoWGx4DKET2AXzopnFe/fE608FIhzc/kcg2p8JdyMRCCUzitQ==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
- "Microsoft.Extensions.Primitives": "9.0.4"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8",
+ "Microsoft.Extensions.Primitives": "9.0.8"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "aridVhAT3Ep+vsirR1pzjaOw0Jwiob6dc73VFQn2XmDfBA2X98M8YKO1GarvsXRX7gX1Aj+hj2ijMzrMHDOm0A==",
+ "resolved": "9.0.8",
+ "contentHash": "eW2s6n06x0w6w4nsX+SvpgsFYkl+Y0CttYAt6DKUXeqprX+hzNqjSfOh637fwNJBg7wRBrOIRHe49gKiTgJxzQ==",
"dependencies": {
- "Microsoft.Extensions.Configuration.Abstractions": "9.0.4",
- "Microsoft.Extensions.Configuration.Binder": "9.0.4",
- "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4",
- "Microsoft.Extensions.Options": "9.0.4",
- "Microsoft.Extensions.Primitives": "9.0.4"
+ "Microsoft.Extensions.Configuration.Abstractions": "9.0.8",
+ "Microsoft.Extensions.Configuration.Binder": "9.0.8",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8",
+ "Microsoft.Extensions.Options": "9.0.8",
+ "Microsoft.Extensions.Primitives": "9.0.8"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
- "resolved": "9.0.4",
- "contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA=="
+ "resolved": "9.0.8",
+ "contentHash": "tizSIOEsIgSNSSh+hKeUVPK7xmTIjR8s+mJWOu1KXV3htvNQiPMFRMO17OdI1y/4ZApdBVk49u/08QGC9yvLug=="
},
"Microsoft.Identity.Client": {
"type": "Transitive",
- "resolved": "4.67.2",
- "contentHash": "37t0TfekfG6XM8kue/xNaA66Qjtti5Qe1xA41CK+bEd8VD76/oXJc+meFJHGzygIC485dCpKoamG/pDfb9Qd7Q==",
+ "resolved": "4.73.1",
+ "contentHash": "NnDLS8QwYqO5ZZecL2oioi1LUqjh5Ewk4bMLzbgiXJbQmZhDLtKwLxL3DpGMlQAJ2G4KgEnvGPKa+OOgffeJbw==",
"dependencies": {
"Microsoft.IdentityModel.Abstractions": "6.35.0",
"System.Diagnostics.DiagnosticSource": "6.0.1"
@@ -422,10 +470,10 @@
},
"Microsoft.Identity.Client.Extensions.Msal": {
"type": "Transitive",
- "resolved": "4.67.2",
- "contentHash": "DKs+Lva6csEUZabw+JkkjtFgVmcXh4pJeQy5KH5XzPOaKNoZhAMYj1qpKd97qYTZKXIFH12bHPk0DA+6krw+Cw==",
+ "resolved": "4.73.1",
+ "contentHash": "xDztAiV2F0wI0W8FLKv5cbaBefyLD6JVaAsvgSN7bjWNCzGYzHbcOEIP5s4TJXUpQzMfUyBsFl1mC6Zmgpz0PQ==",
"dependencies": {
- "Microsoft.Identity.Client": "4.67.2",
+ "Microsoft.Identity.Client": "4.73.1",
"System.Security.Cryptography.ProtectedData": "4.5.0"
}
},
@@ -498,6 +546,16 @@
"Newtonsoft.Json": "13.0.3"
}
},
+ "OpenTelemetry": {
+ "type": "Transitive",
+ "resolved": "1.12.0",
+ "contentHash": "aIEu2O3xFOdwIVH0AJsIHPIMH1YuX18nzu7BHyaDNQ6NWSk4Zyrs9Pp6y8SATuSbvdtmvue4mj/QZ3838srbwA==",
+ "dependencies": {
+ "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.0",
+ "Microsoft.Extensions.Logging.Configuration": "9.0.0",
+ "OpenTelemetry.Api.ProviderBuilderExtensions": "1.12.0"
+ }
+ },
"OpenTelemetry.Api": {
"type": "Transitive",
"resolved": "1.12.0",
@@ -578,11 +636,11 @@
},
"System.ClientModel": {
"type": "Transitive",
- "resolved": "1.1.0",
- "contentHash": "UocOlCkxLZrG2CKMAAImPcldJTxeesHnHGHwhJ0pNlZEvEXcWKuQvVOER2/NiOkJGRJk978SNdw3j6/7O9H1lg==",
+ "resolved": "1.4.1",
+ "contentHash": "MY7eFGKp+Hu7Ciub8wigQ0odGrkml4eTjUy8d5Bu2eGAVvm8Qskkq+YuXiiS5wMJGq7iSvqseV4skd5WxTUdDA==",
"dependencies": {
- "System.Memory.Data": "1.0.2",
- "System.Text.Json": "6.0.9"
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.3",
+ "System.Memory.Data": "6.0.1"
}
},
"System.Diagnostics.DiagnosticSource": {
@@ -597,16 +655,8 @@
},
"System.Memory.Data": {
"type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "ntFHArH3I4Lpjf5m4DCXQHJuGwWPNVJPaAvM95Jy/u+2Yzt2ryiyIN04LAogkjP9DeRcEOiviAjQotfmPq/FrQ==",
- "dependencies": {
- "System.Text.Json": "6.0.0"
- }
- },
- "System.Numerics.Vectors": {
- "type": "Transitive",
- "resolved": "4.5.0",
- "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ=="
+ "resolved": "6.0.1",
+ "contentHash": "yliDgLh9S9Mcy5hBIdZmX6yphYIW3NH+3HN1kV1m7V1e0s7LNTw/tHNjJP4U9nSMEgl3w1TzYv/KA1Tg9NYy6w=="
},
"System.Runtime.CompilerServices.Unsafe": {
"type": "Transitive",
@@ -618,23 +668,18 @@
"resolved": "4.5.0",
"contentHash": "wLBKzFnDCxP12VL9ANydSYhk59fC4cvOr9ypYQLPnAj48NQIhqnjdD2yhP8yEKyBJEjERWS9DisKL7rX5eU25Q=="
},
- "System.Text.Encodings.Web": {
- "type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "Vg8eB5Tawm1IFqj4TVK1czJX89rhFxJo9ELqc/Eiq0eXy13RK00eubyU6TJE6y+GQXjyV5gSfiewDUZjQgSE0w==",
- "dependencies": {
- "System.Runtime.CompilerServices.Unsafe": "6.0.0"
- }
- },
"System.Text.Json": {
"type": "Transitive",
"resolved": "9.0.4",
"contentHash": "pYtmpcO6R3Ef1XilZEHgXP2xBPVORbYEzRP7dl0IAAbN8Dm+kfwio8aCKle97rAWXOExr292MuxWYurIuwN62g=="
},
- "System.Threading.Tasks.Extensions": {
+ "Unobtanium.Web.Proxy.Events": {
"type": "Transitive",
- "resolved": "4.5.4",
- "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg=="
+ "resolved": "0.9.0-beta.1",
+ "contentHash": "ckgncJ95Tr1GCx3Kv3yJK3sANfJznKIHOqZebqR0w3JblHrNF2VLJ5ALA5YZ5CcO7QuqqXVnfkusapy7QJRFwA==",
+ "dependencies": {
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.1"
+ }
},
"YamlDotNet": {
"type": "Transitive",
@@ -654,7 +699,7 @@
"Newtonsoft.Json.Schema": "[4.0.1, )",
"Scriban": "[6.2.1, )",
"System.CommandLine": "[2.0.0-beta5.25306.1, )",
- "Unobtanium.Web.Proxy": "[0.1.5, )",
+ "Unobtanium.Web.Proxy": "[0.9.0-beta.1, )",
"YamlDotNet": "[16.3.0, )"
}
}
From 331a9e89bb57fa4733eaa48917f241544db3a86b Mon Sep 17 00:00:00 2001
From: Stephan van Rooij <1292510+svrooij@users.noreply.github.com>
Date: Mon, 18 Aug 2025 16:02:15 +0200
Subject: [PATCH 02/14] Main proxy migration
---
.../DevProxy.Abstractions.csproj | 4 +-
.../Extensions/FuncExtensions.cs | 34 --
.../Extensions/ILoggerExtensions.cs | 8 +-
.../Models/PluginResponse.cs | 15 +
.../Models/RequestArguments.cs | 5 +
.../Models/ResponseArguments.cs | 6 +
DevProxy.Abstractions/Plugins/BasePlugin.cs | 4 +
DevProxy.Abstractions/Plugins/IPlugin.cs | 4 +
DevProxy.Abstractions/Plugins/PluginEvents.cs | 5 +-
DevProxy.Abstractions/Proxy/IProxyLogger.cs | 7 +-
DevProxy.Abstractions/Proxy/ProxyEvents.cs | 50 +-
DevProxy.Abstractions/Utils/ProxyUtils.cs | 17 +-
DevProxy.Abstractions/packages.lock.json | 31 +-
DevProxy/ApiControllers/ProxyController.cs | 6 +-
DevProxy/Commands/CertCommand.cs | 23 +-
DevProxy/DevProxy.csproj | 2 +-
.../IServiceCollectionExtensions.cs | 44 +-
DevProxy/Logging/ILoggerExtensions.cs | 8 +-
DevProxy/Logging/ProxyConsoleFormatter.cs | 26 +-
DevProxy/Program.cs | 15 +
DevProxy/Proxy/CertificateDiskCache.cs | 142 ------
.../Proxy/EfficientProxyHttpClientFactory.cs | 4 +-
DevProxy/Proxy/ProxyEngine.cs | 471 +++++++++---------
DevProxy/Proxy/ProxyStateController.cs | 6 +-
DevProxy/packages.lock.json | 27 +-
25 files changed, 406 insertions(+), 558 deletions(-)
delete mode 100644 DevProxy.Abstractions/Extensions/FuncExtensions.cs
create mode 100644 DevProxy.Abstractions/Models/PluginResponse.cs
create mode 100644 DevProxy.Abstractions/Models/RequestArguments.cs
create mode 100644 DevProxy.Abstractions/Models/ResponseArguments.cs
delete mode 100644 DevProxy/Proxy/CertificateDiskCache.cs
diff --git a/DevProxy.Abstractions/DevProxy.Abstractions.csproj b/DevProxy.Abstractions/DevProxy.Abstractions.csproj
index eaa9d82f..f853b097 100644
--- a/DevProxy.Abstractions/DevProxy.Abstractions.csproj
+++ b/DevProxy.Abstractions/DevProxy.Abstractions.csproj
@@ -1,4 +1,4 @@
-
+
net9.0
@@ -23,7 +23,7 @@
-
+
diff --git a/DevProxy.Abstractions/Extensions/FuncExtensions.cs b/DevProxy.Abstractions/Extensions/FuncExtensions.cs
deleted file mode 100644
index 6740b0b3..00000000
--- a/DevProxy.Abstractions/Extensions/FuncExtensions.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-// from: https://github.com/justcoding121/titanium-web-proxy/blob/902504a324425e4e49fc5ba604c2b7fa172e68ce/src/Titanium.Web.Proxy/Extensions/FuncExtensions.cs
-
-#pragma warning disable IDE0130
-namespace Unobtanium.Web.Proxy.EventArguments;
-#pragma warning restore IDE0130
-
-public static class FuncExtensions
-{
- internal static async Task InvokeAsync(this AsyncEventHandler callback, object sender, T args, ExceptionHandler? exceptionFunc)
- {
- var invocationList = callback.GetInvocationList();
-
- foreach (var @delegate in invocationList)
- {
- await InternalInvokeAsync((AsyncEventHandler)@delegate, sender, args, exceptionFunc);
- }
- }
-
- private static async Task InternalInvokeAsync(AsyncEventHandler callback, object sender, T e, ExceptionHandler? exceptionFunc)
- {
- try
- {
- await callback(sender, e);
- }
- catch (Exception ex)
- {
- exceptionFunc?.Invoke(new InvalidOperationException("Exception thrown in user event", ex));
- }
- }
-}
\ No newline at end of file
diff --git a/DevProxy.Abstractions/Extensions/ILoggerExtensions.cs b/DevProxy.Abstractions/Extensions/ILoggerExtensions.cs
index dafa6aee..26c3d4d8 100644
--- a/DevProxy.Abstractions/Extensions/ILoggerExtensions.cs
+++ b/DevProxy.Abstractions/Extensions/ILoggerExtensions.cs
@@ -7,8 +7,9 @@ namespace Microsoft.Extensions.Logging;
public static class ILoggerExtensions
{
- public static void LogRequest(this ILogger logger, string message, MessageType messageType, LoggingContext? context = null)
+ public static void LogRequest(this ILogger logger, string message, MessageType messageType, object? context = null)
{
+ ArgumentNullException.ThrowIfNull(logger);
logger.Log(new RequestLog(message, messageType, context));
}
@@ -17,6 +18,11 @@ public static void LogRequest(this ILogger logger, string message, MessageType m
logger.Log(new RequestLog(message, messageType, method, url));
}
+ public static void LogRequest(this ILogger logger, string message, MessageType messageType, HttpRequestMessage httpRequestMessage)
+ {
+ logger.Log(new RequestLog(message, messageType, httpRequestMessage));
+ }
+
public static void Log(this ILogger logger, RequestLog message)
{
ArgumentNullException.ThrowIfNull(logger);
diff --git a/DevProxy.Abstractions/Models/PluginResponse.cs b/DevProxy.Abstractions/Models/PluginResponse.cs
new file mode 100644
index 00000000..4bc1dc98
--- /dev/null
+++ b/DevProxy.Abstractions/Models/PluginResponse.cs
@@ -0,0 +1,15 @@
+namespace DevProxy.Abstractions.Models;
+public class PluginResponse
+{
+ public HttpRequestMessage? Request { get; private set; }
+ public HttpResponseMessage? Response { get; private set; }
+ private PluginResponse(HttpResponseMessage? response, HttpRequestMessage? request)
+ {
+ Response = response;
+ Request = request;
+ }
+
+ public static PluginResponse Continue() => new(null, null);
+ public static PluginResponse Continue(HttpRequestMessage request) => new(null, request);
+ public static PluginResponse Respond(HttpResponseMessage response) => new(response, null);
+}
diff --git a/DevProxy.Abstractions/Models/RequestArguments.cs b/DevProxy.Abstractions/Models/RequestArguments.cs
new file mode 100644
index 00000000..9c5f5f17
--- /dev/null
+++ b/DevProxy.Abstractions/Models/RequestArguments.cs
@@ -0,0 +1,5 @@
+namespace DevProxy.Abstractions.Models;
+public class RequestArguments(HttpRequestMessage request)
+{
+ public HttpRequestMessage Request { get; } = request;
+}
diff --git a/DevProxy.Abstractions/Models/ResponseArguments.cs b/DevProxy.Abstractions/Models/ResponseArguments.cs
new file mode 100644
index 00000000..cc45c2a7
--- /dev/null
+++ b/DevProxy.Abstractions/Models/ResponseArguments.cs
@@ -0,0 +1,6 @@
+namespace DevProxy.Abstractions.Models;
+public class ResponseArguments(HttpRequestMessage httpRequestMessage, HttpResponseMessage httpResponseMessage)
+{
+ public HttpRequestMessage HttpRequestMessage { get; } = httpRequestMessage;
+ public HttpResponseMessage HttpResponseMessage { get; } = httpResponseMessage;
+}
diff --git a/DevProxy.Abstractions/Plugins/BasePlugin.cs b/DevProxy.Abstractions/Plugins/BasePlugin.cs
index d093f9da..eab92cb1 100644
--- a/DevProxy.Abstractions/Plugins/BasePlugin.cs
+++ b/DevProxy.Abstractions/Plugins/BasePlugin.cs
@@ -5,11 +5,13 @@
using System.CommandLine;
using System.Globalization;
using System.Text.Json;
+using DevProxy.Abstractions.Models;
using DevProxy.Abstractions.Proxy;
using DevProxy.Abstractions.Utils;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using Unobtanium.Web.Proxy.Events;
namespace DevProxy.Abstractions.Plugins;
@@ -22,6 +24,8 @@ public abstract class BasePlugin(
protected ISet UrlsToWatch { get; } = urlsToWatch;
public abstract string Name { get; }
+ public Func>? OnRequestAsync { get; set; }
+ public Func>? OnResponseAsync { get; set; }
public virtual Option[] GetOptions() => [];
public virtual Command[] GetCommands() => [];
diff --git a/DevProxy.Abstractions/Plugins/IPlugin.cs b/DevProxy.Abstractions/Plugins/IPlugin.cs
index a86d6d71..8c63b8f4 100644
--- a/DevProxy.Abstractions/Plugins/IPlugin.cs
+++ b/DevProxy.Abstractions/Plugins/IPlugin.cs
@@ -6,6 +6,7 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.CommandLine;
+using Unobtanium.Web.Proxy.Events;
namespace DevProxy.Abstractions.Plugins;
@@ -18,6 +19,9 @@ public interface IPlugin
Task InitializeAsync(InitArgs e, CancellationToken cancellationToken);
void OptionsLoaded(OptionsLoadedArgs e);
+
+ Func>? OnRequestAsync { get; set; }
+ Func>? OnResponseAsync { get; set; }
Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken cancellationToken);
Task BeforeResponseAsync(ProxyResponseArgs e, CancellationToken cancellationToken);
Task AfterResponseAsync(ProxyResponseArgs e, CancellationToken cancellationToken);
diff --git a/DevProxy.Abstractions/Plugins/PluginEvents.cs b/DevProxy.Abstractions/Plugins/PluginEvents.cs
index fb6333a1..7b59d80b 100644
--- a/DevProxy.Abstractions/Plugins/PluginEvents.cs
+++ b/DevProxy.Abstractions/Plugins/PluginEvents.cs
@@ -2,11 +2,10 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using Unobtanium.Web.Proxy.Http;
namespace DevProxy.Abstractions.Plugins;
-public class ThrottlerInfo(string throttlingKey, Func shouldThrottle, DateTime resetTime)
+public class ThrottlerInfo(string throttlingKey, Func shouldThrottle, DateTime resetTime)
{
///
/// Time when the throttling window will be reset
@@ -20,7 +19,7 @@ public class ThrottlerInfo(string throttlingKey, Func
- public Func ShouldThrottle { get; private set; } = shouldThrottle ?? throw new ArgumentNullException(nameof(shouldThrottle));
+ public Func ShouldThrottle { get; private set; } = shouldThrottle ?? throw new ArgumentNullException(nameof(shouldThrottle));
///
/// Throttling key used to identify which requests should be throttled.
/// Can be set to a hostname, full URL or a custom string value, that
diff --git a/DevProxy.Abstractions/Proxy/IProxyLogger.cs b/DevProxy.Abstractions/Proxy/IProxyLogger.cs
index 16d42e3d..b7c34c2c 100644
--- a/DevProxy.Abstractions/Proxy/IProxyLogger.cs
+++ b/DevProxy.Abstractions/Proxy/IProxyLogger.cs
@@ -2,7 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using Unobtanium.Web.Proxy.EventArguments;
+
namespace DevProxy.Abstractions.Proxy;
@@ -22,8 +22,3 @@ public enum MessageType
Processed,
Timestamp
}
-
-public class LoggingContext(SessionEventArgs session)
-{
- public SessionEventArgs Session { get; } = session;
-}
\ No newline at end of file
diff --git a/DevProxy.Abstractions/Proxy/ProxyEvents.cs b/DevProxy.Abstractions/Proxy/ProxyEvents.cs
index 55c5fb20..35076ec6 100644
--- a/DevProxy.Abstractions/Proxy/ProxyEvents.cs
+++ b/DevProxy.Abstractions/Proxy/ProxyEvents.cs
@@ -2,10 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using DevProxy.Abstractions.Utils;
using System.CommandLine;
using System.Text.Json.Serialization;
-using Unobtanium.Web.Proxy.EventArguments;
namespace DevProxy.Abstractions.Proxy;
@@ -15,16 +13,16 @@ public class ProxyEventArgsBase
public Dictionary GlobalData { get; init; } = [];
}
-public class ProxyHttpEventArgsBase(SessionEventArgs session) : ProxyEventArgsBase
+public class ProxyHttpEventArgsBase(object session) : ProxyEventArgsBase
{
- public SessionEventArgs Session { get; } = session ??
+ public object Session { get; } = session ??
throw new ArgumentNullException(nameof(session));
- public bool HasRequestUrlMatch(ISet watchedUrls) =>
- ProxyUtils.MatchesUrlToWatch(watchedUrls, Session.HttpClient.Request.RequestUri.AbsoluteUri);
+ public static bool HasRequestUrlMatch(ISet _) => true;
+ //ProxyUtils.MatchesUrlToWatch(watchedUrls, Session.HttpClient.Request.RequestUri.AbsoluteUri);
}
-public class ProxyRequestArgs(SessionEventArgs session, ResponseState responseState) :
+public class ProxyRequestArgs(object session, ResponseState responseState) :
ProxyHttpEventArgsBase(session)
{
public ResponseState ResponseState { get; } = responseState ??
@@ -35,7 +33,7 @@ public bool ShouldExecute(ISet watchedUrls) =>
&& HasRequestUrlMatch(watchedUrls);
}
-public class ProxyResponseArgs(SessionEventArgs session, ResponseState responseState) :
+public class ProxyResponseArgs(object session, ResponseState responseState) :
ProxyHttpEventArgsBase(session)
{
public ResponseState ResponseState { get; } = responseState ??
@@ -55,41 +53,49 @@ public class OptionsLoadedArgs(ParseResult parseResult)
public class RequestLog
{
+ //[JsonIgnore]
+ //public LoggingContext? Context { get; set; }
[JsonIgnore]
- public LoggingContext? Context { get; set; }
+ public HttpRequestMessage? Request { get; internal set; }
public string Message { get; set; }
public MessageType MessageType { get; set; }
public string? Method { get; init; }
public string? PluginName { get; set; }
public string? Url { get; init; }
- public RequestLog(string message, MessageType messageType, LoggingContext? context) :
- this(message, messageType, context?.Session.HttpClient.Request.Method, context?.Session.HttpClient.Request.Url, context)
+ public RequestLog(string message, MessageType messageType, object? context)
{
+ throw new NotImplementedException("This constructor is not implemented. Use the other constructors instead.");
+ }
+
+ public RequestLog(string message, MessageType messageType, HttpRequestMessage requestMessage) :
+ this(message, messageType, requestMessage?.Method.Method, requestMessage?.RequestUri!.AbsoluteUri, _: null)
+ {
+ Request = requestMessage;
}
public RequestLog(string message, MessageType messageType, string method, string url) :
- this(message, messageType, method, url, context: null)
+ this(message, messageType, method, url, _: null)
{
}
- private RequestLog(string message, MessageType messageType, string? method, string? url, LoggingContext? context)
+ private RequestLog(string message, MessageType messageType, string? method, string? url, object? _)
{
Message = message ?? throw new ArgumentNullException(nameof(message));
MessageType = messageType;
- Context = context;
+ //Context = context;
Method = method;
Url = url;
}
- public void Deconstruct(out string message, out MessageType messageType, out LoggingContext? context, out string? method, out string? url)
- {
- message = Message;
- messageType = MessageType;
- context = Context;
- method = Method;
- url = Url;
- }
+ //public void Deconstruct(out string message, out MessageType messageType, out LoggingContext? context, out string? method, out string? url)
+ //{
+ // message = Message;
+ // messageType = MessageType;
+ // context = Context;
+ // method = Method;
+ // url = Url;
+ //}
}
public class RecordingArgs(IEnumerable requestLogs) : ProxyEventArgsBase
diff --git a/DevProxy.Abstractions/Utils/ProxyUtils.cs b/DevProxy.Abstractions/Utils/ProxyUtils.cs
index 56b8da9b..55828e26 100644
--- a/DevProxy.Abstractions/Utils/ProxyUtils.cs
+++ b/DevProxy.Abstractions/Utils/ProxyUtils.cs
@@ -12,7 +12,6 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
-using Unobtanium.Web.Proxy.Http;
namespace DevProxy.Abstractions.Utils;
@@ -84,14 +83,14 @@ static ProxyUtils()
JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
}
- public static bool IsGraphRequest(Request request)
+ public static bool IsGraphRequest(HttpRequestMessage request)
{
ArgumentNullException.ThrowIfNull(request);
return IsGraphUrl(request.RequestUri);
}
- public static bool IsGraphUrl(Uri uri)
+ public static bool IsGraphUrl(Uri? uri)
{
ArgumentNullException.ThrowIfNull(uri);
@@ -116,18 +115,18 @@ public static Uri GetAbsoluteRequestUrlFromBatch(Uri batchRequestUri, string rel
return absoluteRequestUrl;
}
- public static bool IsSdkRequest(Request request)
+ public static bool IsSdkRequest(HttpRequestMessage request)
{
ArgumentNullException.ThrowIfNull(request);
- return request.Headers.HeaderExists("SdkVersion");
+ return request.Headers.Contains("SdkVersion");
}
- public static bool IsGraphBetaRequest(Request request) =>
+ public static bool IsGraphBetaRequest(HttpRequestMessage request) =>
IsGraphRequest(request) &&
IsGraphBetaUrl(request.RequestUri);
- public static bool IsGraphBetaUrl(Uri uri)
+ public static bool IsGraphBetaUrl(Uri? uri)
{
ArgumentNullException.ThrowIfNull(uri);
@@ -141,7 +140,7 @@ public static bool IsGraphBetaUrl(Uri uri)
/// string a guid representing the a unique identifier for the request
/// string representation of the date and time the request was made
/// IList with defaults consistent with Microsoft Graph. Automatically adds CORS headers when the Origin header is present
- public static IList BuildGraphResponseHeaders(Request request, string requestId, string requestDate)
+ public static IList BuildGraphResponseHeaders(HttpRequestMessage request, string requestId, string requestDate)
{
if (!IsGraphRequest(request))
{
@@ -158,7 +157,7 @@ public static IList BuildGraphResponseHeaders(Request reques
new ("Date", requestDate),
new ("Content-Type", "application/json")
};
- if (request.Headers.FirstOrDefault((h) => h.Name.Equals("Origin", StringComparison.OrdinalIgnoreCase)) is not null)
+ if (request.Headers.Contains("Origin"))
{
headers.Add(new("Access-Control-Allow-Origin", "*"));
headers.Add(new("Access-Control-Expose-Headers", "ETag, Location, Preference-Applied, Content-Range, request-id, client-request-id, ReadWriteConsistencyToken, SdkVersion, WWW-Authenticate, x-ms-client-gcc-tenant, Retry-After"));
diff --git a/DevProxy.Abstractions/packages.lock.json b/DevProxy.Abstractions/packages.lock.json
index ba78b533..0d1d5cc6 100644
--- a/DevProxy.Abstractions/packages.lock.json
+++ b/DevProxy.Abstractions/packages.lock.json
@@ -95,16 +95,13 @@
"resolved": "2.0.0-beta5.25306.1",
"contentHash": "ce0wuowuh13Cd7GXqLCq77/YWlxQMxrVCMIO/2/QUP6CdP/JWnlYSN/N3/55wwGsUwa9CvPuT8ddjgyypUr5ag=="
},
- "Unobtanium.Web.Proxy": {
+ "Unobtanium.Web.Proxy.Events": {
"type": "Direct",
- "requested": "[0.9.0-beta.1, )",
- "resolved": "0.9.0-beta.1",
- "contentHash": "Ae/2f7v3T3NQkRknBhAHqBrY85nJHQr8lxRD/j8B3rMK4KbIlbGe3A6Hd71ponq+sNHnLxa3BPSg8pdldc9RHg==",
+ "requested": "[0.9.1-beta.2, )",
+ "resolved": "0.9.1-beta.2",
+ "contentHash": "8vO+KuRBp/UsvNtUtsKx2hdBGoYzPu2wQoAcQL63UnT6hphPCdqm2mt6JNgH6bprI4f9cbn/O3ny+EP5ftEV0Q==",
"dependencies": {
- "BouncyCastle.Cryptography": "2.4.0",
- "Microsoft.Extensions.Logging.Abstractions": "8.0.3",
- "System.Runtime.CompilerServices.Unsafe": "6.0.0",
- "Unobtanium.Web.Proxy.Events": "0.9.0-beta.1"
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.1"
}
},
"YamlDotNet": {
@@ -113,11 +110,6 @@
"resolved": "16.3.0",
"contentHash": "SgMOdxbz8X65z8hraIs6hOEdnkH6hESTAIUa7viEngHOYaH+6q5XJmwr1+yb9vJpNQ19hCQY69xbFsLtXpobQA=="
},
- "BouncyCastle.Cryptography": {
- "type": "Transitive",
- "resolved": "2.4.0",
- "contentHash": "SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ=="
- },
"Microsoft.Data.Sqlite.Core": {
"type": "Transitive",
"resolved": "9.0.4",
@@ -328,23 +320,10 @@
"resolved": "4.5.3",
"contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA=="
},
- "System.Runtime.CompilerServices.Unsafe": {
- "type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
- },
"System.Text.Json": {
"type": "Transitive",
"resolved": "9.0.4",
"contentHash": "pYtmpcO6R3Ef1XilZEHgXP2xBPVORbYEzRP7dl0IAAbN8Dm+kfwio8aCKle97rAWXOExr292MuxWYurIuwN62g=="
- },
- "Unobtanium.Web.Proxy.Events": {
- "type": "Transitive",
- "resolved": "0.9.0-beta.1",
- "contentHash": "ckgncJ95Tr1GCx3Kv3yJK3sANfJznKIHOqZebqR0w3JblHrNF2VLJ5ALA5YZ5CcO7QuqqXVnfkusapy7QJRFwA==",
- "dependencies": {
- "Microsoft.Extensions.Logging.Abstractions": "8.0.1"
- }
}
}
}
diff --git a/DevProxy/ApiControllers/ProxyController.cs b/DevProxy/ApiControllers/ProxyController.cs
index 1b0c88ec..cb9db278 100644
--- a/DevProxy/ApiControllers/ProxyController.cs
+++ b/DevProxy/ApiControllers/ProxyController.cs
@@ -15,7 +15,7 @@ namespace DevProxy.ApiControllers;
[ApiController]
[Route("[controller]")]
#pragma warning disable CA1515 // required for the API controller
-public sealed class ProxyController(IProxyStateController proxyStateController, IProxyConfiguration proxyConfiguration, ProxyServer proxyServer) : ControllerBase
+public sealed class ProxyController(IProxyStateController proxyStateController, IProxyConfiguration proxyConfiguration, ICertificateManager certificateManager) : ControllerBase
#pragma warning restore CA1515
{
private readonly IProxyStateController _proxyStateController = proxyStateController;
@@ -101,7 +101,7 @@ public IActionResult CreateJwtToken([FromBody] JwtOptions jwtOptions)
}
[HttpGet("rootCertificate")]
- public IActionResult GetRootCertificate([FromQuery][Required] string format)
+ public async Task GetRootCertificateAsync([FromQuery][Required] string format, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(format))
{
@@ -115,7 +115,7 @@ public IActionResult GetRootCertificate([FromQuery][Required] string format)
return ValidationProblem(ModelState);
}
- var certificate = proxyServer.CertificateManager.RootCertificate;
+ var certificate = await certificateManager.GetRootCertificateAsync(false, cancellationToken);
if (certificate == null)
{
var problemDetails = new ProblemDetails
diff --git a/DevProxy/Commands/CertCommand.cs b/DevProxy/Commands/CertCommand.cs
index 0e745037..d46d0182 100644
--- a/DevProxy/Commands/CertCommand.cs
+++ b/DevProxy/Commands/CertCommand.cs
@@ -6,33 +6,33 @@
using System.CommandLine;
using System.CommandLine.Parsing;
using System.Diagnostics;
+using System.Runtime.InteropServices;
using Unobtanium.Web.Proxy;
-using Unobtanium.Web.Proxy.Helpers;
namespace DevProxy.Commands;
sealed class CertCommand : Command
{
private readonly ILogger _logger;
- private readonly ProxyServer _server;
+ private readonly ICertificateManager _certificateManager;
private readonly Option _forceOption = new("--force", "-f")
{
Description = "Don't prompt for confirmation when removing the certificate"
};
- public CertCommand(ILogger logger, ProxyServer server) :
+ public CertCommand(ILogger logger, ICertificateManager certificateManager) :
base("cert", "Manage the Dev Proxy certificate")
{
_logger = logger;
ConfigureCommand();
- _server = server;
+ _certificateManager = certificateManager;
}
private void ConfigureCommand()
{
var certEnsureCommand = new Command("ensure", "Ensure certificates are setup (creates root if required). Also makes root certificate trusted.");
- certEnsureCommand.SetAction(async _ => await EnsureCertAsync());
+ certEnsureCommand.SetAction(async (_, cancellationToken) => await EnsureCertAsync(cancellationToken));
var certRemoveCommand = new Command("remove", "Remove the certificate from Root Store");
certRemoveCommand.SetAction(RemoveCert);
@@ -45,14 +45,19 @@ private void ConfigureCommand()
}.OrderByName());
}
- private async Task EnsureCertAsync()
+ private async Task EnsureCertAsync(CancellationToken cancellationToken)
{
_logger.LogTrace("EnsureCertAsync() called");
try
{
_logger.LogInformation("Ensuring certificate exists and is trusted...");
- await _server.CertificateManager.EnsureRootCertificateAsync();
+ // TODO: Make the computer trust this certificate
+ _ = await _certificateManager.GetRootCertificateAsync(false, cancellationToken);
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ // ...
+ }
_logger.LogInformation("DONE");
}
catch (Exception ex)
@@ -82,7 +87,7 @@ public void RemoveCert(ParseResult parseResult)
_logger.LogInformation("Uninstalling the root certificate...");
RemoveTrustedCertificateOnMac();
- _server.CertificateManager.RemoveTrustedRootCertificate(machineTrusted: false);
+ // TODO: Implement for Windows/Linux
_logger.LogInformation("DONE");
}
@@ -120,7 +125,7 @@ private static bool PromptConfirmation(string message, bool acceptByDefault)
private static void RemoveTrustedCertificateOnMac()
{
- if (!RunTime.IsMac)
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return;
}
diff --git a/DevProxy/DevProxy.csproj b/DevProxy/DevProxy.csproj
index 52df788f..74d10b60 100644
--- a/DevProxy/DevProxy.csproj
+++ b/DevProxy/DevProxy.csproj
@@ -49,7 +49,7 @@
-
+
diff --git a/DevProxy/Extensions/IServiceCollectionExtensions.cs b/DevProxy/Extensions/IServiceCollectionExtensions.cs
index 1c07d7ce..6290856a 100644
--- a/DevProxy/Extensions/IServiceCollectionExtensions.cs
+++ b/DevProxy/Extensions/IServiceCollectionExtensions.cs
@@ -12,7 +12,7 @@
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
using Unobtanium.Web.Proxy;
-using Unobtanium.Web.Proxy.Models;
+using Unobtanium.Web.Proxy.Services;
#pragma warning disable IDE0130
namespace Microsoft.Extensions.DependencyInjection;
@@ -44,15 +44,26 @@ static IServiceCollection AddApplicationServices(
{
_ = services
.AddProxyHttpClientFactory()
- .AddProxyConfiguration(configuration)
- .AddSingleton() // ProxyServer has to be injected
+ .AddProxyEvents(new Unobtanium.Web.Proxy.Events.ProxyServerEvents())
+ .Configure(options =>
+ {
+ options.Port = configuration.GetValue("Port", ProxyServerDefaults.DEFAULT_PORT);
+ //options.TrustCertificateOnStart = true; // Automatically trust the certificate on start, NOT IMPLEMENTED YET!!
+ options.TrustCertificateOnStartAsUser = true; // Automatically trust the certificate on start as user, NOT IMPLEMENTED YET!!
+ })
+ .Configure (certOptions =>
+ {
+ certOptions.RootCertificateName = "Dev Proxy CA";
+ certOptions.CachePath = configuration.GetValue("DEV_PROXY_CERT_PATH");
+ })
+ .AddProxyServices() // This adds the background services for the proxy and adds the default ICertificateManager
.AddSingleton((IConfigurationRoot)configuration)
.AddSingleton()
.AddSingleton()
.AddSingleton()
// TODO: Removed the injected certificate
//.AddSingleton(sp => ProxyEngine.Certificate!) // Why is this injected?
- .AddSingleton(sp => sp.GetRequiredService().CertificateManager.RootCertificate!)
+ //.AddSingleton(sp => sp.GetRequiredService().CertificateManager.RootCertificate!)
.AddSingleton(sp => LanguageModelClientFactory.Create(sp, configuration))
.AddSingleton()
.AddSingleton()
@@ -81,28 +92,7 @@ static IServiceCollection AddProxyHttpClientFactory(this IServiceCollection serv
Proxy = null // Ensure no proxy is set
};
});
- _ = services.AddTransient();
- return services;
- }
-
- static IServiceCollection AddProxyConfiguration(this IServiceCollection services, IConfigurationRoot configuration)
- {
- var ipAddressString = configuration.GetValue("IPAddress");
- var ipAddress = string.IsNullOrEmpty(ipAddressString) ? System.Net.IPAddress.Any : System.Net.IPAddress.Parse(ipAddressString);
- var proxyConfig = new ProxyServerConfiguration
- {
- TcpTimeWaitSeconds = 10,
- ConnectionTimeOutSeconds = 10,
- ReuseSocket = false,
- EnableConnectionPool = true,
- ForwardToUpstreamGateway = true,
- CertificateTrustMode = ProxyCertificateTrustMode.UserTrust,
- // Default endpoint? Load port from config?
- EndPoints = [new ExplicitProxyEndPoint(ipAddress, configuration.GetValue("Port", 8000))],
- CertificateCacheFolder = configuration.GetValue("DEV_PROXY_CERT_PATH"), // By default the configuration also has environment variables. Loading this from config makes it more flexible.
- RootCertificateName = "Dev Proxy CA",
- };
- _ = services.AddSingleton(proxyConfig);
+ _ = services.AddTransient();
return services;
}
@@ -119,7 +109,7 @@ static IServiceCollection AddOpenTelemetryConfig(this IServiceCollection service
}).WithTracing(tracing =>
{
_ = tracing
- .AddSource(ProxyServerDefaults.ActivitySourceName)
+ .AddSource(ProxyServerDefaults.ACTIVITY_SOURCE_NAME)
.AddSource(ProxyEngine.ACTIVITY_SOURCE_NAME)
.AddHttpClientInstrumentation()
;
diff --git a/DevProxy/Logging/ILoggerExtensions.cs b/DevProxy/Logging/ILoggerExtensions.cs
index 96505787..882c9d32 100644
--- a/DevProxy/Logging/ILoggerExtensions.cs
+++ b/DevProxy/Logging/ILoggerExtensions.cs
@@ -16,6 +16,12 @@ static class ILoggerExtensions
{ nameof(requestId), requestId }
});
- public static IDisposable? BeginRequestScope(this ILogger logger, HttpMethod method, Uri url, int requestId) => logger.BeginScope(method.Method!, url.ToString(), requestId);
+ public static IDisposable? BeginRequestScope(this ILogger logger, HttpMethod method, Uri url, string requestId) =>
+ logger.BeginScope(new Dictionary
+ {
+ { nameof(method), method },
+ { nameof(url), url },
+ { nameof(requestId), requestId }
+ });
}
\ No newline at end of file
diff --git a/DevProxy/Logging/ProxyConsoleFormatter.cs b/DevProxy/Logging/ProxyConsoleFormatter.cs
index f1e3b3ad..498e8633 100644
--- a/DevProxy/Logging/ProxyConsoleFormatter.cs
+++ b/DevProxy/Logging/ProxyConsoleFormatter.cs
@@ -69,7 +69,7 @@ sealed class ProxyConsoleFormatter : ConsoleFormatter
[MessageType.Timestamp] = (Console.BackgroundColor, ConsoleColor.Gray)
};
- private readonly ConcurrentDictionary> _messages = [];
+ private readonly ConcurrentDictionary> _messages = [];
private readonly ProxyConsoleFormatterOptions _options;
private readonly HashSet _filteredMessageTypes;
@@ -118,16 +118,20 @@ private void LogRequest(RequestLog requestLog, string category, IExternalScopePr
if (messageType == MessageType.FinishedProcessingRequest)
{
- FlushLogsForRequest(requestId.Value, textWriter);
+ FlushLogsForRequest(requestId, textWriter);
}
else
{
- BufferRequestLog(requestLog, category, requestId.Value);
+ BufferRequestLog(requestLog, category, requestId);
}
}
- private void FlushLogsForRequest(int requestId, TextWriter textWriter)
+ private void FlushLogsForRequest(string? requestId, TextWriter textWriter)
{
+ if (string.IsNullOrEmpty(requestId) || textWriter is null)
+ {
+ return;
+ }
if (!_messages.TryGetValue(requestId, out var messages))
{
return;
@@ -153,8 +157,12 @@ private void FlushLogsForRequest(int requestId, TextWriter textWriter)
_ = _messages.TryRemove(requestId, out _);
}
- private void BufferRequestLog(RequestLog requestLog, string category, int requestId)
+ private void BufferRequestLog(RequestLog requestLog, string category, string? requestId)
{
+ if (string.IsNullOrEmpty(requestId))
+ {
+ return;
+ }
requestLog.PluginName = category == DefaultCategoryName ? null : category;
var messages = _messages.GetOrAdd(requestId, _ => []);
messages.Add(requestLog);
@@ -170,7 +178,7 @@ private void LogRegularLogMessage(in LogEntry logEntry, IExterna
else
{
var message = LogEntry.FromLogEntry(logEntry);
- var messages = _messages.GetOrAdd(requestId.Value, _ => []);
+ var messages = _messages.GetOrAdd(requestId, _ => []);
messages.Add(message);
}
}
@@ -289,15 +297,15 @@ private static string GetMessageTypeString(MessageType messageType) =>
private static (ConsoleColor bg, ConsoleColor fg) GetMessageTypeColor(MessageType messageType) =>
_messageTypeColors.TryGetValue(messageType, out var color) ? color : (Console.BackgroundColor, Console.ForegroundColor);
- private static int? GetRequestIdScope(IExternalScopeProvider? scopeProvider)
+ private static string? GetRequestIdScope(IExternalScopeProvider? scopeProvider)
{
- int? requestId = null;
+ string? requestId = null;
scopeProvider?.ForEachScope((scope, _) =>
{
if (scope is Dictionary dictionary &&
dictionary.TryGetValue(nameof(requestId), out var req))
{
- requestId = (int)req;
+ requestId = $"{req}";
}
}, "");
return requestId;
diff --git a/DevProxy/Program.cs b/DevProxy/Program.cs
index 8a16ae1e..c254ea08 100644
--- a/DevProxy/Program.cs
+++ b/DevProxy/Program.cs
@@ -5,6 +5,8 @@
using DevProxy;
using DevProxy.Commands;
using System.Net;
+using Unobtanium.Web.Proxy;
+using Unobtanium.Web.Proxy.Services;
static WebApplication BuildApplication(string[] args, DevProxyConfigOptions options)
{
@@ -13,6 +15,19 @@ static WebApplication BuildApplication(string[] args, DevProxyConfigOptions opti
_ = builder.Configuration.ConfigureDevProxyConfig(options);
_ = builder.Logging.ConfigureDevProxyLogging(builder.Configuration, options);
_ = builder.Services.ConfigureDevProxyServices(builder.Configuration, options);
+ _ = builder.Services
+ .Configure(options =>
+ {
+ options.Port = ProxyServerDefaults.DEFAULT_PORT; // Set the port for the proxy server
+ options.HttpsPort = ProxyServerDefaults.DEFAULT_HTTPS_PORT;
+ })
+ .Configure(config =>
+ {
+ config.CachePath = DevProxy.Abstractions.Utils.ProxyUtils.ReplacePathTokens(
+ builder.Configuration.GetValue("certificateCachePath", "certs"));
+ })
+ .AddProxyEvents(new Unobtanium.Web.Proxy.Events.ProxyServerEvents())
+ .AddProxyServices();
var defaultIpAddress = "127.0.0.1";
var ipAddress = options.IPAddress ??
diff --git a/DevProxy/Proxy/CertificateDiskCache.cs b/DevProxy/Proxy/CertificateDiskCache.cs
deleted file mode 100644
index efa91085..00000000
--- a/DevProxy/Proxy/CertificateDiskCache.cs
+++ /dev/null
@@ -1,142 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-using System.Security.Cryptography.X509Certificates;
-using Unobtanium.Web.Proxy.Certificates.Cache;
-using Unobtanium.Web.Proxy.Helpers;
-
-namespace DevProxy.Proxy;
-
-// based on https://github.com/justcoding121/titanium-web-proxy/blob/9e71608d204e5b67085656dd6b355813929801e4/src/Titanium.Web.Proxy/Certificates/Cache/DefaultCertificateDiskCache.cs
-internal sealed class CertificateDiskCache : ICertificateCache
-{
- private const string DefaultCertificateDirectoryName = "crts";
- private const string DefaultCertificateFileExtension = ".pfx";
- private const string DefaultRootCertificateFileName = "rootCert" + DefaultCertificateFileExtension;
- private const string ProxyConfigurationFolderName = "dev-proxy";
-
- private string? rootCertificatePath;
-
- public Task LoadRootCertificateAsync(string pathOrName, string password, X509KeyStorageFlags storageFlags, CancellationToken cancellationToken)
- {
- var path = GetRootCertificatePath(pathOrName, false);
- return Task.FromResult(LoadCertificate(path, password, storageFlags));
- }
-
- public async Task SaveRootCertificateAsync(string pathOrName, string password, X509Certificate2 certificate, CancellationToken cancellationToken)
- {
- var path = GetRootCertificatePath(pathOrName, true);
- var exported = certificate.Export(X509ContentType.Pkcs12, password);
- await File.WriteAllBytesAsync(path, exported, cancellationToken);
- }
-
- public Task LoadCertificateAsync(string subjectName, X509KeyStorageFlags storageFlags, CancellationToken cancellationToken)
- {
- var filePath = Path.Combine(GetCertificatePath(false), subjectName + DefaultCertificateFileExtension);
- return Task.FromResult(LoadCertificate(filePath, string.Empty, storageFlags));
- }
-
- public async Task SaveCertificateAsync(string subjectName, X509Certificate2 certificate, CancellationToken cancellationToken)
- {
- var filePath = Path.Combine(GetCertificatePath(true), subjectName + DefaultCertificateFileExtension);
- var exported = certificate.Export(X509ContentType.Pkcs12);
- await File.WriteAllBytesAsync(filePath, exported, cancellationToken);
- }
-
- public void Clear()
- {
- try
- {
- var path = GetCertificatePath(false);
- if (Directory.Exists(path))
- {
- Directory.Delete(path, true);
- }
- }
- catch (Exception)
- {
- // do nothing
- }
- }
-
- private string GetRootCertificatePath(string pathOrName, bool create)
- {
- if (Path.IsPathRooted(pathOrName))
- {
- return pathOrName;
- }
-
- return Path.Combine(GetRootCertificateDirectory(create),
- string.IsNullOrEmpty(pathOrName) ? DefaultRootCertificateFileName : pathOrName);
- }
-
- private string GetCertificatePath(bool create)
- {
- var path = GetRootCertificateDirectory(create);
-
- var certPath = Path.Combine(path, DefaultCertificateDirectoryName);
- if (create && !Directory.Exists(certPath))
- {
- _ = Directory.CreateDirectory(certPath);
- }
-
- return certPath;
- }
-
- private string GetRootCertificateDirectory(bool create)
- {
- if (rootCertificatePath == null)
- {
- if (RunTime.IsUwpOnWindows)
- {
- rootCertificatePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ProxyConfigurationFolderName);
- }
- else if (RunTime.IsLinux)
- {
- rootCertificatePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), ProxyConfigurationFolderName);
- }
- else if (RunTime.IsMac)
- {
- rootCertificatePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), ProxyConfigurationFolderName);
- }
- else
- {
- var assemblyLocation = AppContext.BaseDirectory;
-
- var path = Path.GetDirectoryName(assemblyLocation);
-
- rootCertificatePath = path ?? throw new InvalidOperationException("Unable to resolve root certificate directory path.");
- }
- }
-
- if (create && !Directory.Exists(rootCertificatePath))
- {
- _ = Directory.CreateDirectory(rootCertificatePath);
- }
-
- return rootCertificatePath;
- }
-
- private static X509Certificate2? LoadCertificate(string path, string password, X509KeyStorageFlags storageFlags)
- {
- byte[] exported;
-
- if (!File.Exists(path))
- {
- return null;
- }
-
- try
- {
- exported = File.ReadAllBytes(path);
- }
- catch (IOException)
- {
- // file or directory not found
- return null;
- }
-
- return X509CertificateLoader.LoadPkcs12(exported, password, storageFlags);
- }
-}
\ No newline at end of file
diff --git a/DevProxy/Proxy/EfficientProxyHttpClientFactory.cs b/DevProxy/Proxy/EfficientProxyHttpClientFactory.cs
index 7f70364e..37d4ca56 100644
--- a/DevProxy/Proxy/EfficientProxyHttpClientFactory.cs
+++ b/DevProxy/Proxy/EfficientProxyHttpClientFactory.cs
@@ -6,10 +6,10 @@ namespace DevProxy.Proxy;
/// for efficient re-use of available ports
///
/// Is added to Dependency Injection
-internal sealed class EfficientProxyHttpClientFactory(IHttpClientFactory httpClientFactory) : IProxyServerHttpClientFactory
+internal sealed class EfficientProxyHttpClientFactory(IHttpClientFactory httpClientFactory) : IProxyHttpClientFactory
{
internal const string HTTP_CLIENT_NAME = "DevProxy.Proxy.EfficientHttpClient";
private readonly IHttpClientFactory _httpClientFactory = httpClientFactory;
- public HttpClient CreateHttpClient() => _httpClientFactory.CreateClient(HTTP_CLIENT_NAME);
+ public HttpClient CreateHttpClient(string host) => _httpClientFactory.CreateClient(HTTP_CLIENT_NAME);
}
diff --git a/DevProxy/Proxy/ProxyEngine.cs b/DevProxy/Proxy/ProxyEngine.cs
index 9d7258ab..50e3a0fa 100755
--- a/DevProxy/Proxy/ProxyEngine.cs
+++ b/DevProxy/Proxy/ProxyEngine.cs
@@ -5,16 +5,14 @@
using DevProxy.Abstractions.Plugins;
using DevProxy.Abstractions.Proxy;
using DevProxy.Abstractions.Utils;
+using System;
using System.Collections.Concurrent;
using System.Diagnostics;
+using System.Net.Http.Headers;
+using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using Unobtanium.Web.Proxy;
using Unobtanium.Web.Proxy.Events;
-using Unobtanium.Web.Proxy.EventArguments;
-using Unobtanium.Web.Proxy.Helpers;
-using Unobtanium.Web.Proxy.Http;
-using Unobtanium.Web.Proxy.Models;
-using Org.BouncyCastle.Asn1.X509.Qualified;
namespace DevProxy.Proxy;
@@ -30,8 +28,8 @@ sealed class ProxyEngine(
ISet urlsToWatch,
IProxyStateController proxyController,
ILogger logger,
- ProxyServer proxyServer,
- ProxyServerConfiguration proxyServerConfiguration) : BackgroundService, IDisposable
+ ProxyServerEvents proxyEvents,
+ ICertificateManager certificateManager) : BackgroundService, IDisposable
{
internal const string ACTIVITY_SOURCE_NAME = "DevProxy.Proxy.ProxyEngine";
public static readonly ActivitySource ActivitySource = new(ACTIVITY_SOURCE_NAME);
@@ -49,13 +47,13 @@ sealed class ProxyEngine(
private readonly IProxyStateController _proxyController = proxyController;
// Dictionary for plugins to store data between requests
// the key is HashObject of the SessionEventArgs object
- private readonly ConcurrentDictionary> _pluginData = [];
+ private readonly ConcurrentDictionary> _pluginData = [];
private InactivityTimer? _inactivityTimer;
private CancellationToken? _cancellationToken;
//public static X509Certificate2? Certificate => proxyServer?.CertificateManager.RootCertificate;
- private ExceptionHandler ExceptionHandler => ex => _logger.LogError(ex, "An error occurred in a plugin");
+ //private ExceptionHandler ExceptionHandler => ex => _logger.LogError(ex, "An error occurred in a plugin");
//static ProxyEngine()
//{
@@ -76,7 +74,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_cancellationToken = stoppingToken;
- Debug.Assert(proxyServer is not null, "Proxy server is not initialized");
+ Debug.Assert(proxyEvents is not null, "Proxy server is not initialized");
if (!_urlsToWatch.Any())
{
@@ -88,17 +86,17 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
// TODO: Handle replacement of BeforeRequest
//proxyServer.BeforeRequest += OnRequestAsync;
- proxyServerConfiguration.Events.OnRequest += OnRequestAsync;
+ proxyEvents.OnRequest += OnRequestAsync;
// TODO: Handle removal of BeforeResponse
//proxyServer.BeforeResponse += OnBeforeResponseAsync;
// TODO: Handle replacement of AfterResponse
//proxyServer.AfterResponse += OnAfterResponseAsync;
- proxyServerConfiguration.Events.OnResponse += OnResponseAsync;
+ proxyEvents.OnResponse += OnResponseAsync;
- proxyServer.ServerCertificateValidationCallback += OnCertificateValidationAsync;
- proxyServer.ClientCertificateSelectionCallback += OnCertificateSelectionAsync;
+ //proxyServer.ServerCertificateValidationCallback += OnCertificateValidationAsync;
+ //proxyServer.ClientCertificateSelectionCallback += OnCertificateSelectionAsync;
// Endpoint is configured in IServiceCollectionExtensions.AddProxyConfiguration
//var ipAddress = string.IsNullOrEmpty(_config.IPAddress) ? IPAddress.Any : IPAddress.Parse(_config.IPAddress);
@@ -108,10 +106,11 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
// Fired when a CONNECT request is received
//_explicitEndPoint.BeforeTunnelConnectRequest += OnBeforeTunnelConnectRequestAsync;
// This is superceeded by:
- proxyServerConfiguration.Events.ShouldDecryptNewConnection = (host, cts) => Task.FromResult(IsProxiedHost(host));// || IsProxiedProcess(...));
+ proxyEvents.ShouldDecryptNewConnection = (host, client, cts) => Task.FromResult(IsProxiedHost(host));// || IsProxiedProcess(...));
if (_config.InstallCert)
{
- await proxyServer.CertificateManager.EnsureRootCertificateAsync(stoppingToken);
+ _ = await certificateManager.GetRootCertificateAsync(false, stoppingToken);
+ // TODO: Execute code to trust certificate
}
else
{
@@ -122,30 +121,29 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
}
//proxyServer.AddEndPoint(_explicitEndPoint);
- await proxyServer.StartAsync(cancellationToken: stoppingToken);
+ //await proxyServer.StartAsync(cancellationToken: stoppingToken);
// run first-run setup on macOS
FirstRunSetup();
- ExplicitProxyEndPoint? explicitProxyEndPoint = null;
+ //ExplicitProxyEndPoint? explicitProxyEndPoint = null;
- foreach (var endPoint in proxyServer.ProxyEndPoints)
- {
- _logger.LogInformation("Dev Proxy listening on {IPAddress}:{Port}...", endPoint.IpAddress, endPoint.Port);
- if (explicitProxyEndPoint is null && endPoint is ExplicitProxyEndPoint explicitProxyEnd)
- {
- explicitProxyEndPoint = explicitProxyEnd;
- }
- }
+ //foreach (var endPoint in proxyServer.ProxyEndPoints)
+ //{
+ // _logger.LogInformation("Dev Proxy listening on {IPAddress}:{Port}...", endPoint.IpAddress, endPoint.Port);
+ // if (explicitProxyEndPoint is null && endPoint is ExplicitProxyEndPoint explicitProxyEnd)
+ // {
+ // explicitProxyEndPoint = explicitProxyEnd;
+ // }
+ //}
if (_config.AsSystemProxy)
{
- if (RunTime.IsWindows)
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
- proxyServer.SetAsSystemHttpProxy(explicitProxyEndPoint!);
- proxyServer.SetAsSystemHttpsProxy(explicitProxyEndPoint!);
+ //TODO: Implement Windows system proxy toggle
}
- else if (RunTime.IsMac)
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
ToggleSystemProxy(ToggleSystemProxyAction.On, _config.IPAddress, _config.Port);
}
@@ -185,7 +183,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
try
{
- while (!stoppingToken.IsCancellationRequested && proxyServer.ProxyRunning)
+ while (!stoppingToken.IsCancellationRequested)
{
while (!Console.KeyAvailable)
{
@@ -203,7 +201,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
private void FirstRunSetup()
{
- if (!RunTime.IsMac ||
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ||
_config.NoFirstRun ||
!HasRunFlag.CreateIfMissing() ||
!_config.InstallCert)
@@ -321,24 +319,11 @@ private void StopProxy()
//{
// _explicitEndPoint.BeforeTunnelConnectRequest -= OnBeforeTunnelConnectRequestAsync;
//}
-
- if (proxyServer is not null) // Irrelevant is always defined
- {
- //ProxyServer.BeforeRequest -= OnRequestAsync;
- //ProxyServer.BeforeResponse -= OnBeforeResponseAsync;
- //ProxyServer.AfterResponse -= OnAfterResponseAsync;
- proxyServer.ServerCertificateValidationCallback -= OnCertificateValidationAsync;
- proxyServer.ClientCertificateSelectionCallback -= OnCertificateSelectionAsync;
-
- if (proxyServer.ProxyRunning)
- {
- proxyServer.Stop();
- }
- }
+ // proxyServer is stopped automatically when the service is stopped
_inactivityTimer?.Stop();
- if (RunTime.IsMac && _config.AsSystemProxy)
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && _config.AsSystemProxy)
{
ToggleSystemProxy(ToggleSystemProxyAction.Off);
}
@@ -357,18 +342,7 @@ public override async Task StopAsync(CancellationToken cancellationToken)
await base.StopAsync(cancellationToken);
}
- async Task OnBeforeTunnelConnectRequestAsync(object sender, TunnelConnectSessionEventArgs e)
- {
- // Ensures that only the targeted Https domains are proxyied
- if (!IsProxiedHost(e.HttpClient.Request.RequestUri.Host) ||
- !IsProxiedProcess(e))
- {
- e.DecryptSsl = false;
- }
- await Task.CompletedTask;
- }
-
- private bool IsProxiedProcess(TunnelConnectSessionEventArgs e)
+ private bool IsProxiedProcess(ClientDetails clientDetails)
{
// If no process names or IDs are specified, we proxy all processes
if (!_config.WatchPids.Any() &&
@@ -377,14 +351,13 @@ private bool IsProxiedProcess(TunnelConnectSessionEventArgs e)
return true;
}
- var processId = GetProcessId(e);
+ var processId = GetProcessId(clientDetails);
if (processId == -1)
{
return false;
}
- if (_config.WatchPids.Any() &&
- _config.WatchPids.Contains(processId))
+ if (_config.WatchPids.Contains(processId))
{
return true;
}
@@ -401,110 +374,105 @@ private bool IsProxiedProcess(TunnelConnectSessionEventArgs e)
return false;
}
- async Task OnRequestAsync(object sender, SessionEventArgs e)
+ async Task OnRequestAsync(object _, RequestEventArguments requestEventArguments, CancellationToken cancellationToken)
{
_inactivityTimer?.Reset();
- if (IsProxiedHost(e.HttpClient.Request.RequestUri.Host) &&
- IsIncludedByHeaders(e.HttpClient.Request.Headers))
+ if (IsProxiedHost(requestEventArguments.Request.RequestUri!.Host) &&
+ IsIncludedByHeaders(requestEventArguments.Request.Headers))
{
- if (!_pluginData.TryAdd(e.GetHashCode(), []))
- {
- throw new InvalidOperationException($"Unable to initialize the plugin data storage for hash key {e.GetHashCode()}");
- }
- var responseState = new ResponseState();
- var proxyRequestArgs = new ProxyRequestArgs(e, responseState)
- {
- SessionData = _pluginData[e.GetHashCode()],
- GlobalData = _proxyController.ProxyState.GlobalData
- };
- if (!proxyRequestArgs.HasRequestUrlMatch(_urlsToWatch))
+ if (!_pluginData.TryAdd(requestEventArguments.RequestId, []))
{
- return;
+ throw new InvalidOperationException($"Unable to initialize the plugin data storage for hash key {requestEventArguments.RequestId}");
}
- // we need to keep the request body for further processing
- // by plugins
- e.HttpClient.Request.KeepBody = true;
- if (e.HttpClient.Request.HasBody)
+ if (!ProxyUtils.MatchesUrlToWatch(_urlsToWatch, requestEventArguments.Request.RequestUri.AbsoluteUri))
{
- _ = await e.GetRequestBodyAsString();
+ return RequestEventResponse.ContinueResponse();
}
- using var scope = _logger.BeginScope(e.HttpClient.Request.Method ?? "", e.HttpClient.Request.Url, e.GetHashCode());
-
- e.UserData = e.HttpClient.Request;
-
- var loggingContext = new LoggingContext(e);
- _logger.LogRequest($"{e.HttpClient.Request.Method} {e.HttpClient.Request.Url}", MessageType.InterceptedRequest, loggingContext);
- _logger.LogRequest($"{DateTimeOffset.UtcNow}", MessageType.Timestamp, loggingContext);
-
- await HandleRequestAsync(e, proxyRequestArgs);
- }
- }
-
- // Unobtanium Request handler
- private async Task OnRequestAsync(object sender, RequestEventArguments e, CancellationToken cancellationToken)
- {
- // Distributed tracing
- using var activity = ActivitySource.StartActivity(nameof(OnRequestAsync), ActivityKind.Server, e.RequestActivity?.Context ?? default);
- var uri = e.Request.RequestUri!;
- var hashCode = e.RequestActivity?.GetHashCode() ?? e.Request.GetHashCode();
- if (IsProxiedHost(uri.Host)) // && IsIncludedByHeaders??
- {
-
- if (!_pluginData.TryAdd(hashCode, []))
+ if (!_pluginData.TryAdd(requestEventArguments.RequestId, []))
{
// Throwing here will break the request....
- throw new InvalidOperationException($"Unable to initialize the plugin data storage for hash key {hashCode}");
+ throw new InvalidOperationException($"Unable to initialize the plugin data storage for hash key {requestEventArguments.RequestId}");
}
- // If I start this scope I no longer see logs appear
- using var scope = _logger.BeginRequestScope(e.Request.Method, uri, hashCode);
+ using var scope = _logger.BeginRequestScope(requestEventArguments.Request.Method, requestEventArguments.Request.RequestUri, requestEventArguments.RequestId);
- _logger.LogInformation("OnRequestAsync {method} {url}", e.Request.Method, uri);
- // This is definity not using structured logging...
- _logger.LogRequest($"{e.Request.Method} {uri}", MessageType.InterceptedRequest, e.Request.Method.Method, uri.ToString()); //Logging context?
+ //var loggingContext = new LoggingContext(e);
+ _logger.LogRequest($"{requestEventArguments.Request.Method} {requestEventArguments.Request.RequestUri}", MessageType.InterceptedRequest, requestEventArguments.Request);
+ _logger.LogRequest($"{DateTimeOffset.UtcNow}", MessageType.Timestamp, requestEventArguments.Request);
- // Execute the required plugins here and decide what to return.
- //return RequestEventResponse.EarlyResponse(new HttpResponseMessage(System.Net.HttpStatusCode.UnavailableForLegalReasons));
+ return await HandleRequestAsync(requestEventArguments, cancellationToken);
}
- _logger.LogRequest("Done", MessageType.FinishedProcessingRequest);
+
return RequestEventResponse.ContinueResponse();
}
- private async Task HandleRequestAsync(SessionEventArgs e, ProxyRequestArgs proxyRequestArgs)
+ private async Task HandleRequestAsync(RequestEventArguments arguments, CancellationToken cancellationToken)
{
- foreach (var plugin in _plugins.Where(p => p.Enabled))
- {
- _cancellationToken?.ThrowIfCancellationRequested();
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken ?? CancellationToken.None);
+ HttpResponseMessage? response = null;
+ HttpRequestMessage? request = null;
+ foreach (var plugin in _plugins.Where(p => p.Enabled && p.OnRequestAsync is not null))
+ {
+ cts.Token.ThrowIfCancellationRequested();
try
{
- await plugin.BeforeRequestAsync(proxyRequestArgs, _cancellationToken ?? CancellationToken.None);
+ var result = await plugin.OnRequestAsync!(new Abstractions.Models.RequestArguments(arguments.Request), cts.Token);
+ if (result is not null)
+ {
+ if (result.Request is not null)
+ {
+ request = result.Request;
+ // TODO: Decide what to do in this case, continue processing or return the request?
+ }
+ else if (result.Response is not null)
+ {
+ response = result.Response;
+ // TODO: Decide what to do in this case, continue processing or return the response?
+ }
+ else
+ {
+ // If both are null, we continue processing
+ continue;
+ }
+ }
}
catch (Exception ex)
{
- ExceptionHandler(ex);
+ _logger.LogError(ex, "An error occurred in plugin {PluginName} while processing request {RequestMethod} {RequestUrl}",
+ plugin.Name, arguments.Request.Method, arguments.Request.RequestUri);
+
}
}
// We only need to set the proxy header if the proxy has not set a response and the request is going to be sent to the target.
- if (!proxyRequestArgs.ResponseState.HasBeenSet)
+ if (response is not null)
+ {
+ _ = _pluginData.Remove(arguments.RequestId, out _);
+ return RequestEventResponse.EarlyResponse(response);
+ }
+ else if (request is not null)
{
- _logger?.LogRequest("Passed through", MessageType.PassedThrough, new(e));
- AddProxyHeader(e.HttpClient.Request);
+ // If the request is modified, we need to add the Via header
+ AddProxyHeader(request);
+ // We can return the request to be sent to the target
+ return RequestEventResponse.ModifyRequest(request);
}
+ // If no plugins modified the request, we add the Via header to the original request
+ AddProxyHeader(arguments.Request);
+ return RequestEventResponse.ModifyRequest(arguments.Request);
}
private bool IsProxiedHost(string hostName)
{
- return true;
var urlMatch = _hostsToWatch.FirstOrDefault(h => h.Url.IsMatch(hostName));
return urlMatch is not null && !urlMatch.Exclude;
}
- private bool IsIncludedByHeaders(HeaderCollection requestHeaders)
+ private bool IsIncludedByHeaders(HttpRequestHeaders requestHeaders)
{
if (_config.FilterByHeaders is null)
{
@@ -518,7 +486,7 @@ private bool IsIncludedByHeaders(HeaderCollection requestHeaders)
string.IsNullOrEmpty(header.Value) ? "(any)" : header.Value
);
- if (requestHeaders.HeaderExists(header.Name))
+ if (requestHeaders.Contains(header.Name))
{
if (string.IsNullOrEmpty(header.Value))
{
@@ -526,7 +494,7 @@ private bool IsIncludedByHeaders(HeaderCollection requestHeaders)
return true;
}
- if (requestHeaders.GetHeaders(header.Name)!.Any(h => h.Value.Contains(header.Value, StringComparison.OrdinalIgnoreCase)))
+ if (requestHeaders.Any(h => h.Key.Equals(header.Name, StringComparison.OrdinalIgnoreCase) && (h.Value.ToString()?.Equals(header.Value, StringComparison.OrdinalIgnoreCase) ?? false)))
{
_logger.LogDebug("Request header {Header} contains value {Value}", header.Name, header.Value);
return true;
@@ -542,125 +510,154 @@ private bool IsIncludedByHeaders(HeaderCollection requestHeaders)
return false;
}
- // Modify response
- // OnBeforeResponseAsync is no longer supported, where was this used for?
- async Task OnBeforeResponseAsync(object sender, SessionEventArgs e)
- {
- // read response headers
- if (IsProxiedHost(e.HttpClient.Request.RequestUri.Host))
- {
- var proxyResponseArgs = new ProxyResponseArgs(e, new())
- {
- SessionData = _pluginData[e.GetHashCode()],
- GlobalData = _proxyController.ProxyState.GlobalData
- };
- if (!proxyResponseArgs.HasRequestUrlMatch(_urlsToWatch))
- {
- return;
- }
-
- using var scope = _logger.BeginScope(e.HttpClient.Request.Method ?? "", e.HttpClient.Request.Url, e.GetHashCode());
-
- // necessary to make the response body available to plugins
- e.HttpClient.Response.KeepBody = true;
- if (e.HttpClient.Response.HasBody)
- {
- _ = await e.GetResponseBody();
- }
-
- foreach (var plugin in _plugins.Where(p => p.Enabled))
- {
- _cancellationToken?.ThrowIfCancellationRequested();
-
- try
- {
- await plugin.BeforeResponseAsync(proxyResponseArgs, _cancellationToken ?? CancellationToken.None);
- }
- catch (Exception ex)
- {
- ExceptionHandler(ex);
- }
- }
- }
- }
-
- async Task OnAfterResponseAsync(object sender, SessionEventArgs e)
- {
- // read response headers
- if (IsProxiedHost(e.HttpClient.Request.RequestUri.Host))
- {
- var proxyResponseArgs = new ProxyResponseArgs(e, new())
- {
- SessionData = _pluginData[e.GetHashCode()],
- GlobalData = _proxyController.ProxyState.GlobalData
- };
- if (!proxyResponseArgs.HasRequestUrlMatch(_urlsToWatch))
- {
- // clean up
- _ = _pluginData.Remove(e.GetHashCode(), out _);
- return;
- }
-
- // necessary to repeat to make the response body
- // of mocked requests available to plugins
- e.HttpClient.Response.KeepBody = true;
-
- using var scope = _logger.BeginScope(e.HttpClient.Request.Method ?? "", e.HttpClient.Request.Url, e.GetHashCode());
-
- var message = $"{e.HttpClient.Request.Method} {e.HttpClient.Request.Url}";
- var loggingContext = new LoggingContext(e);
- _logger.LogRequest(message, MessageType.InterceptedResponse, loggingContext);
-
- foreach (var plugin in _plugins.Where(p => p.Enabled))
- {
- _cancellationToken?.ThrowIfCancellationRequested();
-
- try
- {
- await plugin.AfterResponseAsync(proxyResponseArgs, _cancellationToken ?? CancellationToken.None);
- }
- catch (Exception ex)
- {
- ExceptionHandler(ex);
- }
- }
-
- _logger.LogRequest(message, MessageType.FinishedProcessingRequest, loggingContext);
+ //// Modify response
+ //// OnBeforeResponseAsync is no longer supported, where was this used for?
+ //async Task OnBeforeResponseAsync(object sender, SessionEventArgs e)
+ //{
+ // // read response headers
+ // if (IsProxiedHost(e.HttpClient.Request.RequestUri.Host))
+ // {
+ // var proxyResponseArgs = new ProxyResponseArgs(e, new())
+ // {
+ // SessionData = _pluginData[e.GetHashCode()],
+ // GlobalData = _proxyController.ProxyState.GlobalData
+ // };
+ // if (!proxyResponseArgs.HasRequestUrlMatch(_urlsToWatch))
+ // {
+ // return;
+ // }
+
+ // using var scope = _logger.BeginScope(e.HttpClient.Request.Method ?? "", e.HttpClient.Request.Url, e.GetHashCode());
+
+ // // necessary to make the response body available to plugins
+ // e.HttpClient.Response.KeepBody = true;
+ // if (e.HttpClient.Response.HasBody)
+ // {
+ // _ = await e.GetResponseBody();
+ // }
+
+ // foreach (var plugin in _plugins.Where(p => p.Enabled))
+ // {
+ // _cancellationToken?.ThrowIfCancellationRequested();
+
+ // try
+ // {
+ // await plugin.BeforeResponseAsync(proxyResponseArgs, _cancellationToken ?? CancellationToken.None);
+ // }
+ // catch (Exception ex)
+ // {
+ // ExceptionHandler(ex);
+ // }
+ // }
+ // }
+ //}
- // clean up
- _ = _pluginData.Remove(e.GetHashCode(), out _);
- }
- }
+ //async Task OnAfterResponseAsync(object sender, SessionEventArgs e)
+ //{
+ // // read response headers
+ // if (IsProxiedHost(e.HttpClient.Request.RequestUri.Host))
+ // {
+ // var proxyResponseArgs = new ProxyResponseArgs(e, new())
+ // {
+ // SessionData = _pluginData[e.GetHashCode()],
+ // GlobalData = _proxyController.ProxyState.GlobalData
+ // };
+ // if (!proxyResponseArgs.HasRequestUrlMatch(_urlsToWatch))
+ // {
+ // // clean up
+ // _ = _pluginData.Remove(e.GetHashCode(), out _);
+ // return;
+ // }
+
+ // // necessary to repeat to make the response body
+ // // of mocked requests available to plugins
+ // e.HttpClient.Response.KeepBody = true;
+
+ // using var scope = _logger.BeginScope(e.HttpClient.Request.Method ?? "", e.HttpClient.Request.Url, e.GetHashCode());
+
+ // var message = $"{e.HttpClient.Request.Method} {e.HttpClient.Request.Url}";
+ // var loggingContext = new LoggingContext(e);
+ // _logger.LogRequest(message, MessageType.InterceptedResponse, loggingContext);
+
+ // foreach (var plugin in _plugins.Where(p => p.Enabled))
+ // {
+ // _cancellationToken?.ThrowIfCancellationRequested();
+
+ // try
+ // {
+ // await plugin.AfterResponseAsync(proxyResponseArgs, _cancellationToken ?? CancellationToken.None);
+ // }
+ // catch (Exception ex)
+ // {
+ // ExceptionHandler(ex);
+ // }
+ // }
+
+ // _logger.LogRequest(message, MessageType.FinishedProcessingRequest, loggingContext);
+
+ // // clean up
+ // _ = _pluginData.Remove(e.GetHashCode(), out _);
+ // }
+ //}
// Unobtanium ResponseHandler
private async Task OnResponseAsync(object sender, ResponseEventArguments e, CancellationToken cancellationToken)
{
// Distributed tracing
using var activity = ActivitySource.StartActivity(nameof(OnResponseAsync), ActivityKind.Consumer, e.RequestActivity?.Context ?? default);
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken ?? CancellationToken.None);
var uri = e.Request.RequestUri!;
- if (IsProxiedHost(uri.Host)) // This is already checked but lets mimic the existing
+
+ using var scope = _logger.BeginRequestScope(e.Request.Method, uri, e.RequestId);
+ var message = $"{e.Request.Method} {e.Response}";
+ _logger.LogRequest(message, MessageType.InterceptedResponse, e.Request);
+ HttpResponseMessage? response = null;
+
+ foreach (var plugin in _plugins.Where(p => p.Enabled && p.OnResponseAsync is not null))
{
+ cts.Token.ThrowIfCancellationRequested();
+ try
+ {
+ var result = await plugin.OnResponseAsync!(e, cts.Token);
+ if (result is not null)
+ {
+ if (result.ModifiedResponse is not null)
+ {
+ response = result.ModifiedResponse;
+ // Maybe exit the loop here?
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "An error occurred in plugin {PluginName} while processing response {ResponseStatusCode} for request {RequestMethod} {RequestUrl}",
+ plugin.Name, e.Response.StatusCode, e.Request.Method, uri);
+ }
}
- return ResponseEventResponse.ContinueResponse();
+ _logger.LogRequest(message, MessageType.FinishedProcessingRequest, e.Request);
+ _ = _pluginData.Remove(e.RequestId, out _);
+ return response is not null
+ ? ResponseEventResponse.ModifyResponse(response)
+ : ResponseEventResponse.ContinueResponse();
}
- // Allows overriding default certificate validation logic
- Task OnCertificateValidationAsync(object sender, CertificateValidationEventArgs e)
- {
- // set IsValid to true/false based on Certificate Errors
- if (e.SslPolicyErrors == System.Net.Security.SslPolicyErrors.None)
- {
- e.IsValid = true;
- }
+ //// Allows overriding default certificate validation logic
+ //Task OnCertificateValidationAsync(object sender, CertificateValidationEventArgs e)
+ //{
+ // // set IsValid to true/false based on Certificate Errors
+ // if (e.SslPolicyErrors == System.Net.Security.SslPolicyErrors.None)
+ // {
+ // e.IsValid = true;
+ // }
- return Task.CompletedTask;
- }
+ // return Task.CompletedTask;
+ //}
- // Allows overriding default client certificate selection logic during mutual authentication
- Task OnCertificateSelectionAsync(object sender, CertificateSelectionEventArgs e) =>
- // set e.clientCertificate to override
- Task.CompletedTask;
+ //// Allows overriding default client certificate selection logic during mutual authentication
+ //Task OnCertificateSelectionAsync(object sender, CertificateSelectionEventArgs e) =>
+ // // set e.clientCertificate to override
+ // Task.CompletedTask;
private static void PrintHotkeys()
{
@@ -694,17 +691,17 @@ private static void ToggleSystemProxy(ToggleSystemProxyAction toggle, string? ip
process.WaitForExit();
}
- private static int GetProcessId(TunnelConnectSessionEventArgs e)
+ private static int GetProcessId(ClientDetails e)
{
- if (RunTime.IsWindows)
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
- return e.HttpClient.ProcessId.Value;
+ return -1;
}
var psi = new ProcessStartInfo
{
FileName = "lsof",
- Arguments = $"-i :{e.ClientRemoteEndPoint?.Port}",
+ Arguments = $"-i :{e.Port}",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
@@ -718,7 +715,7 @@ private static int GetProcessId(TunnelConnectSessionEventArgs e)
proc.WaitForExit();
var lines = output.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
- var matchingLine = lines.FirstOrDefault(l => l.Contains($"{e.ClientRemoteEndPoint?.Port}->", StringComparison.OrdinalIgnoreCase));
+ var matchingLine = lines.FirstOrDefault(l => l.Contains($"{e.Port}->", StringComparison.OrdinalIgnoreCase));
if (matchingLine is null)
{
return -1;
@@ -732,7 +729,7 @@ private static int GetProcessId(TunnelConnectSessionEventArgs e)
return int.TryParse(pidString, out var pid) ? pid : -1;
}
- private static void AddProxyHeader(Request r) => r.Headers?.AddHeader("Via", $"{r.HttpVersion} dev-proxy/{ProxyUtils.ProductVersion}");
+ private static void AddProxyHeader(HttpRequestMessage r) => r.Headers.TryAddWithoutValidation("Via", $"dev-proxy/{ProxyUtils.ProductVersion}");
public override void Dispose()
{
diff --git a/DevProxy/Proxy/ProxyStateController.cs b/DevProxy/Proxy/ProxyStateController.cs
index afe5481b..a4c6c01d 100644
--- a/DevProxy/Proxy/ProxyStateController.cs
+++ b/DevProxy/Proxy/ProxyStateController.cs
@@ -4,7 +4,6 @@
using DevProxy.Abstractions.Plugins;
using DevProxy.Abstractions.Proxy;
-using Unobtanium.Web.Proxy;
namespace DevProxy.Proxy;
@@ -21,7 +20,6 @@ sealed class ProxyStateController(
private readonly IEnumerable _plugins = plugins;
private readonly IHostApplicationLifetime _hostApplicationLifetime = hostApplicationLifetime;
private readonly ILogger _logger = logger;
- private ExceptionHandler ExceptionHandler => ex => _logger.LogError(ex, "An error occurred in a plugin");
public void StartRecording()
{
@@ -61,7 +59,7 @@ public async Task StopRecordingAsync(CancellationToken cancellationToken)
}
catch (Exception ex)
{
- ExceptionHandler(ex);
+ _logger.LogError(ex, "Error in plugin {PluginName} after recording stop", plugin.Name);
}
}
_logger.LogInformation("DONE");
@@ -79,7 +77,7 @@ public async Task MockRequestAsync(CancellationToken cancellationToken)
}
catch (Exception ex)
{
- ExceptionHandler(ex);
+ _logger.LogError(ex, "Error in plugin {PluginName} after mock request", plugin.Name);
}
}
}
diff --git a/DevProxy/packages.lock.json b/DevProxy/packages.lock.json
index 8601bc87..ab7bccbc 100644
--- a/DevProxy/packages.lock.json
+++ b/DevProxy/packages.lock.json
@@ -181,14 +181,11 @@
},
"Unobtanium.Web.Proxy": {
"type": "Direct",
- "requested": "[0.9.0-beta.1, )",
- "resolved": "0.9.0-beta.1",
- "contentHash": "Ae/2f7v3T3NQkRknBhAHqBrY85nJHQr8lxRD/j8B3rMK4KbIlbGe3A6Hd71ponq+sNHnLxa3BPSg8pdldc9RHg==",
+ "requested": "[0.9.1-beta-2, )",
+ "resolved": "0.9.1-beta-2",
+ "contentHash": "/nRDmaQQ9xRqWiSXf4mAN32anOM/e8aZcnD/Kty376d1JliqiKmR58sHS/yO8LITCo2eSGzH+cobm+e8MmpTPQ==",
"dependencies": {
- "BouncyCastle.Cryptography": "2.4.0",
- "Microsoft.Extensions.Logging.Abstractions": "8.0.3",
- "System.Runtime.CompilerServices.Unsafe": "6.0.0",
- "Unobtanium.Web.Proxy.Events": "0.9.0-beta.1"
+ "Unobtanium.Web.Proxy.Events": "0.9.1-beta-2"
}
},
"Azure.Core": {
@@ -201,11 +198,6 @@
"System.Memory.Data": "6.0.1"
}
},
- "BouncyCastle.Cryptography": {
- "type": "Transitive",
- "resolved": "2.4.0",
- "contentHash": "SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ=="
- },
"Markdig": {
"type": "Transitive",
"resolved": "0.41.3",
@@ -658,11 +650,6 @@
"resolved": "6.0.1",
"contentHash": "yliDgLh9S9Mcy5hBIdZmX6yphYIW3NH+3HN1kV1m7V1e0s7LNTw/tHNjJP4U9nSMEgl3w1TzYv/KA1Tg9NYy6w=="
},
- "System.Runtime.CompilerServices.Unsafe": {
- "type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
- },
"System.Security.Cryptography.ProtectedData": {
"type": "Transitive",
"resolved": "4.5.0",
@@ -675,8 +662,8 @@
},
"Unobtanium.Web.Proxy.Events": {
"type": "Transitive",
- "resolved": "0.9.0-beta.1",
- "contentHash": "ckgncJ95Tr1GCx3Kv3yJK3sANfJznKIHOqZebqR0w3JblHrNF2VLJ5ALA5YZ5CcO7QuqqXVnfkusapy7QJRFwA==",
+ "resolved": "0.9.1-beta-2",
+ "contentHash": "OaDCVpnDYah/2DDqAwyhFwl/wt1Ggat8yGXF8GqtLDuUqxv/cYtowjI6ZMnKmLM/Bkz6gMM4lSITY0cO0rPVgA==",
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "8.0.1"
}
@@ -699,7 +686,7 @@
"Newtonsoft.Json.Schema": "[4.0.1, )",
"Scriban": "[6.2.1, )",
"System.CommandLine": "[2.0.0-beta5.25306.1, )",
- "Unobtanium.Web.Proxy": "[0.9.0-beta.1, )",
+ "Unobtanium.Web.Proxy.Events": "[0.9.1-beta.2, )",
"YamlDotNet": "[16.3.0, )"
}
}
From 0633c3b920b95e982033391a56a91a3ce749ac15 Mon Sep 17 00:00:00 2001
From: Stephan van Rooij <1292510+svrooij@users.noreply.github.com>
Date: Mon, 18 Aug 2025 16:53:37 +0200
Subject: [PATCH 03/14] First plugin migrated
---
.../Models/RequestArguments.cs | 3 +-
.../Models/ResponseArguments.cs | 3 +-
DevProxy.Abstractions/Plugins/BasePlugin.cs | 6 +-
DevProxy.Abstractions/Plugins/IPlugin.cs | 4 +-
DevProxy.Abstractions/Utils/ProxyUtils.cs | 7 +
.../Behavior/GraphRandomErrorPlugin.cs | 325 ++++++++++--------
DevProxy.Plugins/Utils/GraphUtils.cs | 3 +-
DevProxy.Plugins/packages.lock.json | 22 +-
DevProxy/Proxy/ProxyEngine.cs | 16 +-
9 files changed, 216 insertions(+), 173 deletions(-)
diff --git a/DevProxy.Abstractions/Models/RequestArguments.cs b/DevProxy.Abstractions/Models/RequestArguments.cs
index 9c5f5f17..8c2a232a 100644
--- a/DevProxy.Abstractions/Models/RequestArguments.cs
+++ b/DevProxy.Abstractions/Models/RequestArguments.cs
@@ -1,5 +1,6 @@
namespace DevProxy.Abstractions.Models;
-public class RequestArguments(HttpRequestMessage request)
+public class RequestArguments(HttpRequestMessage request, string requestId)
{
public HttpRequestMessage Request { get; } = request;
+ public string RequestId { get; } = requestId ?? throw new ArgumentNullException(nameof(requestId));
}
diff --git a/DevProxy.Abstractions/Models/ResponseArguments.cs b/DevProxy.Abstractions/Models/ResponseArguments.cs
index cc45c2a7..e888299c 100644
--- a/DevProxy.Abstractions/Models/ResponseArguments.cs
+++ b/DevProxy.Abstractions/Models/ResponseArguments.cs
@@ -1,6 +1,7 @@
namespace DevProxy.Abstractions.Models;
-public class ResponseArguments(HttpRequestMessage httpRequestMessage, HttpResponseMessage httpResponseMessage)
+public class ResponseArguments(HttpRequestMessage httpRequestMessage, HttpResponseMessage httpResponseMessage, string requestId)
{
public HttpRequestMessage HttpRequestMessage { get; } = httpRequestMessage;
public HttpResponseMessage HttpResponseMessage { get; } = httpResponseMessage;
+ public string RequestId { get; } = requestId ?? throw new ArgumentNullException(nameof(requestId));
}
diff --git a/DevProxy.Abstractions/Plugins/BasePlugin.cs b/DevProxy.Abstractions/Plugins/BasePlugin.cs
index eab92cb1..e4e36893 100644
--- a/DevProxy.Abstractions/Plugins/BasePlugin.cs
+++ b/DevProxy.Abstractions/Plugins/BasePlugin.cs
@@ -21,11 +21,11 @@ public abstract class BasePlugin(
{
public bool Enabled { get; protected set; } = true;
protected ILogger Logger { get; } = logger;
- protected ISet UrlsToWatch { get; } = urlsToWatch;
+ public ISet UrlsToWatch { get; } = urlsToWatch;
public abstract string Name { get; }
- public Func>? OnRequestAsync { get; set; }
- public Func>? OnResponseAsync { get; set; }
+ public virtual Func>? OnRequestAsync { get; }
+ public virtual Func>? OnResponseAsync { get; }
public virtual Option[] GetOptions() => [];
public virtual Command[] GetCommands() => [];
diff --git a/DevProxy.Abstractions/Plugins/IPlugin.cs b/DevProxy.Abstractions/Plugins/IPlugin.cs
index 8c63b8f4..1f084991 100644
--- a/DevProxy.Abstractions/Plugins/IPlugin.cs
+++ b/DevProxy.Abstractions/Plugins/IPlugin.cs
@@ -20,8 +20,8 @@ public interface IPlugin
Task InitializeAsync(InitArgs e, CancellationToken cancellationToken);
void OptionsLoaded(OptionsLoadedArgs e);
- Func>? OnRequestAsync { get; set; }
- Func>? OnResponseAsync { get; set; }
+ Func>? OnRequestAsync { get; }
+ Func>? OnResponseAsync { get; }
Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken cancellationToken);
Task BeforeResponseAsync(ProxyResponseArgs e, CancellationToken cancellationToken);
Task AfterResponseAsync(ProxyResponseArgs e, CancellationToken cancellationToken);
diff --git a/DevProxy.Abstractions/Utils/ProxyUtils.cs b/DevProxy.Abstractions/Utils/ProxyUtils.cs
index 55828e26..a10c0ba1 100644
--- a/DevProxy.Abstractions/Utils/ProxyUtils.cs
+++ b/DevProxy.Abstractions/Utils/ProxyUtils.cs
@@ -430,6 +430,13 @@ public static void MergeHeaders(IList allHeaders, IList watchedUrls, Uri? url, bool evaluateWildcards = false)
+ {
+ ArgumentNullException.ThrowIfNull(watchedUrls);
+ ArgumentNullException.ThrowIfNull(url);
+ return MatchesUrlToWatch(watchedUrls, url!.AbsoluteUri, evaluateWildcards);
+ }
+
public static bool MatchesUrlToWatch(ISet watchedUrls, string url, bool evaluateWildcards = false)
{
ArgumentNullException.ThrowIfNull(watchedUrls);
diff --git a/DevProxy.Plugins/Behavior/GraphRandomErrorPlugin.cs b/DevProxy.Plugins/Behavior/GraphRandomErrorPlugin.cs
index 7ec347a8..4579c9bb 100644
--- a/DevProxy.Plugins/Behavior/GraphRandomErrorPlugin.cs
+++ b/DevProxy.Plugins/Behavior/GraphRandomErrorPlugin.cs
@@ -15,8 +15,6 @@
using System.Net;
using System.Text.Json;
using System.Text.RegularExpressions;
-using Unobtanium.Web.Proxy.Http;
-using Unobtanium.Web.Proxy.Models;
using DevProxy.Plugins.Models;
namespace DevProxy.Plugins.Behavior;
@@ -50,6 +48,7 @@ public sealed class GraphRandomErrorPlugin(
private const string _allowedErrorsOptionName = "--allowed-errors";
private const string _rateOptionName = "--failure-rate";
+
private readonly Dictionary _methodStatusCode = new()
{
{
@@ -168,166 +167,218 @@ public override void OptionsLoaded(OptionsLoadedArgs e)
}
}
- public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken cancellationToken)
+ public override Func>? OnRequestAsync => (args, cancellationToken) =>
{
- Logger.LogTrace("{Method} called", nameof(BeforeRequestAsync));
-
- ArgumentNullException.ThrowIfNull(e);
+ ArgumentNullException.ThrowIfNull(args);
+ Logger.LogTrace("{Method} called", nameof(OnRequestAsync));
- var state = e.ResponseState;
- if (state.HasBeenSet)
+ if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.Request.RequestUri))
{
- Logger.LogRequest("Response already set", MessageType.Skipped, new(e.Session));
- return Task.CompletedTask;
- }
- if (!e.HasRequestUrlMatch(UrlsToWatch))
- {
- Logger.LogRequest("URL not matched", MessageType.Skipped, new(e.Session));
- return Task.CompletedTask;
+ Logger.LogRequest("URL not matched", MessageType.Skipped, args.Request);
+ return Task.FromResult(PluginResponse.Continue());
}
var failMode = ShouldFail();
if (failMode == GraphRandomErrorFailMode.PassThru && Configuration.Rate != 100)
{
- Logger.LogRequest("Pass through", MessageType.Skipped, new(e.Session));
- return Task.CompletedTask;
+ Logger.LogRequest("Pass through", MessageType.Skipped, args.Request);
+ return Task.FromResult(PluginResponse.Continue());
}
- if (ProxyUtils.IsGraphBatchUrl(e.Session.HttpClient.Request.RequestUri))
+ // If the request is a batch request, we will handle it in BeforeRequestAsync
+ if (ProxyUtils.IsGraphBatchUrl(args.Request.RequestUri!))
{
- FailBatch(e);
+ //TODO: Build batch failure response
+ return Task.FromResult(PluginResponse.Continue());
}
else
{
- FailResponse(e);
+ //Logger.LogRequest("Pass through", MessageType.Skipped, args.Request);
+ return Task.FromResult(PluginResponse.Respond(FailResponse(args.Request)));
}
- state.HasBeenSet = true;
+ };
- Logger.LogTrace("Left {Name}", nameof(BeforeRequestAsync));
- return Task.CompletedTask;
- }
+ //public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken cancellationToken)
+ //{
+ // Logger.LogTrace("{Method} called", nameof(BeforeRequestAsync));
+
+ // ArgumentNullException.ThrowIfNull(e);
+
+ // var state = e.ResponseState;
+ // if (state.HasBeenSet)
+ // {
+ // Logger.LogRequest("Response already set", MessageType.Skipped, new(e.Session));
+ // return Task.CompletedTask;
+ // }
+ // if (!e.HasRequestUrlMatch(UrlsToWatch)) // ProxyUtils.MatchesUrlToWatch(watchedUrls, Session.HttpClient.Request.RequestUri.AbsoluteUri);
+ // {
+ // Logger.LogRequest("URL not matched", MessageType.Skipped, new(e.Session));
+ // return Task.CompletedTask;
+ // }
+
+ // var failMode = ShouldFail();
+ // if (failMode == GraphRandomErrorFailMode.PassThru && Configuration.Rate != 100)
+ // {
+ // Logger.LogRequest("Pass through", MessageType.Skipped, new(e.Session));
+ // return Task.CompletedTask;
+ // }
+ // if (ProxyUtils.IsGraphBatchUrl(e.Session.HttpClient.Request.RequestUri))
+ // {
+ // FailBatch(e);
+ // }
+ // else
+ // {
+ // //FailResponse(e);
+ // }
+ // state.HasBeenSet = true;
+
+ // Logger.LogTrace("Left {Name}", nameof(BeforeRequestAsync));
+ // return Task.CompletedTask;
+ //}
// uses config to determine if a request should be failed
private GraphRandomErrorFailMode ShouldFail() => _random.Next(1, 100) <= Configuration.Rate ? GraphRandomErrorFailMode.Random : GraphRandomErrorFailMode.PassThru;
- private void FailResponse(ProxyRequestArgs e)
- {
- // pick a random error response for the current request method
- var methodStatusCodes = _methodStatusCode[e.Session.HttpClient.Request.Method ?? "GET"];
- var errorStatus = methodStatusCodes[_random.Next(0, methodStatusCodes.Length)];
- UpdateProxyResponse(e, errorStatus);
- }
+ //private void FailResponse(ProxyRequestArgs e)
+ //{
+ // // pick a random error response for the current request method
+ // var methodStatusCodes = _methodStatusCode[e.Session.HttpClient.Request.Method ?? "GET"];
+ // var errorStatus = methodStatusCodes[_random.Next(0, methodStatusCodes.Length)];
+ // UpdateProxyResponse(e, errorStatus);
+ //}
- private void FailBatch(ProxyRequestArgs e)
+ private HttpResponseMessage FailResponse(HttpRequestMessage e)
{
- var batchResponse = new GraphBatchResponsePayload();
-
- var batch = JsonSerializer.Deserialize(e.Session.HttpClient.Request.BodyString, ProxyUtils.JsonSerializerOptions);
- if (batch == null)
- {
- UpdateProxyBatchResponse(e, batchResponse);
- return;
- }
-
- var responses = new List();
- foreach (var request in batch.Requests)
+ var methodStatusCodes = _methodStatusCode[e.Method.Method ?? "GET"];
+ var errorStatus = methodStatusCodes[_random.Next(0, methodStatusCodes.Length)];
+ var response = new HttpResponseMessage(errorStatus)
{
- try
- {
- // pick a random error response for the current request method
- var methodStatusCodes = _methodStatusCode[request.Method];
- var errorStatus = methodStatusCodes[_random.Next(0, methodStatusCodes.Length)];
-
- var response = new GraphBatchResponsePayloadResponse
+ Content = new StringContent(JsonSerializer.Serialize(new GraphErrorResponseBody(
+ new()
{
- Id = request.Id,
- Status = (int)errorStatus,
- Body = new GraphBatchResponsePayloadResponseBody
+ Code = new Regex("([A-Z])").Replace(errorStatus.ToString(), m => { return $" {m.Groups[1]}"; }).Trim(),
+ Message = BuildApiErrorMessage(e),
+ InnerError = new()
{
- Error = new()
- {
- Code = new Regex("([A-Z])").Replace(errorStatus.ToString(), m => { return $" {m.Groups[1]}"; }).Trim(),
- Message = "Some error was generated by the proxy.",
- }
+ RequestId = Guid.NewGuid().ToString(),
+ Date = DateTime.Now.ToString(CultureInfo.CurrentCulture)
}
- };
-
- if (errorStatus == HttpStatusCode.TooManyRequests)
- {
- var retryAfterDate = DateTime.Now.AddSeconds(Configuration.RetryAfterInSeconds);
- var requestUrl = ProxyUtils.GetAbsoluteRequestUrlFromBatch(e.Session.HttpClient.Request.RequestUri, request.Url);
- var throttledRequests = e.GlobalData[RetryAfterPlugin.ThrottledRequestsKey] as List;
- throttledRequests?.Add(new(GraphUtils.BuildThrottleKey(requestUrl), ShouldThrottle, retryAfterDate));
- response.Headers = new() { { "Retry-After", Configuration.RetryAfterInSeconds.ToString(CultureInfo.InvariantCulture) } };
- }
-
- responses.Add(response);
- }
- catch { }
- }
- batchResponse.Responses = [.. responses];
-
- UpdateProxyBatchResponse(e, batchResponse);
- }
-
- private ThrottlingInfo ShouldThrottle(Request request, string throttlingKey)
- {
- var throttleKeyForRequest = GraphUtils.BuildThrottleKey(request);
- return new(throttleKeyForRequest == throttlingKey ? Configuration.RetryAfterInSeconds : 0, "Retry-After");
- }
-
- private void UpdateProxyResponse(ProxyRequestArgs e, HttpStatusCode errorStatus)
- {
- var session = e.Session;
- var requestId = Guid.NewGuid().ToString();
- var requestDate = DateTime.Now.ToString(CultureInfo.CurrentCulture);
- var request = session.HttpClient.Request;
- var headers = ProxyUtils.BuildGraphResponseHeaders(request, requestId, requestDate);
- if (errorStatus == HttpStatusCode.TooManyRequests)
- {
- var retryAfterDate = DateTime.Now.AddSeconds(Configuration.RetryAfterInSeconds);
- if (!e.GlobalData.TryGetValue(RetryAfterPlugin.ThrottledRequestsKey, out var value))
- {
- value = new List();
- e.GlobalData.Add(RetryAfterPlugin.ThrottledRequestsKey, value);
- }
-
- var throttledRequests = value as List;
- throttledRequests?.Add(new(GraphUtils.BuildThrottleKey(request), ShouldThrottle, retryAfterDate));
- headers.Add(new("Retry-After", Configuration.RetryAfterInSeconds.ToString(CultureInfo.InvariantCulture)));
- }
-
- var body = JsonSerializer.Serialize(new GraphErrorResponseBody(
- new()
- {
- Code = new Regex("([A-Z])").Replace(errorStatus.ToString(), m => { return $" {m.Groups[1]}"; }).Trim(),
- Message = BuildApiErrorMessage(request),
- InnerError = new()
- {
- RequestId = requestId,
- Date = requestDate
- }
- }),
- ProxyUtils.JsonSerializerOptions
- );
- Logger.LogRequest($"{(int)errorStatus} {errorStatus}", MessageType.Chaos, new(e.Session));
- session.GenericResponse(body ?? string.Empty, errorStatus, headers.Select(h => new HttpHeader(h.Name, h.Value)));
- }
-
- private void UpdateProxyBatchResponse(ProxyRequestArgs ev, GraphBatchResponsePayload response)
- {
- // failed batch uses a fixed 424 error status code
- var errorStatus = HttpStatusCode.FailedDependency;
-
- var session = ev.Session;
- var requestId = Guid.NewGuid().ToString();
- var requestDate = DateTime.Now.ToString(CultureInfo.CurrentCulture);
- var request = session.HttpClient.Request;
- var headers = ProxyUtils.BuildGraphResponseHeaders(request, requestId, requestDate);
-
- var body = JsonSerializer.Serialize(response, ProxyUtils.JsonSerializerOptions);
- Logger.LogRequest($"{(int)errorStatus} {errorStatus}", MessageType.Chaos, new(ev.Session));
- session.GenericResponse(body, errorStatus, headers.Select(h => new HttpHeader(h.Name, h.Value)));
+ }), ProxyUtils.JsonSerializerOptions))
+ };
+ Logger.LogRequest($"{(int)errorStatus} {errorStatus}", MessageType.Chaos, e);
+ return response;
}
- private static string BuildApiErrorMessage(Request r) => $"Some error was generated by the proxy. {(ProxyUtils.IsGraphRequest(r) ? ProxyUtils.IsSdkRequest(r) ? "" : string.Join(' ', MessageUtils.BuildUseSdkForErrorsMessage()) : "")}";
+ //private void FailBatch(ProxyRequestArgs e)
+ //{
+ // var batchResponse = new GraphBatchResponsePayload();
+
+ // var batch = JsonSerializer.Deserialize(e.Session.HttpClient.Request.BodyString, ProxyUtils.JsonSerializerOptions);
+ // if (batch == null)
+ // {
+ // UpdateProxyBatchResponse(e, batchResponse);
+ // return;
+ // }
+
+ // var responses = new List();
+ // foreach (var request in batch.Requests)
+ // {
+ // try
+ // {
+ // // pick a random error response for the current request method
+ // var methodStatusCodes = _methodStatusCode[request.Method];
+ // var errorStatus = methodStatusCodes[_random.Next(0, methodStatusCodes.Length)];
+
+ // var response = new GraphBatchResponsePayloadResponse
+ // {
+ // Id = request.Id,
+ // Status = (int)errorStatus,
+ // Body = new GraphBatchResponsePayloadResponseBody
+ // {
+ // Error = new()
+ // {
+ // Code = new Regex("([A-Z])").Replace(errorStatus.ToString(), m => { return $" {m.Groups[1]}"; }).Trim(),
+ // Message = "Some error was generated by the proxy.",
+ // }
+ // }
+ // };
+
+ // if (errorStatus == HttpStatusCode.TooManyRequests)
+ // {
+ // var retryAfterDate = DateTime.Now.AddSeconds(Configuration.RetryAfterInSeconds);
+ // var requestUrl = ProxyUtils.GetAbsoluteRequestUrlFromBatch(e.Session.HttpClient.Request.RequestUri, request.Url);
+ // var throttledRequests = e.GlobalData[RetryAfterPlugin.ThrottledRequestsKey] as List;
+ // throttledRequests?.Add(new(GraphUtils.BuildThrottleKey(requestUrl), ShouldThrottle, retryAfterDate));
+ // response.Headers = new() { { "Retry-After", Configuration.RetryAfterInSeconds.ToString(CultureInfo.InvariantCulture) } };
+ // }
+
+ // responses.Add(response);
+ // }
+ // catch { }
+ // }
+ // batchResponse.Responses = [.. responses];
+
+ // UpdateProxyBatchResponse(e, batchResponse);
+ //}
+
+ //private ThrottlingInfo ShouldThrottle(HttpRequestMessage request, string throttlingKey)
+ //{
+ // var throttleKeyForRequest = GraphUtils.BuildThrottleKey(request);
+ // return new(throttleKeyForRequest == throttlingKey ? Configuration.RetryAfterInSeconds : 0, "Retry-After");
+ //}
+
+ //private void UpdateProxyResponse(ProxyRequestArgs e, HttpStatusCode errorStatus)
+ //{
+ // var session = e.Session;
+ // var requestId = Guid.NewGuid().ToString();
+ // var requestDate = DateTime.Now.ToString(CultureInfo.CurrentCulture);
+ // var request = session.HttpClient.Request;
+ // var headers = ProxyUtils.BuildGraphResponseHeaders(request, requestId, requestDate);
+ // if (errorStatus == HttpStatusCode.TooManyRequests)
+ // {
+ // var retryAfterDate = DateTime.Now.AddSeconds(Configuration.RetryAfterInSeconds);
+ // if (!e.GlobalData.TryGetValue(RetryAfterPlugin.ThrottledRequestsKey, out var value))
+ // {
+ // value = new List();
+ // e.GlobalData.Add(RetryAfterPlugin.ThrottledRequestsKey, value);
+ // }
+
+ // var throttledRequests = value as List;
+ // throttledRequests?.Add(new(GraphUtils.BuildThrottleKey(request), ShouldThrottle, retryAfterDate));
+ // headers.Add(new("Retry-After", Configuration.RetryAfterInSeconds.ToString(CultureInfo.InvariantCulture)));
+ // }
+
+ // var body = JsonSerializer.Serialize(new GraphErrorResponseBody(
+ // new()
+ // {
+ // Code = new Regex("([A-Z])").Replace(errorStatus.ToString(), m => { return $" {m.Groups[1]}"; }).Trim(),
+ // Message = BuildApiErrorMessage(request),
+ // InnerError = new()
+ // {
+ // RequestId = requestId,
+ // Date = requestDate
+ // }
+ // }),
+ // ProxyUtils.JsonSerializerOptions
+ // );
+ // Logger.LogRequest($"{(int)errorStatus} {errorStatus}", MessageType.Chaos, new(e.Session));
+ // session.GenericResponse(body ?? string.Empty, errorStatus, headers.Select(h => new HttpHeader(h.Name, h.Value)));
+ //}
+
+ //private void UpdateProxyBatchResponse(ProxyRequestArgs ev, GraphBatchResponsePayload response)
+ //{
+ // // failed batch uses a fixed 424 error status code
+ // var errorStatus = HttpStatusCode.FailedDependency;
+
+ // var session = ev.Session;
+ // var requestId = Guid.NewGuid().ToString();
+ // var requestDate = DateTime.Now.ToString(CultureInfo.CurrentCulture);
+ // var request = session.HttpClient.Request;
+ // var headers = ProxyUtils.BuildGraphResponseHeaders(request, requestId, requestDate);
+
+ // var body = JsonSerializer.Serialize(response, ProxyUtils.JsonSerializerOptions);
+ // Logger.LogRequest($"{(int)errorStatus} {errorStatus}", MessageType.Chaos, new(ev.Session));
+ // session.GenericResponse(body, errorStatus, headers.Select(h => new HttpHeader(h.Name, h.Value)));
+ //}
+
+ private static string BuildApiErrorMessage(HttpRequestMessage r) => $"Some error was generated by the proxy. {(ProxyUtils.IsGraphRequest(r) ? ProxyUtils.IsSdkRequest(r) ? "" : string.Join(' ', MessageUtils.BuildUseSdkForErrorsMessage()) : "")}";
}
diff --git a/DevProxy.Plugins/Utils/GraphUtils.cs b/DevProxy.Plugins/Utils/GraphUtils.cs
index ccd2ecb2..70843438 100644
--- a/DevProxy.Plugins/Utils/GraphUtils.cs
+++ b/DevProxy.Plugins/Utils/GraphUtils.cs
@@ -5,7 +5,6 @@
using DevProxy.Plugins.Models;
using Microsoft.Extensions.Logging;
using System.Net.Http.Json;
-using Unobtanium.Web.Proxy.Http;
namespace DevProxy.Plugins.Utils;
@@ -17,7 +16,7 @@ sealed class GraphUtils(
private readonly ILogger _logger = logger;
// throttle requests per workload
- public static string BuildThrottleKey(Request r) => BuildThrottleKey(r.RequestUri);
+ public static string BuildThrottleKey(HttpRequestMessage r) => BuildThrottleKey(r.RequestUri!);
public static string BuildThrottleKey(Uri uri)
{
diff --git a/DevProxy.Plugins/packages.lock.json b/DevProxy.Plugins/packages.lock.json
index 1180752a..f6b6631b 100644
--- a/DevProxy.Plugins/packages.lock.json
+++ b/DevProxy.Plugins/packages.lock.json
@@ -120,11 +120,6 @@
"System.Threading.Tasks.Extensions": "4.5.4"
}
},
- "BouncyCastle.Cryptography": {
- "type": "Transitive",
- "resolved": "2.4.0",
- "contentHash": "SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ=="
- },
"Markdig": {
"type": "Transitive",
"resolved": "0.41.3",
@@ -543,21 +538,10 @@
"resolved": "4.5.4",
"contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg=="
},
- "Unobtanium.Web.Proxy": {
- "type": "Transitive",
- "resolved": "0.9.0-beta.1",
- "contentHash": "Ae/2f7v3T3NQkRknBhAHqBrY85nJHQr8lxRD/j8B3rMK4KbIlbGe3A6Hd71ponq+sNHnLxa3BPSg8pdldc9RHg==",
- "dependencies": {
- "BouncyCastle.Cryptography": "2.4.0",
- "Microsoft.Extensions.Logging.Abstractions": "8.0.3",
- "System.Runtime.CompilerServices.Unsafe": "6.0.0",
- "Unobtanium.Web.Proxy.Events": "0.9.0-beta.1"
- }
- },
"Unobtanium.Web.Proxy.Events": {
"type": "Transitive",
- "resolved": "0.9.0-beta.1",
- "contentHash": "ckgncJ95Tr1GCx3Kv3yJK3sANfJznKIHOqZebqR0w3JblHrNF2VLJ5ALA5YZ5CcO7QuqqXVnfkusapy7QJRFwA==",
+ "resolved": "0.9.1-beta.2",
+ "contentHash": "8vO+KuRBp/UsvNtUtsKx2hdBGoYzPu2wQoAcQL63UnT6hphPCdqm2mt6JNgH6bprI4f9cbn/O3ny+EP5ftEV0Q==",
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "8.0.1"
}
@@ -580,7 +564,7 @@
"Newtonsoft.Json.Schema": "[4.0.1, )",
"Scriban": "[6.2.1, )",
"System.CommandLine": "[2.0.0-beta5.25306.1, )",
- "Unobtanium.Web.Proxy": "[0.9.0-beta.1, )",
+ "Unobtanium.Web.Proxy.Events": "[0.9.1-beta.2, )",
"YamlDotNet": "[16.3.0, )"
}
}
diff --git a/DevProxy/Proxy/ProxyEngine.cs b/DevProxy/Proxy/ProxyEngine.cs
index 50e3a0fa..2a2575d2 100755
--- a/DevProxy/Proxy/ProxyEngine.cs
+++ b/DevProxy/Proxy/ProxyEngine.cs
@@ -415,12 +415,15 @@ private async Task HandleRequestAsync(RequestEventArgument
HttpResponseMessage? response = null;
HttpRequestMessage? request = null;
- foreach (var plugin in _plugins.Where(p => p.Enabled && p.OnRequestAsync is not null))
+ foreach (var plugin in _plugins
+ .Where(p =>
+ p.Enabled
+ && p.OnRequestAsync is not null)) // Only plugins that have OnRequestAsync defined, maybe pre-select matches based on url?
{
cts.Token.ThrowIfCancellationRequested();
try
{
- var result = await plugin.OnRequestAsync!(new Abstractions.Models.RequestArguments(arguments.Request), cts.Token);
+ var result = await plugin.OnRequestAsync!(new Abstractions.Models.RequestArguments(arguments.Request, arguments.RequestId), cts.Token);
if (result is not null)
{
if (result.Request is not null)
@@ -431,12 +434,9 @@ private async Task HandleRequestAsync(RequestEventArgument
else if (result.Response is not null)
{
response = result.Response;
- // TODO: Decide what to do in this case, continue processing or return the response?
- }
- else
- {
- // If both are null, we continue processing
- continue;
+ // Plugins no longer have to check if the response is already been set.
+ // If a plugin sets a response, it is expected to be the final response.
+ break;
}
}
}
From e107b22350d77b3fe02a5a9047d28152e77112f2 Mon Sep 17 00:00:00 2001
From: Stephan van Rooij <1292510+svrooij@users.noreply.github.com>
Date: Tue, 19 Aug 2025 11:10:32 +0200
Subject: [PATCH 04/14] Added log-only methods in interface and generated
inventory and migration guide
---
DevProxy.Abstractions/Plugins/BasePlugin.cs | 13 +
DevProxy.Abstractions/Plugins/IPlugin.cs | 13 +
DevProxy.Plugins/inventory.md | 427 +++++++++++++++++
DevProxy.Plugins/migration.md | 504 ++++++++++++++++++++
DevProxy/Proxy/ProxyEngine.cs | 21 +
5 files changed, 978 insertions(+)
create mode 100644 DevProxy.Plugins/inventory.md
create mode 100644 DevProxy.Plugins/migration.md
diff --git a/DevProxy.Abstractions/Plugins/BasePlugin.cs b/DevProxy.Abstractions/Plugins/BasePlugin.cs
index e4e36893..905aa8ae 100644
--- a/DevProxy.Abstractions/Plugins/BasePlugin.cs
+++ b/DevProxy.Abstractions/Plugins/BasePlugin.cs
@@ -24,7 +24,20 @@ public abstract class BasePlugin(
public ISet UrlsToWatch { get; } = urlsToWatch;
public abstract string Name { get; }
+
+ ///
+ /// Implement this to handle requests, if you won't be modifying requests or respond, use .
+ ///
public virtual Func>? OnRequestAsync { get; }
+
+ ///
+ /// Implement this to log requests, you cannot modify the request or response here.
+ ///
+ public virtual Func? OnRequestLogAsync { get; }
+
+ ///
+ /// Implement this to modify responses from the remote server.
+ ///
public virtual Func>? OnResponseAsync { get; }
public virtual Option[] GetOptions() => [];
diff --git a/DevProxy.Abstractions/Plugins/IPlugin.cs b/DevProxy.Abstractions/Plugins/IPlugin.cs
index 1f084991..718f74b5 100644
--- a/DevProxy.Abstractions/Plugins/IPlugin.cs
+++ b/DevProxy.Abstractions/Plugins/IPlugin.cs
@@ -20,7 +20,20 @@ public interface IPlugin
Task InitializeAsync(InitArgs e, CancellationToken cancellationToken);
void OptionsLoaded(OptionsLoadedArgs e);
+
+ ///
+ /// Implement this to handle requests.
+ ///
Func>? OnRequestAsync { get; }
+
+ ///
+ /// Implement this to log requests, you cannot modify the request or response here.
+ ///
+ Func? OnRequestLogAsync { get; }
+
+ ///
+ /// Implement this to modify responses from the remote server.
+ ///
Func>? OnResponseAsync { get; }
Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken cancellationToken);
Task BeforeResponseAsync(ProxyResponseArgs e, CancellationToken cancellationToken);
diff --git a/DevProxy.Plugins/inventory.md b/DevProxy.Plugins/inventory.md
new file mode 100644
index 00000000..f9e1c74f
--- /dev/null
+++ b/DevProxy.Plugins/inventory.md
@@ -0,0 +1,427 @@
+# DevProxy Plugins Inventory
+
+This document provides an inventory of all plugins in the DevProxy.Plugins project and their implemented methods. This inventory was created to assist with the migration from the old event-based API to the new functional API.
+
+## ApiCenterMinimalPermissionsPlugin
+
+**Base Class:** BaseReportingPlugin
+**Methods Implemented:**
+- InitializeAsync
+- BeforeRequestAsync
+
+**Behavior:** Read-only (collects API permission data for reporting)
+
+## ApiCenterOnboardingPlugin
+
+**Base Class:** BaseReportingPlugin
+**Methods Implemented:**
+- InitializeAsync
+- BeforeRequestAsync
+
+**Behavior:** Read-only (collects API metadata for reporting)
+
+## ApiCenterProductionVersionPlugin
+
+**Base Class:** BaseReportingPlugin
+**Methods Implemented:**
+- InitializeAsync
+- BeforeRequestAsync
+
+**Behavior:** Read-only (collects API version information for reporting)
+
+## AuthPlugin
+
+**Base Class:** BasePlugin
+**Methods Implemented:**
+- InitializeAsync
+- BeforeRequestAsync
+
+**Behavior:** Modifies responses (returns 401/403 for unauthorized requests)
+
+## CachingGuidancePlugin
+
+**Base Class:** BasePlugin
+**Methods Implemented:**
+- BeforeRequestAsync
+
+**Behavior:** Read-only (analyzes request patterns and provides caching guidance)
+
+## CrudApiPlugin
+
+**Base Class:** BasePlugin
+**Methods Implemented:**
+- GetOptions
+- OptionsLoaded
+- InitializeAsync
+- BeforeRequestAsync
+
+**Behavior:** Modifies responses (creates mock CRUD API responses)
+
+## DevToolsPlugin
+
+**Base Class:** BasePlugin
+**Methods Implemented:**
+- GetOptions
+- OptionsLoaded
+- InitializeAsync
+- BeforeRequestAsync
+- BeforeResponseAsync
+- AfterResponseAsync
+- AfterRequestLogAsync
+
+**Behavior:** Read-only (captures request/response data for developer tools)
+
+## EntraMockResponsePlugin
+
+**Base Class:** BasePlugin
+**Methods Implemented:**
+- InitializeAsync
+- BeforeRequestAsync
+
+**Behavior:** Modifies responses (provides mock Entra ID responses)
+
+## ExecutionSummaryPlugin
+
+**Base Class:** BaseReportingPlugin
+**Methods Implemented:**
+- GetOptions
+- OptionsLoaded
+- InitializeAsync
+- BeforeRequestAsync
+- AfterRecordingStopAsync
+
+**Behavior:** Read-only (collects execution statistics for reporting)
+
+## GenericRandomErrorPlugin
+
+**Base Class:** BasePlugin
+**Methods Implemented:**
+- GetOptions
+- OptionsLoaded
+- InitializeAsync
+- BeforeRequestAsync
+
+**Behavior:** Modifies responses (generates random error responses)
+
+## GraphBetaSupportGuidancePlugin
+
+**Base Class:** BasePlugin
+**Methods Implemented:**
+- AfterResponseAsync
+
+**Behavior:** Read-only (provides guidance about beta API usage)
+
+## GraphClientRequestIdGuidancePlugin
+
+**Base Class:** BasePlugin
+**Methods Implemented:**
+- AfterResponseAsync
+
+**Behavior:** Read-only (provides guidance about request ID headers)
+
+## GraphConnectorGuidancePlugin
+
+**Base Class:** BasePlugin
+**Methods Implemented:**
+- AfterResponseAsync
+
+**Behavior:** Read-only (provides guidance about Graph connector usage)
+
+## GraphMinimalPermissionsGuidancePlugin
+
+**Base Class:** BaseReportingPlugin
+**Methods Implemented:**
+- InitializeAsync
+- BeforeRequestAsync
+
+**Behavior:** Read-only (analyzes and reports on Graph API permissions)
+
+## GraphMinimalPermissionsPlugin
+
+**Base Class:** BaseReportingPlugin
+**Methods Implemented:**
+- InitializeAsync
+- BeforeRequestAsync
+
+**Behavior:** Read-only (collects Graph API permission data for reporting)
+
+## GraphMockResponsePlugin
+
+**Base Class:** MockResponsePlugin
+**Methods Implemented:**
+- BeforeRequestAsync
+
+**Behavior:** Modifies responses (provides mock Graph API responses, including batch processing)
+
+## GraphRandomErrorPlugin
+
+**Base Class:** BasePlugin
+**Methods Implemented:**
+- GetOptions
+- OptionsLoaded
+- OnRequestAsync (NEW API - MIGRATED)
+
+**Behavior:** Modifies responses (generates random Graph API error responses)
+
+## GraphSdkGuidancePlugin
+
+**Base Class:** BasePlugin
+**Methods Implemented:**
+- AfterResponseAsync
+
+**Behavior:** Read-only (provides guidance about using Graph SDKs)
+
+## GraphSelectGuidancePlugin
+
+**Base Class:** BasePlugin
+**Methods Implemented:**
+- AfterResponseAsync
+
+**Behavior:** Read-only (provides guidance about Graph $select optimization)
+
+## HttpFileGeneratorPlugin
+
+**Base Class:** BaseReportingPlugin
+**Methods Implemented:**
+- InitializeAsync
+- BeforeRequestAsync
+- AfterRecordingStopAsync
+
+**Behavior:** Read-only (generates HTTP files from recorded requests)
+
+## LanguageModelFailurePlugin
+
+**Base Class:** BasePlugin
+**Methods Implemented:**
+- InitializeAsync
+- BeforeRequestAsync
+
+**Behavior:** Modifies responses (simulates AI/ML service failures)
+
+## LanguageModelRateLimitingPlugin
+
+**Base Class:** BasePlugin
+**Methods Implemented:**
+- InitializeAsync
+- BeforeRequestAsync
+- BeforeResponseAsync
+
+**Behavior:** Modifies responses (enforces token-based rate limiting for AI services)
+
+## LatencyPlugin
+
+**Base Class:** BasePlugin
+**Methods Implemented:**
+- BeforeRequestAsync
+
+**Behavior:** Read-only (adds artificial delay to requests, doesn't modify responses)
+
+## MinimalCsomPermissionsPlugin
+
+**Base Class:** BaseReportingPlugin
+**Methods Implemented:**
+- InitializeAsync
+- BeforeRequestAsync
+
+**Behavior:** Read-only (analyzes SharePoint CSOM permissions for reporting)
+
+## MinimalPermissionsGuidancePlugin
+
+**Base Class:** BaseReportingPlugin
+**Methods Implemented:**
+- InitializeAsync
+- BeforeRequestAsync
+
+**Behavior:** Read-only (provides permission optimization guidance)
+
+## MinimalPermissionsPlugin
+
+**Base Class:** BaseReportingPlugin
+**Methods Implemented:**
+- InitializeAsync
+- BeforeRequestAsync
+
+**Behavior:** Read-only (collects API permission data for reporting)
+
+## MockGeneratorPlugin
+
+**Base Class:** BaseReportingPlugin
+**Methods Implemented:**
+- AfterRecordingStopAsync
+
+**Behavior:** Read-only (generates mock response files from recorded requests)
+
+## MockRequestPlugin
+
+**Base Class:** BasePlugin
+**Methods Implemented:**
+- GetOptions
+- OptionsLoaded
+- InitializeAsync
+- BeforeRequestAsync
+
+**Behavior:** Modifies responses (provides mock request/response functionality)
+
+## MockResponsePlugin
+
+**Base Class:** BasePlugin
+**Methods Implemented:**
+- GetOptions
+- OptionsLoaded
+- InitializeAsync
+- BeforeRequestAsync
+
+**Behavior:** Modifies responses (provides comprehensive mock response functionality)
+
+## ODSPSearchGuidancePlugin
+
+**Base Class:** BasePlugin
+**Methods Implemented:**
+- AfterResponseAsync
+
+**Behavior:** Read-only (provides guidance about SharePoint search optimization)
+
+## ODataPagingGuidancePlugin
+
+**Base Class:** BasePlugin
+**Methods Implemented:**
+- AfterResponseAsync
+
+**Behavior:** Read-only (provides guidance about OData paging patterns)
+
+## OpenAIMockResponsePlugin
+
+**Base Class:** BasePlugin
+**Methods Implemented:**
+- InitializeAsync
+- BeforeRequestAsync
+
+**Behavior:** Modifies responses (provides mock OpenAI API responses using local language models)
+
+## OpenAITelemetryPlugin
+
+**Base Class:** BaseReportingPlugin
+**Methods Implemented:**
+- InitializeAsync
+- BeforeRequestAsync
+
+**Behavior:** Read-only (collects OpenAI API usage telemetry for reporting)
+
+## OpenApiSpecGeneratorPlugin
+
+**Base Class:** BaseReportingPlugin
+**Methods Implemented:**
+- InitializeAsync
+- BeforeRequestAsync
+- AfterRecordingStopAsync
+
+**Behavior:** Read-only (generates OpenAPI specifications from recorded requests)
+
+## RateLimitingPlugin
+
+**Base Class:** BasePlugin
+**Methods Implemented:**
+- GetOptions
+- OptionsLoaded
+- InitializeAsync
+- BeforeRequestAsync
+- BeforeResponseAsync
+
+**Behavior:** Modifies responses (enforces rate limits and adds rate limit headers)
+
+## RetryAfterPlugin
+
+**Base Class:** BasePlugin
+**Methods Implemented:**
+- BeforeRequestAsync
+
+**Behavior:** Modifies responses (throttles requests that don't respect Retry-After headers)
+
+## RewritePlugin
+
+**Base Class:** BasePlugin
+**Methods Implemented:**
+- InitializeAsync
+- BeforeRequestAsync
+
+**Behavior:** Modifies requests (rewrites request URLs, doesn't modify responses directly)
+
+## TypeSpecGeneratorPlugin
+
+**Base Class:** BaseReportingPlugin
+**Methods Implemented:**
+- InitializeAsync
+- BeforeRequestAsync
+- AfterRecordingStopAsync
+
+**Behavior:** Read-only (generates TypeSpec definitions from recorded requests)
+
+## UrlDiscoveryPlugin
+
+**Base Class:** BaseReportingPlugin
+**Methods Implemented:**
+- BeforeRequestAsync
+
+**Behavior:** Read-only (discovers and reports API URLs)
+
+---
+
+## Summary
+
+- **Total Plugins:** 38
+- **Plugins using BeforeRequestAsync:** 32
+- **Plugins using BeforeResponseAsync:** 2 (DevToolsPlugin, RateLimitingPlugin)
+- **Plugins using AfterResponseAsync:** 9
+- **Plugins using AfterRequestLogAsync:** 1 (DevToolsPlugin)
+- **Plugins using AfterRecordingStopAsync:** 4
+- **Plugins using OnRequestAsync (NEW API):** 1 (GraphRandomErrorPlugin - already migrated)
+- **Plugins using OnResponseAsync (NEW API):** 0
+
+### Behavior Classification
+
+**Response Modifying Plugins (17):** These plugins intercept requests and return custom responses
+- AuthPlugin
+- CrudApiPlugin
+- EntraMockResponsePlugin
+- GenericRandomErrorPlugin
+- GraphMockResponsePlugin
+- GraphRandomErrorPlugin
+- LanguageModelFailurePlugin
+- LanguageModelRateLimitingPlugin
+- MockRequestPlugin
+- MockResponsePlugin
+- OpenAIMockResponsePlugin
+- RateLimitingPlugin
+- RetryAfterPlugin
+
+**Request Modifying Plugins (1):** These plugins modify requests before they proceed
+- RewritePlugin
+
+**Read-Only Analysis Plugins (20):** These plugins only read request/response data for analysis, guidance, or reporting
+- ApiCenterMinimalPermissionsPlugin
+- ApiCenterOnboardingPlugin
+- ApiCenterProductionVersionPlugin
+- CachingGuidancePlugin
+- DevToolsPlugin
+- ExecutionSummaryPlugin
+- GraphBetaSupportGuidancePlugin
+- GraphClientRequestIdGuidancePlugin
+- GraphConnectorGuidancePlugin
+- GraphMinimalPermissionsGuidancePlugin
+- GraphMinimalPermissionsPlugin
+- GraphSdkGuidancePlugin
+- GraphSelectGuidancePlugin
+- HttpFileGeneratorPlugin
+- LatencyPlugin
+- MinimalCsomPermissionsPlugin
+- MinimalPermissionsGuidancePlugin
+- MinimalPermissionsPlugin
+- MockGeneratorPlugin
+- ODSPSearchGuidancePlugin
+- ODataPagingGuidancePlugin
+- OpenAITelemetryPlugin
+- OpenApiSpecGeneratorPlugin
+- TypeSpecGeneratorPlugin
+- UrlDiscoveryPlugin
+
+**Migration Priority:** Response modifying plugins should be migrated first as they have the most complex logic for creating and returning custom responses. Read-only plugins can potentially remain on the old API longer since they don't affect request flow.
\ No newline at end of file
diff --git a/DevProxy.Plugins/migration.md b/DevProxy.Plugins/migration.md
new file mode 100644
index 00000000..9a2f9fbb
--- /dev/null
+++ b/DevProxy.Plugins/migration.md
@@ -0,0 +1,504 @@
+# Plugin Migration Guide: From Event-Based to Functional API
+
+This document provides detailed guidance on migrating DevProxy plugins from the old event-based API to the new functional API pattern.
+
+## Overview of API Changes
+
+The DevProxy plugin architecture is transitioning from an event-based model to a functional model for better control flow and testability.
+
+### Old API (Event-Based)
+```csharp
+public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken cancellationToken)
+{
+ // Logic to decide whether to intercept
+ if (!ShouldIntercept(e))
+ {
+ return Task.CompletedTask;
+ }
+
+ // Modify response directly through session
+ e.Session.GenericResponse(body, statusCode, headers);
+ e.ResponseState.HasBeenSet = true;
+
+ return Task.CompletedTask;
+}
+
+public override Task BeforeResponseAsync(ProxyResponseArgs e, CancellationToken cancellationToken)
+{
+ // Process response before it's sent
+ return Task.CompletedTask;
+}
+
+public override Task AfterResponseAsync(ProxyResponseArgs e, CancellationToken cancellationToken)
+{
+ // Process response after it's sent (read-only)
+ return Task.CompletedTask;
+}
+```
+
+### New API (Functional)
+```csharp
+// For plugins that need to modify requests or responses
+public override Func>? OnRequestAsync =>
+ async (args, cancellationToken) =>
+{
+ // Logic to decide whether to intercept
+ if (!ShouldIntercept(args.Request))
+ {
+ return PluginResponse.Continue();
+ }
+
+ // Create and return response
+ var response = new HttpResponseMessage(HttpStatusCode.BadRequest)
+ {
+ Content = new StringContent(body)
+ };
+
+ return PluginResponse.Respond(response);
+};
+
+// For guidance plugins that only need to log or analyze requests
+public override Func? OnRequestLogAsync =>
+ async (args, cancellationToken) =>
+{
+ // Analyze request and provide guidance
+ if (ShouldProvideGuidance(args.Request))
+ {
+ Logger.LogRequest("Guidance message", MessageType.Tip, args.Request);
+ }
+};
+
+// For plugins that need to modify responses from remote server
+public override Func>? OnResponseAsync =>
+ async (args, cancellationToken) =>
+{
+ // Process response and optionally modify it
+ // Return null to continue, or ResponseEventResponse to modify
+ return null;
+};
+```
+
+## Key Differences
+
+### 1. Input Arguments
+- **Old API:** `ProxyRequestArgs e` containing session, response state, and global data
+- **New API:** `RequestArguments args` containing `HttpRequestMessage` and `RequestId`
+
+### 2. Return Values
+- **Old API:** `Task` (void) - side effects through `e.Session` and `e.ResponseState`
+- **New API:**
+ - `Task` for `OnRequestAsync` - explicit return values to control flow
+ - `Task` for `OnRequestLogAsync` - read-only logging/analysis
+ - `Task` for `OnResponseAsync` - response modification
+
+### 3. Response Creation
+- **Old API:** Direct manipulation of session: `e.Session.GenericResponse(...)`
+- **New API:** Create and return `HttpResponseMessage`: `PluginResponse.Respond(response)`
+
+### 4. Flow Control
+- **Old API:** Check `e.ResponseState.HasBeenSet` and set it to `true`
+- **New API:** Return `PluginResponse.Continue()` or `PluginResponse.Respond(response)`
+
+### 5. Method Selection Guide
+Choose the appropriate new API method based on your plugin's behavior:
+
+- **`OnRequestAsync`**: Use for plugins that need to intercept and potentially modify or respond to requests
+- **`OnRequestLogAsync`**: Use for guidance plugins that only need to analyze requests and provide logging/guidance (cannot modify requests or responses)
+- **`OnResponseAsync`**: Use for plugins that need to modify responses from the remote server
+
+## Migration Steps
+
+### Step 1: Determine the Appropriate New Method
+
+**Response Modifying Plugins** ? Use `OnRequestAsync`:
+- MockResponsePlugin
+- AuthPlugin
+- RateLimitingPlugin
+- GenericRandomErrorPlugin
+- etc.
+
+**Guidance/Analysis Plugins** ? Use `OnRequestLogAsync`:
+- CachingGuidancePlugin
+- GraphSdkGuidancePlugin (when migrated from AfterResponseAsync)
+- UrlDiscoveryPlugin
+- Most reporting plugins
+- etc.
+
+### Step 2: Change Method Signature
+
+**For Response Modifying Plugins (OnRequestAsync):**
+```csharp
+// Before
+public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken cancellationToken)
+
+// After
+public override Func>? OnRequestAsync =>
+ async (args, cancellationToken) =>
+```
+
+**For Guidance Plugins (OnRequestLogAsync):**
+```csharp
+// Before
+public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken cancellationToken)
+
+// After
+public override Func? OnRequestLogAsync =>
+ async (args, cancellationToken) =>
+```
+
+### Step 3: Update Input Data Access
+
+**Before:**
+```csharp
+var request = e.Session.HttpClient.Request;
+var url = request.RequestUri;
+var method = request.Method;
+var body = request.BodyString;
+```
+
+**After:**
+```csharp
+var request = args.Request;
+var url = request.RequestUri;
+var method = request.Method.Method;
+var body = await request.Content.ReadAsStringAsync();
+```
+
+### Step 4: Update URL Matching Logic
+
+**Before:**
+```csharp
+if (!e.HasRequestUrlMatch(UrlsToWatch))
+{
+ Logger.LogRequest("URL not matched", MessageType.Skipped, new(e.Session));
+ return Task.CompletedTask;
+}
+```
+
+**After (OnRequestAsync):**
+```csharp
+if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.Request.RequestUri))
+{
+ Logger.LogRequest("URL not matched", MessageType.Skipped, args.Request);
+ return PluginResponse.Continue();
+}
+```
+
+**After (OnRequestLogAsync):**
+```csharp
+if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.Request.RequestUri))
+{
+ Logger.LogRequest("URL not matched", MessageType.Skipped, args.Request);
+ return;
+}
+```
+
+### Step 5: Update Response State Checking
+
+**Before:**
+```csharp
+if (e.ResponseState.HasBeenSet)
+{
+ Logger.LogRequest("Response already set", MessageType.Skipped, new(e.Session));
+ return Task.CompletedTask;
+}
+```
+
+**After:**
+```csharp
+// Not needed in new API - flow control is handled by return values
+// OnRequestAsync: Each plugin returns either Continue() or Respond()
+// OnRequestLogAsync: Cannot modify responses, so this check is irrelevant
+```
+
+### Step 6: Update Response Creation (OnRequestAsync only)
+
+**Before:**
+```csharp
+var headers = new List
+{
+ new("Content-Type", "application/json"),
+ new("X-Custom", "value")
+};
+
+e.Session.GenericResponse(jsonBody, HttpStatusCode.BadRequest, headers);
+e.ResponseState.HasBeenSet = true;
+```
+
+**After:**
+```csharp
+var response = new HttpResponseMessage(HttpStatusCode.BadRequest)
+{
+ Content = new StringContent(jsonBody, Encoding.UTF8, "application/json")
+};
+response.Headers.Add("X-Custom", "value");
+
+return PluginResponse.Respond(response);
+```
+
+### Step 7: Update Passthrough Logic
+
+**Before:**
+```csharp
+if (shouldPassThrough)
+{
+ Logger.LogRequest("Pass through", MessageType.Skipped, new(e.Session));
+ return Task.CompletedTask;
+}
+```
+
+**After (OnRequestAsync):**
+```csharp
+if (shouldPassThrough)
+{
+ Logger.LogRequest("Pass through", MessageType.Skipped, args.Request);
+ return PluginResponse.Continue();
+}
+```
+
+**After (OnRequestLogAsync):**
+```csharp
+if (shouldSkip)
+{
+ Logger.LogRequest("Skipping analysis", MessageType.Skipped, args.Request);
+ return;
+}
+```
+
+## Complete Migration Examples
+
+### Example 1: Response Modifying Plugin (OnRequestAsync)
+
+**Before (Old API):**
+```csharp
+public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken cancellationToken)
+{
+ if (!e.HasRequestUrlMatch(UrlsToWatch))
+ {
+ return Task.CompletedTask;
+ }
+
+ if (e.ResponseState.HasBeenSet)
+ {
+ return Task.CompletedTask;
+ }
+
+ if (ShouldFail())
+ {
+ var error = GetRandomError();
+ var body = JsonSerializer.Serialize(error.Body);
+ var headers = error.Headers.Select(h => new HttpHeader(h.Name, h.Value));
+
+ e.Session.GenericResponse(body, (HttpStatusCode)error.StatusCode, headers);
+ e.ResponseState.HasBeenSet = true;
+ }
+
+ return Task.CompletedTask;
+}
+```
+
+**After (New API):**
+```csharp
+public override Func>? OnRequestAsync =>
+ (args, cancellationToken) =>
+{
+ if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.Request.RequestUri))
+ {
+ return Task.FromResult(PluginResponse.Continue());
+ }
+
+ if (!ShouldFail())
+ {
+ return Task.FromResult(PluginResponse.Continue());
+ }
+
+ var error = GetRandomError();
+ var response = new HttpResponseMessage((HttpStatusCode)error.StatusCode)
+ {
+ Content = new StringContent(
+ JsonSerializer.Serialize(error.Body),
+ Encoding.UTF8,
+ "application/json"
+ )
+ };
+
+ foreach (var header in error.Headers)
+ {
+ response.Headers.Add(header.Name, header.Value);
+ }
+
+ return Task.FromResult(PluginResponse.Respond(response));
+};
+```
+
+### Example 2: Guidance Plugin (OnRequestLogAsync)
+
+**Before (Old API):**
+```csharp
+public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken cancellationToken)
+{
+ if (!e.HasRequestUrlMatch(UrlsToWatch))
+ {
+ Logger.LogRequest("URL not matched", MessageType.Skipped, new(e.Session));
+ return Task.CompletedTask;
+ }
+
+ var request = e.Session.HttpClient.Request;
+ if (ShouldProvideGuidance(request))
+ {
+ Logger.LogRequest("Consider using cache for better performance", MessageType.Tip, new(e.Session));
+ }
+
+ return Task.CompletedTask;
+}
+```
+
+**After (New API):**
+```csharp
+public override Func? OnRequestLogAsync =>
+ (args, cancellationToken) =>
+{
+ if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.Request.RequestUri))
+ {
+ Logger.LogRequest("URL not matched", MessageType.Skipped, args.Request);
+ return Task.CompletedTask;
+ }
+
+ if (ShouldProvideGuidance(args.Request))
+ {
+ Logger.LogRequest("Consider using cache for better performance", MessageType.Tip, args.Request);
+ }
+
+ return Task.CompletedTask;
+};
+```
+
+## Important Notes
+
+### 1. Logging Context
+The logging context changes from `LoggingContext(e.Session)` to just the `HttpRequestMessage`:
+```csharp
+// Old
+Logger.LogRequest("Message", MessageType.Info, new LoggingContext(e.Session));
+
+// New
+Logger.LogRequest("Message", MessageType.Info, args.Request);
+```
+
+### 2. Global Data and Session Data
+Global data and session data access patterns will need to be reviewed as they may not be available in the new API. These features may be handled differently or through dependency injection.
+
+### 3. OnRequestLogAsync Benefits
+The new `OnRequestLogAsync` method provides several advantages for guidance plugins:
+- **Better Control Flow**: The proxy can respond quickly without waiting for guidance analysis
+- **Clear Intent**: Explicitly indicates the plugin is read-only and cannot modify requests/responses
+- **Performance**: Guidance plugins don't block the request pipeline
+- **Separation of Concerns**: Clearly separates modification logic from analysis logic
+
+### 4. Async Considerations
+Both new API methods expect functions that return Tasks, so you can use async/await within the lambda:
+```csharp
+public override Func>? OnRequestAsync =>
+ async (args, cancellationToken) =>
+{
+ var data = await SomeAsyncOperation(cancellationToken);
+ // ... process data
+ return PluginResponse.Continue();
+};
+```
+
+### 5. Error Handling
+Error handling should be done within the function and appropriate responses returned:
+```csharp
+public override Func>? OnRequestAsync =>
+ async (args, cancellationToken) =>
+{
+ try
+ {
+ // Plugin logic
+ return PluginResponse.Continue();
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Error in plugin");
+ return PluginResponse.Continue(); // or return an error response
+ }
+};
+```
+
+## Migration Checklist
+
+### For Response Modifying Plugins (OnRequestAsync):
+- [ ] Update method signature from `BeforeRequestAsync` to `OnRequestAsync`
+- [ ] Change return type from `Task` to `Task`
+- [ ] Update input parameter from `ProxyRequestArgs` to `RequestArguments`
+- [ ] Replace `e.Session.HttpClient.Request` with `args.Request`
+- [ ] Replace `e.HasRequestUrlMatch()` with `ProxyUtils.MatchesUrlToWatch()`
+- [ ] Remove `e.ResponseState.HasBeenSet` checks
+- [ ] Replace `e.Session.GenericResponse()` with `HttpResponseMessage` creation
+- [ ] Replace `e.ResponseState.HasBeenSet = true` with `PluginResponse.Respond()`
+- [ ] Replace `return Task.CompletedTask` with `PluginResponse.Continue()`
+- [ ] Update logging context from `LoggingContext(e.Session)` to `args.Request`
+- [ ] Test the migrated plugin thoroughly
+
+### For Guidance Plugins (OnRequestLogAsync):
+- [ ] Update method signature from `BeforeRequestAsync` to `OnRequestLogAsync`
+- [ ] Keep return type as `Task` (no PluginResponse needed)
+- [ ] Update input parameter from `ProxyRequestArgs` to `RequestArguments`
+- [ ] Replace `e.Session.HttpClient.Request` with `args.Request`
+- [ ] Replace `e.HasRequestUrlMatch()` with `ProxyUtils.MatchesUrlToWatch()`
+- [ ] Remove any response modification logic (not allowed in OnRequestLogAsync)
+- [ ] Update logging context from `LoggingContext(e.Session)` to `args.Request`
+- [ ] Test the migrated plugin thoroughly
+
+## Plugin Migration Categorization
+
+Based on the inventory, here's how plugins should be migrated:
+
+### OnRequestAsync (Response Modifying - 17 plugins):
+1. AuthPlugin
+2. CrudApiPlugin
+3. EntraMockResponsePlugin
+4. GenericRandomErrorPlugin
+5. GraphMockResponsePlugin
+6. GraphRandomErrorPlugin (already migrated)
+7. LanguageModelFailurePlugin
+8. LanguageModelRateLimitingPlugin
+9. MockRequestPlugin
+10. MockResponsePlugin
+11. OpenAIMockResponsePlugin
+12. RateLimitingPlugin
+13. RetryAfterPlugin
+
+### OnRequestLogAsync (Guidance/Analysis - 20+ plugins):
+1. ApiCenterMinimalPermissionsPlugin
+2. ApiCenterOnboardingPlugin
+3. ApiCenterProductionVersionPlugin
+4. CachingGuidancePlugin
+5. ExecutionSummaryPlugin
+6. GraphMinimalPermissionsGuidancePlugin
+7. GraphMinimalPermissionsPlugin
+8. HttpFileGeneratorPlugin
+9. MinimalCsomPermissionsPlugin
+10. MinimalPermissionsGuidancePlugin
+11. MinimalPermissionsPlugin
+12. OpenAITelemetryPlugin
+13. OpenApiSpecGeneratorPlugin
+14. TypeSpecGeneratorPlugin
+15. UrlDiscoveryPlugin
+
+### Special Cases:
+- **RewritePlugin**: Modifies requests before they proceed (may need custom handling)
+- **DevToolsPlugin**: Uses multiple methods (BeforeRequestAsync, BeforeResponseAsync, AfterResponseAsync, AfterRequestLogAsync)
+- **LatencyPlugin**: Adds delay but doesn't modify responses (could use OnRequestLogAsync)
+
+### Plugins using AfterResponseAsync (may migrate to OnResponseAsync):
+- GraphBetaSupportGuidancePlugin
+- GraphClientRequestIdGuidancePlugin
+- GraphConnectorGuidancePlugin
+- GraphSdkGuidancePlugin
+- GraphSelectGuidancePlugin
+- ODSPSearchGuidancePlugin
+- ODataPagingGuidancePlugin
+
+The `OnRequestLogAsync` method enables better control flow by allowing the proxy to respond quickly while still providing comprehensive guidance and analysis capabilities.
\ No newline at end of file
diff --git a/DevProxy/Proxy/ProxyEngine.cs b/DevProxy/Proxy/ProxyEngine.cs
index 2a2575d2..67ccf3e4 100755
--- a/DevProxy/Proxy/ProxyEngine.cs
+++ b/DevProxy/Proxy/ProxyEngine.cs
@@ -413,6 +413,27 @@ private async Task HandleRequestAsync(RequestEventArgument
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken ?? CancellationToken.None);
+ // Plugins that don't modify the request but log it
+ // can be called in parallel, because they don't affect each other.
+ var logPlugins = _plugins.Where(p => p.Enabled && p.OnRequestLogAsync is not null);
+ if (logPlugins.Any())
+ {
+ var logArguments = new Abstractions.Models.RequestArguments(arguments.Request, arguments.RequestId);
+ // Call OnRequestLogAsync for all plugins at the same time and wait for all of them to complete
+ var logTasks = logPlugins
+ .Select(plugin => plugin.OnRequestLogAsync!(logArguments, cts.Token))
+ .ToArray();
+ try
+ {
+ await Task.WhenAll(logTasks);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "An error occurred in a plugin while logging request {RequestMethod} {RequestUrl}",
+ arguments.Request.Method, arguments.Request.RequestUri);
+ }
+ }
+
HttpResponseMessage? response = null;
HttpRequestMessage? request = null;
foreach (var plugin in _plugins
From 1d87e56d3d8439d6a265ab82021dde27a137d70e Mon Sep 17 00:00:00 2001
From: Stephan van Rooij <1292510+svrooij@users.noreply.github.com>
Date: Tue, 19 Aug 2025 16:26:27 +0200
Subject: [PATCH 05/14] Guidance plugins migrated
---
DevProxy.Abstractions/Plugins/BasePlugin.cs | 7 +-
DevProxy.Abstractions/Plugins/IPlugin.cs | 8 +-
.../Guidance/CachingGuidancePlugin.cs | 35 +--
.../GraphBetaSupportGuidancePlugin.cs | 25 +-
.../GraphClientRequestIdGuidancePlugin.cs | 32 +-
.../Guidance/GraphConnectorGuidancePlugin.cs | 43 +--
.../Guidance/GraphSdkGuidancePlugin.cs | 33 +--
.../Guidance/GraphSelectGuidancePlugin.cs | 40 +--
.../Guidance/ODSPSearchGuidancePlugin.cs | 38 ++-
.../Guidance/ODataPagingGuidancePlugin.cs | 79 ++---
DevProxy.Plugins/inventory.md | 75 +----
DevProxy.Plugins/migration.md | 274 +++++++++++++-----
DevProxy/Proxy/ProxyEngine.cs | 29 +-
13 files changed, 414 insertions(+), 304 deletions(-)
diff --git a/DevProxy.Abstractions/Plugins/BasePlugin.cs b/DevProxy.Abstractions/Plugins/BasePlugin.cs
index 905aa8ae..c5b86fb0 100644
--- a/DevProxy.Abstractions/Plugins/BasePlugin.cs
+++ b/DevProxy.Abstractions/Plugins/BasePlugin.cs
@@ -38,7 +38,12 @@ public abstract class BasePlugin(
///
/// Implement this to modify responses from the remote server.
///
- public virtual Func>? OnResponseAsync { get; }
+ public virtual Func>? OnResponseAsync { get; }
+
+ ///
+ /// Implement this to modify responses from the remote server.
+ ///
+ public virtual Func? OnResponseLogAsync { get; }
public virtual Option[] GetOptions() => [];
public virtual Command[] GetCommands() => [];
diff --git a/DevProxy.Abstractions/Plugins/IPlugin.cs b/DevProxy.Abstractions/Plugins/IPlugin.cs
index 718f74b5..00383cc3 100644
--- a/DevProxy.Abstractions/Plugins/IPlugin.cs
+++ b/DevProxy.Abstractions/Plugins/IPlugin.cs
@@ -6,7 +6,6 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.CommandLine;
-using Unobtanium.Web.Proxy.Events;
namespace DevProxy.Abstractions.Plugins;
@@ -34,7 +33,12 @@ public interface IPlugin
///
/// Implement this to modify responses from the remote server.
///
- Func>? OnResponseAsync { get; }
+ Func>? OnResponseAsync { get; }
+
+ ///
+ /// Implement this to modify responses from the remote server.
+ ///
+ Func? OnResponseLogAsync { get; }
Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken cancellationToken);
Task BeforeResponseAsync(ProxyResponseArgs e, CancellationToken cancellationToken);
Task AfterResponseAsync(ProxyResponseArgs e, CancellationToken cancellationToken);
diff --git a/DevProxy.Plugins/Guidance/CachingGuidancePlugin.cs b/DevProxy.Plugins/Guidance/CachingGuidancePlugin.cs
index a9f08dad..42508ec5 100644
--- a/DevProxy.Plugins/Guidance/CachingGuidancePlugin.cs
+++ b/DevProxy.Plugins/Guidance/CachingGuidancePlugin.cs
@@ -2,11 +2,12 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using DevProxy.Abstractions.Models;
using DevProxy.Abstractions.Proxy;
using DevProxy.Abstractions.Plugins;
+using DevProxy.Abstractions.Utils;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
-using Unobtanium.Web.Proxy.Http;
namespace DevProxy.Plugins.Guidance;
@@ -32,32 +33,32 @@ public sealed class CachingGuidancePlugin(
public override string Name => nameof(CachingGuidancePlugin);
- public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken cancellationToken)
+ public override Func? OnRequestLogAsync => (args, cancellationToken) =>
{
- Logger.LogTrace("{Method} called", nameof(BeforeRequestAsync));
+ Logger.LogTrace("{Method} called", nameof(OnRequestLogAsync));
- ArgumentNullException.ThrowIfNull(e);
+ ArgumentNullException.ThrowIfNull(args);
- if (!e.HasRequestUrlMatch(UrlsToWatch))
+ if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.Request.RequestUri))
{
- Logger.LogRequest("URL not matched", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("URL not matched", MessageType.Skipped, args.Request);
return Task.CompletedTask;
}
- if (string.Equals(e.Session.HttpClient.Request.Method, "OPTIONS", StringComparison.OrdinalIgnoreCase))
+ if (args.Request.Method == HttpMethod.Options)
{
- Logger.LogRequest("Skipping OPTIONS request", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("Skipping OPTIONS request", MessageType.Skipped, args.Request);
return Task.CompletedTask;
}
- var request = e.Session.HttpClient.Request;
- var url = request.RequestUri.AbsoluteUri;
+ var request = args.Request;
+ var url = request.RequestUri!.AbsoluteUri;
var now = DateTime.Now;
if (!_interceptedRequests.TryGetValue(url, out var value))
{
value = now;
_interceptedRequests.Add(url, value);
- Logger.LogRequest("First request", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("First request", MessageType.Skipped, args.Request);
return Task.CompletedTask;
}
@@ -65,19 +66,19 @@ public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken ca
var secondsSinceLastIntercepted = (now - lastIntercepted).TotalSeconds;
if (secondsSinceLastIntercepted <= Configuration.CacheThresholdSeconds)
{
- Logger.LogRequest(BuildCacheWarningMessage(request, Configuration.CacheThresholdSeconds, lastIntercepted), MessageType.Warning, new LoggingContext(e.Session));
+ Logger.LogRequest(BuildCacheWarningMessage(request, Configuration.CacheThresholdSeconds, lastIntercepted), MessageType.Warning, args.Request);
}
else
{
- Logger.LogRequest("Request outside of cache window", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("Request outside of cache window", MessageType.Skipped, args.Request);
}
_interceptedRequests[url] = now;
- Logger.LogTrace("Left {Name}", nameof(BeforeRequestAsync));
+ Logger.LogTrace("Left {Name}", nameof(OnRequestLogAsync));
return Task.CompletedTask;
- }
+ };
- private static string BuildCacheWarningMessage(Request r, int _warningSeconds, DateTime lastIntercepted) =>
- $"Another request to {r.RequestUri.PathAndQuery} intercepted within {_warningSeconds} seconds. Last intercepted at {lastIntercepted}. Consider using cache to avoid calling the API too often.";
+ private static string BuildCacheWarningMessage(HttpRequestMessage r, int warningSeconds, DateTime lastIntercepted) =>
+ $"Another request to {r.RequestUri!.PathAndQuery} intercepted within {warningSeconds} seconds. Last intercepted at {lastIntercepted}. Consider using cache to avoid calling the API too often.";
}
diff --git a/DevProxy.Plugins/Guidance/GraphBetaSupportGuidancePlugin.cs b/DevProxy.Plugins/Guidance/GraphBetaSupportGuidancePlugin.cs
index 9f345941..905dbe81 100644
--- a/DevProxy.Plugins/Guidance/GraphBetaSupportGuidancePlugin.cs
+++ b/DevProxy.Plugins/Guidance/GraphBetaSupportGuidancePlugin.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using DevProxy.Abstractions.Models;
using DevProxy.Abstractions.Proxy;
using DevProxy.Abstractions.Plugins;
using DevProxy.Abstractions.Utils;
@@ -15,33 +16,33 @@ public sealed class GraphBetaSupportGuidancePlugin(
{
public override string Name => nameof(GraphBetaSupportGuidancePlugin);
- public override Task AfterResponseAsync(ProxyResponseArgs e, CancellationToken cancellationToken)
+ public override Func? OnRequestLogAsync => (args, cancellationToken) =>
{
- Logger.LogTrace("{Method} called", nameof(AfterResponseAsync));
+ Logger.LogTrace("{Method} called", nameof(OnRequestLogAsync));
- ArgumentNullException.ThrowIfNull(e);
+ ArgumentNullException.ThrowIfNull(args);
- var request = e.Session.HttpClient.Request;
- if (!e.HasRequestUrlMatch(UrlsToWatch))
+ var request = args.Request;
+ if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.Request.RequestUri))
{
- Logger.LogRequest("URL not matched", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("URL not matched", MessageType.Skipped, args.Request);
return Task.CompletedTask;
}
- if (string.Equals(e.Session.HttpClient.Request.Method, "OPTIONS", StringComparison.OrdinalIgnoreCase))
+ if (args.Request.Method == HttpMethod.Options)
{
- Logger.LogRequest("Skipping OPTIONS request", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("Skipping OPTIONS request", MessageType.Skipped, args.Request);
return Task.CompletedTask;
}
if (!ProxyUtils.IsGraphBetaRequest(request))
{
- Logger.LogRequest("Not a Microsoft Graph beta request", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("Not a Microsoft Graph beta request", MessageType.Skipped, args.Request);
return Task.CompletedTask;
}
- Logger.LogRequest(BuildBetaSupportMessage(), MessageType.Warning, new(e.Session));
- Logger.LogTrace("Left {Name}", nameof(BeforeRequestAsync));
+ Logger.LogRequest(BuildBetaSupportMessage(), MessageType.Warning, args.Request);
+ Logger.LogTrace("Left {Name}", nameof(OnRequestLogAsync));
return Task.CompletedTask;
- }
+ };
private static string GetBetaSupportGuidanceUrl() => "https://aka.ms/devproxy/guidance/beta-support";
private static string BuildBetaSupportMessage() =>
diff --git a/DevProxy.Plugins/Guidance/GraphClientRequestIdGuidancePlugin.cs b/DevProxy.Plugins/Guidance/GraphClientRequestIdGuidancePlugin.cs
index b241f210..cddc5941 100644
--- a/DevProxy.Plugins/Guidance/GraphClientRequestIdGuidancePlugin.cs
+++ b/DevProxy.Plugins/Guidance/GraphClientRequestIdGuidancePlugin.cs
@@ -2,12 +2,12 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using DevProxy.Abstractions.Models;
using DevProxy.Abstractions.Plugins;
using DevProxy.Abstractions.Proxy;
using DevProxy.Abstractions.Utils;
using DevProxy.Plugins.Utils;
using Microsoft.Extensions.Logging;
-using Unobtanium.Web.Proxy.Http;
namespace DevProxy.Plugins.Guidance;
@@ -17,45 +17,45 @@ public sealed class GraphClientRequestIdGuidancePlugin(
{
public override string Name => nameof(GraphClientRequestIdGuidancePlugin);
- public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken cancellationToken)
+ public override Func? OnRequestLogAsync => (args, cancellationToken) =>
{
- Logger.LogTrace("{Method} called", nameof(BeforeRequestAsync));
+ Logger.LogTrace("{Method} called", nameof(OnRequestLogAsync));
- ArgumentNullException.ThrowIfNull(e);
+ ArgumentNullException.ThrowIfNull(args);
- var request = e.Session.HttpClient.Request;
- if (!e.HasRequestUrlMatch(UrlsToWatch))
+ var request = args.Request;
+ if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.Request.RequestUri))
{
- Logger.LogRequest("URL not matched", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("URL not matched", MessageType.Skipped, args.Request);
return Task.CompletedTask;
}
- if (string.Equals(e.Session.HttpClient.Request.Method, "OPTIONS", StringComparison.OrdinalIgnoreCase))
+ if (args.Request.Method == HttpMethod.Options)
{
- Logger.LogRequest("Skipping OPTIONS request", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("Skipping OPTIONS request", MessageType.Skipped, args.Request);
return Task.CompletedTask;
}
if (WarnNoClientRequestId(request))
{
- Logger.LogRequest(BuildAddClientRequestIdMessage(), MessageType.Warning, new(e.Session));
+ Logger.LogRequest(BuildAddClientRequestIdMessage(), MessageType.Warning, args.Request);
if (!ProxyUtils.IsSdkRequest(request))
{
- Logger.LogRequest(MessageUtils.BuildUseSdkMessage(), MessageType.Tip, new(e.Session));
+ Logger.LogRequest(MessageUtils.BuildUseSdkMessage(), MessageType.Tip, args.Request);
}
}
else
{
- Logger.LogRequest("client-request-id header present", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("client-request-id header present", MessageType.Skipped, args.Request);
}
- Logger.LogTrace("Left {Name}", nameof(BeforeRequestAsync));
+ Logger.LogTrace("Left {Name}", nameof(OnRequestLogAsync));
return Task.CompletedTask;
- }
+ };
- private static bool WarnNoClientRequestId(Request request) =>
+ private static bool WarnNoClientRequestId(HttpRequestMessage request) =>
ProxyUtils.IsGraphRequest(request) &&
- !request.Headers.HeaderExists("client-request-id");
+ !request.Headers.Contains("client-request-id");
private static string GetClientRequestIdGuidanceUrl() => "https://aka.ms/devproxy/guidance/client-request-id";
private static string BuildAddClientRequestIdMessage() =>
diff --git a/DevProxy.Plugins/Guidance/GraphConnectorGuidancePlugin.cs b/DevProxy.Plugins/Guidance/GraphConnectorGuidancePlugin.cs
index cfb5a425..a5aab005 100644
--- a/DevProxy.Plugins/Guidance/GraphConnectorGuidancePlugin.cs
+++ b/DevProxy.Plugins/Guidance/GraphConnectorGuidancePlugin.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using DevProxy.Abstractions.Models;
using DevProxy.Abstractions.Proxy;
using DevProxy.Abstractions.Plugins;
using DevProxy.Abstractions.Utils;
@@ -34,37 +35,42 @@ public sealed class GraphConnectorGuidancePlugin(
{
public override string Name => nameof(GraphConnectorGuidancePlugin);
- public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken cancellationToken)
+ public override Func? OnRequestLogAsync => async (args, cancellationToken) =>
{
- Logger.LogTrace("{Method} called", nameof(BeforeRequestAsync));
+ Logger.LogTrace("{Method} called", nameof(OnRequestLogAsync));
- ArgumentNullException.ThrowIfNull(e);
+ ArgumentNullException.ThrowIfNull(args);
- if (!e.HasRequestUrlMatch(UrlsToWatch))
+ if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.Request.RequestUri))
{
- Logger.LogRequest("URL not matched", MessageType.Skipped, new(e.Session));
- return Task.CompletedTask;
+ Logger.LogRequest("URL not matched", MessageType.Skipped, args.Request);
+ return;
}
- if (!string.Equals(e.Session.HttpClient.Request.Method, "PATCH", StringComparison.OrdinalIgnoreCase))
+ if (args.Request.Method != HttpMethod.Patch)
{
- Logger.LogRequest("Skipping non-PATCH request", MessageType.Skipped, new(e.Session));
- return Task.CompletedTask;
+ Logger.LogRequest("Skipping non-PATCH request", MessageType.Skipped, args.Request);
+ return;
}
try
{
- var schemaString = e.Session.HttpClient.Request.BodyString;
+ var schemaString = string.Empty;
+ if (args.Request.Content is not null)
+ {
+ schemaString = await args.Request.Content.ReadAsStringAsync(cancellationToken);
+ }
+
if (string.IsNullOrEmpty(schemaString))
{
- Logger.LogRequest("No schema found in the request body.", MessageType.Failed, new(e.Session));
- return Task.CompletedTask;
+ Logger.LogRequest("No schema found in the request body.", MessageType.Failed, args.Request);
+ return;
}
var schema = JsonSerializer.Deserialize(schemaString, ProxyUtils.JsonSerializerOptions);
if (schema is null || schema.Properties is null)
{
- Logger.LogRequest("Invalid schema found in the request body.", MessageType.Failed, new(e.Session));
- return Task.CompletedTask;
+ Logger.LogRequest("Invalid schema found in the request body.", MessageType.Failed, args.Request);
+ return;
}
bool hasTitle = false, hasIconUrl = false, hasUrl = false;
@@ -99,12 +105,12 @@ public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken ca
Logger.LogRequest(
$"The schema is missing the following semantic labels: {string.Join(", ", missingLabels.Where(s => !string.IsNullOrEmpty(s)))}. Ingested content might not show up in Microsoft Copilot for Microsoft 365. More information: https://aka.ms/devproxy/guidance/gc/ux",
- MessageType.Failed, new(e.Session)
+ MessageType.Failed, args.Request
);
}
else
{
- Logger.LogRequest("The schema contains all the required semantic labels.", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("The schema contains all the required semantic labels.", MessageType.Skipped, args.Request);
}
}
catch (Exception ex)
@@ -112,7 +118,6 @@ public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken ca
Logger.LogError(ex, "An error has occurred while deserializing the request body");
}
- Logger.LogTrace("Left {Name}", nameof(BeforeRequestAsync));
- return Task.CompletedTask;
- }
+ Logger.LogTrace("Left {Name}", nameof(OnRequestLogAsync));
+ };
}
diff --git a/DevProxy.Plugins/Guidance/GraphSdkGuidancePlugin.cs b/DevProxy.Plugins/Guidance/GraphSdkGuidancePlugin.cs
index 73d5b5bc..a3625732 100644
--- a/DevProxy.Plugins/Guidance/GraphSdkGuidancePlugin.cs
+++ b/DevProxy.Plugins/Guidance/GraphSdkGuidancePlugin.cs
@@ -2,12 +2,12 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using DevProxy.Abstractions.Models;
using DevProxy.Abstractions.Plugins;
using DevProxy.Abstractions.Proxy;
using DevProxy.Abstractions.Utils;
using DevProxy.Plugins.Utils;
using Microsoft.Extensions.Logging;
-using Unobtanium.Web.Proxy.Http;
namespace DevProxy.Plugins.Guidance;
@@ -17,45 +17,42 @@ public sealed class GraphSdkGuidancePlugin(
{
public override string Name => nameof(GraphSdkGuidancePlugin);
- public override Task AfterResponseAsync(ProxyResponseArgs e, CancellationToken cancellationToken)
+ public override Func? OnResponseLogAsync => (args, cancellationToken) =>
{
- Logger.LogTrace("{Method} called", nameof(AfterResponseAsync));
+ Logger.LogTrace("{Method} called", nameof(OnResponseLogAsync));
- ArgumentNullException.ThrowIfNull(e);
-
- var request = e.Session.HttpClient.Request;
- if (!e.HasRequestUrlMatch(UrlsToWatch))
+ if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.HttpRequestMessage.RequestUri))
{
- Logger.LogRequest("URL not matched", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("URL not matched", MessageType.Skipped, args.HttpRequestMessage);
return Task.CompletedTask;
}
- if (string.Equals(e.Session.HttpClient.Request.Method, "OPTIONS", StringComparison.OrdinalIgnoreCase))
+ if (args.HttpRequestMessage.Method == HttpMethod.Options)
{
- Logger.LogRequest("Skipping OPTIONS request", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("Skipping OPTIONS request", MessageType.Skipped, args.HttpRequestMessage);
return Task.CompletedTask;
}
// only show the message if there is an error.
- if (e.Session.HttpClient.Response.StatusCode >= 400)
+ if ((int)args.HttpResponseMessage.StatusCode >= 400)
{
- if (WarnNoSdk(request))
+ if (WarnNoSdk(args.HttpRequestMessage))
{
- Logger.LogRequest(MessageUtils.BuildUseSdkForErrorsMessage(), MessageType.Tip, new(e.Session));
+ Logger.LogRequest(MessageUtils.BuildUseSdkForErrorsMessage(), MessageType.Tip, args.HttpRequestMessage);
}
else
{
- Logger.LogRequest("Request issued using SDK", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("Request issued using SDK", MessageType.Skipped, args.HttpRequestMessage);
}
}
else
{
- Logger.LogRequest("Skipping non-error response", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("Skipping non-error response", MessageType.Skipped, args.HttpRequestMessage);
}
- Logger.LogTrace("Left {Name}", nameof(AfterResponseAsync));
+ Logger.LogTrace("Left {Name}", nameof(OnResponseLogAsync));
return Task.CompletedTask;
- }
+ };
- private static bool WarnNoSdk(Request request) =>
+ private static bool WarnNoSdk(HttpRequestMessage request) =>
ProxyUtils.IsGraphRequest(request) && !ProxyUtils.IsSdkRequest(request);
}
diff --git a/DevProxy.Plugins/Guidance/GraphSelectGuidancePlugin.cs b/DevProxy.Plugins/Guidance/GraphSelectGuidancePlugin.cs
index e27d7528..bd6cbfd2 100644
--- a/DevProxy.Plugins/Guidance/GraphSelectGuidancePlugin.cs
+++ b/DevProxy.Plugins/Guidance/GraphSelectGuidancePlugin.cs
@@ -3,12 +3,12 @@
// See the LICENSE file in the project root for more information.
using DevProxy.Abstractions.Data;
+using DevProxy.Abstractions.Models;
using DevProxy.Abstractions.Proxy;
using DevProxy.Abstractions.Plugins;
using DevProxy.Abstractions.Utils;
using Microsoft.Extensions.Logging;
using System.Globalization;
-using Unobtanium.Web.Proxy.EventArguments;
namespace DevProxy.Plugins.Guidance;
@@ -29,53 +29,53 @@ public override async Task InitializeAsync(InitArgs e, CancellationToken cancell
_ = _msGraphDb.GenerateDbAsync(true, cancellationToken);
}
- public override Task AfterResponseAsync(ProxyResponseArgs e, CancellationToken cancellationToken)
+ public override Func? OnRequestLogAsync => (args, cancellationToken) =>
{
- Logger.LogTrace("{Method} called", nameof(AfterResponseAsync));
+ Logger.LogTrace("{Method} called", nameof(OnRequestLogAsync));
- ArgumentNullException.ThrowIfNull(e);
+ ArgumentNullException.ThrowIfNull(args);
- if (!e.HasRequestUrlMatch(UrlsToWatch))
+ if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.Request.RequestUri))
{
- Logger.LogRequest("URL not matched", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("URL not matched", MessageType.Skipped, args.Request);
return Task.CompletedTask;
}
- if (string.Equals(e.Session.HttpClient.Request.Method, "OPTIONS", StringComparison.OrdinalIgnoreCase))
+ if (args.Request.Method == HttpMethod.Options)
{
- Logger.LogRequest("Skipping OPTIONS request", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("Skipping OPTIONS request", MessageType.Skipped, args.Request);
return Task.CompletedTask;
}
- if (WarnNoSelect(e.Session))
+ if (WarnNoSelect(args.Request))
{
- Logger.LogRequest(BuildUseSelectMessage(), MessageType.Warning, new(e.Session));
+ Logger.LogRequest(BuildUseSelectMessage(), MessageType.Warning, args.Request);
}
- Logger.LogTrace("Left {Name}", nameof(AfterResponseAsync));
+ Logger.LogTrace("Left {Name}", nameof(OnRequestLogAsync));
return Task.CompletedTask;
- }
+ };
- private bool WarnNoSelect(SessionEventArgs session)
+ private bool WarnNoSelect(HttpRequestMessage request)
{
- var request = session.HttpClient.Request;
if (!ProxyUtils.IsGraphRequest(request) ||
- request.Method != "GET")
+ request.Method != HttpMethod.Get)
{
- Logger.LogRequest("Not a Microsoft Graph GET request", MessageType.Skipped, new(session));
+ Logger.LogRequest("Not a Microsoft Graph GET request", MessageType.Skipped, request);
return false;
}
- var graphVersion = ProxyUtils.GetGraphVersion(request.RequestUri.AbsoluteUri);
+ var graphVersion = ProxyUtils.GetGraphVersion(request.RequestUri!.AbsoluteUri);
var tokenizedUrl = GetTokenizedUrl(request.RequestUri.AbsoluteUri);
if (EndpointSupportsSelect(graphVersion, tokenizedUrl))
{
- return !request.Url.Contains("$select", StringComparison.OrdinalIgnoreCase) &&
- !request.Url.Contains("%24select", StringComparison.OrdinalIgnoreCase);
+ var url = request.RequestUri.AbsoluteUri;
+ return !url.Contains("$select", StringComparison.OrdinalIgnoreCase) &&
+ !url.Contains("%24select", StringComparison.OrdinalIgnoreCase);
}
else
{
- Logger.LogRequest("Endpoint does not support $select", MessageType.Skipped, new(session));
+ Logger.LogRequest("Endpoint does not support $select", MessageType.Skipped, request);
return false;
}
}
diff --git a/DevProxy.Plugins/Guidance/ODSPSearchGuidancePlugin.cs b/DevProxy.Plugins/Guidance/ODSPSearchGuidancePlugin.cs
index a4d282ff..20727a6f 100644
--- a/DevProxy.Plugins/Guidance/ODSPSearchGuidancePlugin.cs
+++ b/DevProxy.Plugins/Guidance/ODSPSearchGuidancePlugin.cs
@@ -2,11 +2,11 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using DevProxy.Abstractions.Models;
using DevProxy.Abstractions.Proxy;
using DevProxy.Abstractions.Plugins;
using DevProxy.Abstractions.Utils;
using Microsoft.Extensions.Logging;
-using Unobtanium.Web.Proxy.EventArguments;
namespace DevProxy.Plugins.Guidance;
@@ -16,39 +16,36 @@ public sealed class ODSPSearchGuidancePlugin(
{
public override string Name => nameof(ODSPSearchGuidancePlugin);
- public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken cancellationToken)
+ public override Func? OnRequestLogAsync => (args, cancellationToken) =>
{
- Logger.LogTrace("{Method} called", nameof(BeforeRequestAsync));
+ Logger.LogTrace("{Method} called", nameof(OnRequestLogAsync));
- ArgumentNullException.ThrowIfNull(e);
-
- if (!e.HasRequestUrlMatch(UrlsToWatch))
+ if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.Request.RequestUri))
{
- Logger.LogRequest("URL not matched", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("URL not matched", MessageType.Skipped, args.Request);
return Task.CompletedTask;
}
- if (string.Equals(e.Session.HttpClient.Request.Method, "OPTIONS", StringComparison.OrdinalIgnoreCase))
+ if (args.Request.Method == HttpMethod.Options)
{
- Logger.LogRequest("Skipping OPTIONS request", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("Skipping OPTIONS request", MessageType.Skipped, args.Request);
return Task.CompletedTask;
}
- if (WarnDeprecatedSearch(e.Session))
+ if (WarnDeprecatedSearch(args.Request))
{
- Logger.LogRequest(BuildUseGraphSearchMessage(), MessageType.Warning, new LoggingContext(e.Session));
+ Logger.LogRequest(BuildUseGraphSearchMessage(), MessageType.Warning, args.Request);
}
- Logger.LogTrace("Left {Name}", nameof(BeforeRequestAsync));
+ Logger.LogTrace("Left {Name}", nameof(OnRequestLogAsync));
return Task.CompletedTask;
- }
+ };
- private bool WarnDeprecatedSearch(SessionEventArgs session)
+ private bool WarnDeprecatedSearch(HttpRequestMessage request)
{
- var request = session.HttpClient.Request;
if (!ProxyUtils.IsGraphRequest(request) ||
- request.Method != "GET")
+ request.Method != HttpMethod.Get)
{
- Logger.LogRequest("Not a Microsoft Graph GET request", MessageType.Skipped, new(session));
+ Logger.LogRequest("Not a Microsoft Graph GET request", MessageType.Skipped, request);
return false;
}
@@ -58,15 +55,16 @@ private bool WarnDeprecatedSearch(SessionEventArgs session)
// graph.microsoft.com/{version}/sites/{site-id}/drive/root/search(q='{search-text}')
// graph.microsoft.com/{version}/users/{user-id}/drive/root/search(q='{search-text}')
// graph.microsoft.com/{version}/sites?search={query}
- if (request.RequestUri.AbsolutePath.Contains("/search(q=", StringComparison.OrdinalIgnoreCase) ||
+ if (request.RequestUri != null &&
+ (request.RequestUri.AbsolutePath.Contains("/search(q=", StringComparison.OrdinalIgnoreCase) ||
(request.RequestUri.AbsolutePath.EndsWith("/sites", StringComparison.OrdinalIgnoreCase) &&
- request.RequestUri.Query.Contains("search=", StringComparison.OrdinalIgnoreCase)))
+ request.RequestUri.Query.Contains("search=", StringComparison.OrdinalIgnoreCase))))
{
return true;
}
else
{
- Logger.LogRequest("Not a SharePoint search request", MessageType.Skipped, new(session));
+ Logger.LogRequest("Not a SharePoint search request", MessageType.Skipped, request);
return false;
}
}
diff --git a/DevProxy.Plugins/Guidance/ODataPagingGuidancePlugin.cs b/DevProxy.Plugins/Guidance/ODataPagingGuidancePlugin.cs
index 733948f8..8f7968b4 100644
--- a/DevProxy.Plugins/Guidance/ODataPagingGuidancePlugin.cs
+++ b/DevProxy.Plugins/Guidance/ODataPagingGuidancePlugin.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using DevProxy.Abstractions.Models;
using DevProxy.Abstractions.Proxy;
using DevProxy.Abstractions.Plugins;
using DevProxy.Abstractions.Utils;
@@ -19,89 +20,89 @@ public sealed class ODataPagingGuidancePlugin(
public override string Name => nameof(ODataPagingGuidancePlugin);
- public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken cancellationToken)
+ public override Func? OnRequestLogAsync => (args, cancellationToken) =>
{
- Logger.LogTrace("{Method} called", nameof(BeforeRequestAsync));
+ Logger.LogTrace("{Method} called", nameof(OnRequestLogAsync));
- ArgumentNullException.ThrowIfNull(e);
-
- if (!e.HasRequestUrlMatch(UrlsToWatch))
+ if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.Request.RequestUri))
{
- Logger.LogRequest("URL not matched", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("URL not matched", MessageType.Skipped, args.Request);
return Task.CompletedTask;
}
- if (!string.Equals(e.Session.HttpClient.Request.Method, "GET", StringComparison.OrdinalIgnoreCase))
+ if (args.Request.Method != HttpMethod.Get)
{
- Logger.LogRequest("Skipping non-GET request", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("Skipping non-GET request", MessageType.Skipped, args.Request);
return Task.CompletedTask;
}
- if (IsODataPagingUrl(e.Session.HttpClient.Request.RequestUri))
+ if (args.Request.RequestUri != null && IsODataPagingUrl(args.Request.RequestUri))
{
- if (!pagingUrls.Contains(e.Session.HttpClient.Request.Url))
+ if (!pagingUrls.Contains(args.Request.RequestUri.ToString()))
{
- Logger.LogRequest(BuildIncorrectPagingUrlMessage(), MessageType.Warning, new(e.Session));
+ Logger.LogRequest(BuildIncorrectPagingUrlMessage(), MessageType.Warning, args.Request);
}
else
{
- Logger.LogRequest("Paging URL is correct", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("Paging URL is correct", MessageType.Skipped, args.Request);
}
}
else
{
- Logger.LogRequest("Not an OData paging URL", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("Not an OData paging URL", MessageType.Skipped, args.Request);
}
- Logger.LogTrace("Left {Name}", nameof(BeforeRequestAsync));
+ Logger.LogTrace("Left {Name}", nameof(OnRequestLogAsync));
return Task.CompletedTask;
- }
+ };
- public override async Task BeforeResponseAsync(ProxyResponseArgs e, CancellationToken cancellationToken)
+ public override Func? OnResponseLogAsync => async (args, cancellationToken) =>
{
- Logger.LogTrace("{Method} called", nameof(BeforeResponseAsync));
+ Logger.LogTrace("{Method} called", nameof(OnResponseLogAsync));
- ArgumentNullException.ThrowIfNull(e);
-
- if (!e.HasRequestUrlMatch(UrlsToWatch))
+ if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.HttpRequestMessage.RequestUri))
{
- Logger.LogRequest("URL not matched", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("URL not matched", MessageType.Skipped, args.HttpRequestMessage);
return;
}
- if (!string.Equals(e.Session.HttpClient.Request.Method, "GET", StringComparison.OrdinalIgnoreCase))
+ if (args.HttpRequestMessage.Method != HttpMethod.Get)
{
- Logger.LogRequest("Skipping non-GET request", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("Skipping non-GET request", MessageType.Skipped, args.HttpRequestMessage);
return;
}
- if (e.Session.HttpClient.Response.StatusCode >= 300)
+ if ((int)args.HttpResponseMessage.StatusCode >= 300)
{
- Logger.LogRequest("Skipping non-success response", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("Skipping non-success response", MessageType.Skipped, args.HttpRequestMessage);
return;
}
- if (e.Session.HttpClient.Response.ContentType is null ||
- (!e.Session.HttpClient.Response.ContentType.Contains("json", StringComparison.OrdinalIgnoreCase) &&
- !e.Session.HttpClient.Response.ContentType.Contains("application/atom+xml", StringComparison.OrdinalIgnoreCase)) ||
- !e.Session.HttpClient.Response.HasBody)
+
+ var mediaType = args.HttpResponseMessage.Content?.Headers?.ContentType?.MediaType;
+ if (mediaType is null ||
+ (!mediaType.Contains("json", StringComparison.OrdinalIgnoreCase) &&
+ !mediaType.Contains("application/atom+xml", StringComparison.OrdinalIgnoreCase)))
{
- Logger.LogRequest("Skipping response with unsupported body type", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("Skipping response with unsupported body type", MessageType.Skipped, args.HttpRequestMessage);
return;
}
- e.Session.HttpClient.Response.KeepBody = true;
+ if (args.HttpResponseMessage.Content is null)
+ {
+ Logger.LogRequest("Skipping response with no content", MessageType.Skipped, args.HttpRequestMessage);
+ return;
+ }
var nextLink = string.Empty;
- var bodyString = await e.Session.GetResponseBodyAsString(cancellationToken);
+ var bodyString = await args.HttpResponseMessage.Content.ReadAsStringAsync(cancellationToken);
if (string.IsNullOrEmpty(bodyString))
{
- Logger.LogRequest("Skipping empty response body", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("Skipping empty response body", MessageType.Skipped, args.HttpRequestMessage);
return;
}
- var contentType = e.Session.HttpClient.Response.ContentType;
- if (contentType.Contains("json", StringComparison.OrdinalIgnoreCase))
+ if (mediaType.Contains("json", StringComparison.OrdinalIgnoreCase))
{
nextLink = GetNextLinkFromJson(bodyString);
}
- else if (contentType.Contains("application/atom+xml", StringComparison.OrdinalIgnoreCase))
+ else if (mediaType.Contains("application/atom+xml", StringComparison.OrdinalIgnoreCase))
{
nextLink = GetNextLinkFromXml(bodyString);
}
@@ -112,11 +113,11 @@ public override async Task BeforeResponseAsync(ProxyResponseArgs e, Cancellation
}
else
{
- Logger.LogRequest("No next link found in the response", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("No next link found in the response", MessageType.Skipped, args.HttpRequestMessage);
}
- Logger.LogTrace("Left {Name}", nameof(BeforeResponseAsync));
- }
+ Logger.LogTrace("Left {Name}", nameof(OnResponseLogAsync));
+ };
private string GetNextLinkFromJson(string responseBody)
{
diff --git a/DevProxy.Plugins/inventory.md b/DevProxy.Plugins/inventory.md
index f9e1c74f..50ff166a 100644
--- a/DevProxy.Plugins/inventory.md
+++ b/DevProxy.Plugins/inventory.md
@@ -8,6 +8,7 @@ This document provides an inventory of all plugins in the DevProxy.Plugins proje
**Methods Implemented:**
- InitializeAsync
- BeforeRequestAsync
+- AfterRecordingStopAsync
**Behavior:** Read-only (collects API permission data for reporting)
@@ -107,7 +108,7 @@ This document provides an inventory of all plugins in the DevProxy.Plugins proje
**Base Class:** BasePlugin
**Methods Implemented:**
-- AfterResponseAsync
+- OnRequestLogAsync (NEW API - MIGRATED)
**Behavior:** Read-only (provides guidance about beta API usage)
@@ -115,7 +116,7 @@ This document provides an inventory of all plugins in the DevProxy.Plugins proje
**Base Class:** BasePlugin
**Methods Implemented:**
-- AfterResponseAsync
+- OnRequestLogAsync (NEW API - MIGRATED)
**Behavior:** Read-only (provides guidance about request ID headers)
@@ -123,7 +124,7 @@ This document provides an inventory of all plugins in the DevProxy.Plugins proje
**Base Class:** BasePlugin
**Methods Implemented:**
-- AfterResponseAsync
+- OnRequestLogAsync (NEW API - MIGRATED)
**Behavior:** Read-only (provides guidance about Graph connector usage)
@@ -133,6 +134,7 @@ This document provides an inventory of all plugins in the DevProxy.Plugins proje
**Methods Implemented:**
- InitializeAsync
- BeforeRequestAsync
+- AfterRecordingStopAsync
**Behavior:** Read-only (analyzes and reports on Graph API permissions)
@@ -167,7 +169,7 @@ This document provides an inventory of all plugins in the DevProxy.Plugins proje
**Base Class:** BasePlugin
**Methods Implemented:**
-- AfterResponseAsync
+- OnResponseLogAsync (NEW API - MIGRATED)
**Behavior:** Read-only (provides guidance about using Graph SDKs)
@@ -277,7 +279,7 @@ This document provides an inventory of all plugins in the DevProxy.Plugins proje
**Base Class:** BasePlugin
**Methods Implemented:**
-- AfterResponseAsync
+- OnRequestLogAsync (NEW API - MIGRATED)
**Behavior:** Read-only (provides guidance about SharePoint search optimization)
@@ -285,7 +287,8 @@ This document provides an inventory of all plugins in the DevProxy.Plugins proje
**Base Class:** BasePlugin
**Methods Implemented:**
-- AfterResponseAsync
+- OnRequestLogAsync (NEW API - MIGRATED)
+- OnResponseLogAsync (NEW API - MIGRATED)
**Behavior:** Read-only (provides guidance about OData paging patterns)
@@ -304,6 +307,8 @@ This document provides an inventory of all plugins in the DevProxy.Plugins proje
**Methods Implemented:**
- InitializeAsync
- BeforeRequestAsync
+- AfterResponseAsync
+- AfterRecordingStopAsync
**Behavior:** Read-only (collects OpenAI API usage telemetry for reporting)
@@ -369,59 +374,11 @@ This document provides an inventory of all plugins in the DevProxy.Plugins proje
## Summary
- **Total Plugins:** 38
-- **Plugins using BeforeRequestAsync:** 32
+- **Plugins using BeforeRequestAsync:** 27 (decreased by 3 due to ODataPagingGuidancePlugin and ODSPSearchGuidancePlugin migrations)
- **Plugins using BeforeResponseAsync:** 2 (DevToolsPlugin, RateLimitingPlugin)
-- **Plugins using AfterResponseAsync:** 9
+- **Plugins using AfterResponseAsync:** 5 (decreased by 2 due to ODataPagingGuidancePlugin and GraphSdkGuidancePlugin migrations: GraphSelectGuidancePlugin, OpenAITelemetryPlugin)
- **Plugins using AfterRequestLogAsync:** 1 (DevToolsPlugin)
-- **Plugins using AfterRecordingStopAsync:** 4
+- **Plugins using AfterRecordingStopAsync:** 8 (ApiCenterMinimalPermissionsPlugin, ExecutionSummaryPlugin, GraphMinimalPermissionsGuidancePlugin, HttpFileGeneratorPlugin, MockGeneratorPlugin, OpenAITelemetryPlugin, OpenApiSpecGeneratorPlugin, TypeSpecGeneratorPlugin)
- **Plugins using OnRequestAsync (NEW API):** 1 (GraphRandomErrorPlugin - already migrated)
-- **Plugins using OnResponseAsync (NEW API):** 0
-
-### Behavior Classification
-
-**Response Modifying Plugins (17):** These plugins intercept requests and return custom responses
-- AuthPlugin
-- CrudApiPlugin
-- EntraMockResponsePlugin
-- GenericRandomErrorPlugin
-- GraphMockResponsePlugin
-- GraphRandomErrorPlugin
-- LanguageModelFailurePlugin
-- LanguageModelRateLimitingPlugin
-- MockRequestPlugin
-- MockResponsePlugin
-- OpenAIMockResponsePlugin
-- RateLimitingPlugin
-- RetryAfterPlugin
-
-**Request Modifying Plugins (1):** These plugins modify requests before they proceed
-- RewritePlugin
-
-**Read-Only Analysis Plugins (20):** These plugins only read request/response data for analysis, guidance, or reporting
-- ApiCenterMinimalPermissionsPlugin
-- ApiCenterOnboardingPlugin
-- ApiCenterProductionVersionPlugin
-- CachingGuidancePlugin
-- DevToolsPlugin
-- ExecutionSummaryPlugin
-- GraphBetaSupportGuidancePlugin
-- GraphClientRequestIdGuidancePlugin
-- GraphConnectorGuidancePlugin
-- GraphMinimalPermissionsGuidancePlugin
-- GraphMinimalPermissionsPlugin
-- GraphSdkGuidancePlugin
-- GraphSelectGuidancePlugin
-- HttpFileGeneratorPlugin
-- LatencyPlugin
-- MinimalCsomPermissionsPlugin
-- MinimalPermissionsGuidancePlugin
-- MinimalPermissionsPlugin
-- MockGeneratorPlugin
-- ODSPSearchGuidancePlugin
-- ODataPagingGuidancePlugin
-- OpenAITelemetryPlugin
-- OpenApiSpecGeneratorPlugin
-- TypeSpecGeneratorPlugin
-- UrlDiscoveryPlugin
-
-**Migration Priority:** Response modifying plugins should be migrated first as they have the most complex logic for creating and returning custom responses. Read-only plugins can potentially remain on the old API longer since they don't affect request flow.
\ No newline at end of file
+- **Plugins using OnRequestLogAsync (NEW API):** 6 (GraphBetaSupportGuidancePlugin, CachingGuidancePlugin, GraphClientRequestIdGuidancePlugin, GraphConnectorGuidancePlugin, ODataPagingGuidancePlugin, ODSPSearchGuidancePlugin - migrated)
+- **Plugins using OnResponseLogAsync (NEW API):** 2 (ODataPagingGuidancePlugin, GraphSdkGuidancePlugin - migrated)
\ No newline at end of file
diff --git a/DevProxy.Plugins/migration.md b/DevProxy.Plugins/migration.md
index 9a2f9fbb..03457984 100644
--- a/DevProxy.Plugins/migration.md
+++ b/DevProxy.Plugins/migration.md
@@ -1,4 +1,4 @@
-# Plugin Migration Guide: From Event-Based to Functional API
+# Plugin Migration Guide: From Event-Based to Functional API
This document provides detailed guidance on migrating DevProxy plugins from the old event-based API to the new functional API pattern.
@@ -39,8 +39,7 @@ public override Task AfterResponseAsync(ProxyResponseArgs e, CancellationToken c
### New API (Functional)
```csharp
// For plugins that need to modify requests or responses
-public override Func>? OnRequestAsync =>
- async (args, cancellationToken) =>
+public override Func>? OnRequestAsync => async (args, cancellationToken) =>
{
// Logic to decide whether to intercept
if (!ShouldIntercept(args.Request))
@@ -58,8 +57,7 @@ public override Func>?
};
// For guidance plugins that only need to log or analyze requests
-public override Func? OnRequestLogAsync =>
- async (args, cancellationToken) =>
+public override Func? OnRequestLogAsync => async (args, cancellationToken) =>
{
// Analyze request and provide guidance
if (ShouldProvideGuidance(args.Request))
@@ -69,27 +67,39 @@ public override Func? OnRequestLogAsy
};
// For plugins that need to modify responses from remote server
-public override Func>? OnResponseAsync =>
- async (args, cancellationToken) =>
+public override Func>? OnResponseAsync => async (args, cancellationToken) =>
{
// Process response and optionally modify it
- // Return null to continue, or ResponseEventResponse to modify
+ // Return null to continue, or PluginResponse to modify
return null;
};
+
+// For guidance plugins that only need to log or analyze responses
+public override Func? OnResponseLogAsync => async (args, cancellationToken) =>
+{
+ // Analyze response and provide guidance
+ if (ShouldProvideGuidance(args.HttpResponseMessage))
+ {
+ Logger.LogRequest("Response guidance message", MessageType.Tip, args.HttpRequestMessage);
+ }
+};
```
## Key Differences
### 1. Input Arguments
-- **Old API:** `ProxyRequestArgs e` containing session, response state, and global data
-- **New API:** `RequestArguments args` containing `HttpRequestMessage` and `RequestId`
+- **Old API:** `ProxyRequestArgs e` and `ProxyResponseArgs e` containing session, response state, and global data
+- **New API:**
+ - `RequestArguments args` containing `HttpRequestMessage` and `RequestId`
+ - `ResponseArguments args` containing `HttpRequestMessage`, `HttpResponseMessage` and `RequestId`
### 2. Return Values
- **Old API:** `Task` (void) - side effects through `e.Session` and `e.ResponseState`
- **New API:**
- `Task` for `OnRequestAsync` - explicit return values to control flow
- - `Task` for `OnRequestLogAsync` - read-only logging/analysis
- - `Task` for `OnResponseAsync` - response modification
+ - `Task` for `OnRequestLogAsync` - read-only logging/analysis of requests
+ - `Task` for `OnResponseAsync` - response modification (return null to continue)
+ - `Task` for `OnResponseLogAsync` - read-only logging/analysis of responses
### 3. Response Creation
- **Old API:** Direct manipulation of session: `e.Session.GenericResponse(...)`
@@ -105,24 +115,27 @@ Choose the appropriate new API method based on your plugin's behavior:
- **`OnRequestAsync`**: Use for plugins that need to intercept and potentially modify or respond to requests
- **`OnRequestLogAsync`**: Use for guidance plugins that only need to analyze requests and provide logging/guidance (cannot modify requests or responses)
- **`OnResponseAsync`**: Use for plugins that need to modify responses from the remote server
+- **`OnResponseLogAsync`**: Use for guidance plugins that only need to analyze responses and provide logging/guidance (cannot modify responses)
## Migration Steps
### Step 1: Determine the Appropriate New Method
-**Response Modifying Plugins** ? Use `OnRequestAsync`:
+**Response Modifying Plugins** → Use `OnRequestAsync`:
- MockResponsePlugin
- AuthPlugin
- RateLimitingPlugin
- GenericRandomErrorPlugin
- etc.
-**Guidance/Analysis Plugins** ? Use `OnRequestLogAsync`:
-- CachingGuidancePlugin
-- GraphSdkGuidancePlugin (when migrated from AfterResponseAsync)
-- UrlDiscoveryPlugin
-- Most reporting plugins
-- etc.
+**Guidance/Analysis Plugins** → Use `OnRequestLogAsync` or `OnResponseLogAsync`:
+- CachingGuidancePlugin → `OnRequestLogAsync`
+- GraphSdkGuidancePlugin → `OnResponseLogAsync` (analyzes responses from AfterResponseAsync)
+- UrlDiscoveryPlugin → `OnRequestLogAsync`
+- Most reporting plugins → `OnRequestLogAsync`
+
+**Response Modifying Plugins** → Use `OnResponseAsync`:
+- Plugins that need to modify responses from the remote server
### Step 2: Change Method Signature
@@ -132,23 +145,39 @@ Choose the appropriate new API method based on your plugin's behavior:
public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken cancellationToken)
// After
-public override Func>? OnRequestAsync =>
- async (args, cancellationToken) =>
+public override Func>? OnRequestAsync => async (args, cancellationToken) =>
```
-**For Guidance Plugins (OnRequestLogAsync):**
+**For Request Guidance Plugins (OnRequestLogAsync):**
```csharp
// Before
public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken cancellationToken)
// After
-public override Func? OnRequestLogAsync =>
- async (args, cancellationToken) =>
+public override Func? OnRequestLogAsync => async (args, cancellationToken) =>
+```
+
+**For Response Guidance Plugins (OnResponseLogAsync):**
+```csharp
+// Before
+public override Task AfterResponseAsync(ProxyResponseArgs e, CancellationToken cancellationToken)
+
+// After
+public override Func? OnResponseLogAsync => async (args, cancellationToken) =>
+```
+
+**For Response Modifying Plugins (OnResponseAsync):**
+```csharp
+// Before
+public override Task BeforeResponseAsync(ProxyResponseArgs e, CancellationToken cancellationToken)
+
+// After
+public override Func>? OnResponseAsync => async (args, cancellationToken) =>
```
### Step 3: Update Input Data Access
-**Before:**
+**Before (Request):**
```csharp
var request = e.Session.HttpClient.Request;
var url = request.RequestUri;
@@ -156,7 +185,7 @@ var method = request.Method;
var body = request.BodyString;
```
-**After:**
+**After (Request):**
```csharp
var request = args.Request;
var url = request.RequestUri;
@@ -164,6 +193,22 @@ var method = request.Method.Method;
var body = await request.Content.ReadAsStringAsync();
```
+**Before (Response):**
+```csharp
+var request = e.Session.HttpClient.Request;
+var response = e.Session.HttpClient.Response;
+var statusCode = response.StatusCode;
+var responseBody = response.BodyString;
+```
+
+**After (Response):**
+```csharp
+var request = args.HttpRequestMessage;
+var response = args.HttpResponseMessage;
+var statusCode = response.StatusCode;
+var responseBody = await response.Content.ReadAsStringAsync();
+```
+
### Step 4: Update URL Matching Logic
**Before:**
@@ -184,11 +229,11 @@ if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.Request.RequestUri))
}
```
-**After (OnRequestLogAsync):**
+**After (OnRequestLogAsync/OnResponseLogAsync):**
```csharp
-if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.Request.RequestUri))
+if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.HttpRequestMessage.RequestUri))
{
- Logger.LogRequest("URL not matched", MessageType.Skipped, args.Request);
+ Logger.LogRequest("URL not matched", MessageType.Skipped, args.HttpRequestMessage);
return;
}
```
@@ -208,10 +253,11 @@ if (e.ResponseState.HasBeenSet)
```csharp
// Not needed in new API - flow control is handled by return values
// OnRequestAsync: Each plugin returns either Continue() or Respond()
-// OnRequestLogAsync: Cannot modify responses, so this check is irrelevant
+// OnRequestLogAsync/OnResponseLogAsync: Cannot modify responses, so this check is irrelevant
+// OnResponseAsync: Return null to continue, or PluginResponse to modify
```
-### Step 6: Update Response Creation (OnRequestAsync only)
+### Step 6: Update Response Creation (OnRequestAsync and OnResponseAsync only)
**Before:**
```csharp
@@ -247,20 +293,20 @@ if (shouldPassThrough)
}
```
-**After (OnRequestAsync):**
+**After (OnRequestAsync/OnResponseAsync):**
```csharp
if (shouldPassThrough)
{
- Logger.LogRequest("Pass through", MessageType.Skipped, args.Request);
- return PluginResponse.Continue();
+ Logger.LogRequest("Pass through", MessageType.Skipped, args.Request); // or args.HttpRequestMessage
+ return PluginResponse.Continue(); // or return null for OnResponseAsync
}
```
-**After (OnRequestLogAsync):**
+**After (OnRequestLogAsync/OnResponseLogAsync):**
```csharp
if (shouldSkip)
{
- Logger.LogRequest("Skipping analysis", MessageType.Skipped, args.Request);
+ Logger.LogRequest("Skipping analysis", MessageType.Skipped, args.Request); // or args.HttpRequestMessage
return;
}
```
@@ -299,8 +345,7 @@ public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken ca
**After (New API):**
```csharp
-public override Func>? OnRequestAsync =>
- (args, cancellationToken) =>
+public override Func>? OnRequestAsync => (args, cancellationToken) =>
{
if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.Request.RequestUri))
{
@@ -331,7 +376,7 @@ public override Func>?
};
```
-### Example 2: Guidance Plugin (OnRequestLogAsync)
+### Example 2: Request Guidance Plugin (OnRequestLogAsync)
**Before (Old API):**
```csharp
@@ -355,8 +400,7 @@ public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken ca
**After (New API):**
```csharp
-public override Func? OnRequestLogAsync =>
- (args, cancellationToken) =>
+public override Func? OnRequestLogAsync => (args, cancellationToken) =>
{
if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.Request.RequestUri))
{
@@ -373,33 +417,76 @@ public override Func? OnRequestLogAsy
};
```
+### Example 3: Response Guidance Plugin (OnResponseLogAsync)
+
+**Before (Old API):**
+```csharp
+public override Task AfterResponseAsync(ProxyResponseArgs e, CancellationToken cancellationToken)
+{
+ if (!e.HasRequestUrlMatch(UrlsToWatch))
+ {
+ Logger.LogRequest("URL not matched", MessageType.Skipped, new(e.Session));
+ return Task.CompletedTask;
+ }
+
+ var response = e.Session.HttpClient.Response;
+ if (ShouldProvideGuidance(response))
+ {
+ Logger.LogRequest("Consider optimizing your API queries", MessageType.Tip, new(e.Session));
+ }
+
+ return Task.CompletedTask;
+}
+```
+
+**After (New API):**
+```csharp
+public override Func? OnResponseLogAsync => (args, cancellationToken) =>
+{
+ if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.HttpRequestMessage.RequestUri))
+ {
+ Logger.LogRequest("URL not matched", MessageType.Skipped, args.HttpRequestMessage);
+ return Task.CompletedTask;
+ }
+
+ if (ShouldProvideGuidance(args.HttpResponseMessage))
+ {
+ Logger.LogRequest("Consider optimizing your API queries", MessageType.Tip, args.HttpRequestMessage);
+ }
+
+ return Task.CompletedTask;
+};
+```
+
## Important Notes
### 1. Logging Context
-The logging context changes from `LoggingContext(e.Session)` to just the `HttpRequestMessage`:
+The logging context changes from `LoggingContext(e.Session)` to the appropriate request message:
```csharp
// Old
Logger.LogRequest("Message", MessageType.Info, new LoggingContext(e.Session));
-// New
+// New (Request-based methods)
Logger.LogRequest("Message", MessageType.Info, args.Request);
+
+// New (Response-based methods)
+Logger.LogRequest("Message", MessageType.Info, args.HttpRequestMessage);
```
### 2. Global Data and Session Data
Global data and session data access patterns will need to be reviewed as they may not be available in the new API. These features may be handled differently or through dependency injection.
-### 3. OnRequestLogAsync Benefits
-The new `OnRequestLogAsync` method provides several advantages for guidance plugins:
-- **Better Control Flow**: The proxy can respond quickly without waiting for guidance analysis
-- **Clear Intent**: Explicitly indicates the plugin is read-only and cannot modify requests/responses
-- **Performance**: Guidance plugins don't block the request pipeline
-- **Separation of Concerns**: Clearly separates modification logic from analysis logic
+### 3. New API Benefits
+The new API methods provide several advantages:
+- **Better Control Flow**: Clear separation between modifying and logging operations
+- **Clear Intent**: Method names explicitly indicate their purpose and capabilities
+- **Performance**: Logging methods don't block critical paths
+- **Separation of Concerns**: Clear distinction between modification and analysis logic
### 4. Async Considerations
-Both new API methods expect functions that return Tasks, so you can use async/await within the lambda:
+All new API methods expect functions that return Tasks, so you can use async/await within the lambda:
```csharp
-public override Func>? OnRequestAsync =>
- async (args, cancellationToken) =>
+public override Func>? OnRequestAsync => async (args, cancellationToken) =>
{
var data = await SomeAsyncOperation(cancellationToken);
// ... process data
@@ -410,8 +497,7 @@ public override Func>?
### 5. Error Handling
Error handling should be done within the function and appropriate responses returned:
```csharp
-public override Func>? OnRequestAsync =>
- async (args, cancellationToken) =>
+public override Func>? OnRequestAsync => async (args, cancellationToken) =>
{
try
{
@@ -426,7 +512,17 @@ public override Func>?
};
```
-## Migration Checklist
+## Migration instructions
+
+- We have compilation errors, so no need to try to build the project until all plugins are migrated.
+- Instead of `string.Equals(args.Request.Method.Method, "OPTIONS", StringComparison.OrdinalIgnoreCase)`, use `args.Request.Method == HttpMethod.Options` for better performance.
+- Summarize changes in max two lines
+
+### General Migration Steps
+
+1. Migrate plugin according to the new API method (OnRequestAsync, OnRequestLogAsync, OnResponseAsync, or OnResponseLogAsync).
+2. Update inventory.md to reflect the new method, leave the old methods in place using strikethrough.
+3. Update migration.md with the new migration status
### For Response Modifying Plugins (OnRequestAsync):
- [ ] Update method signature from `BeforeRequestAsync` to `OnRequestAsync`
@@ -441,7 +537,7 @@ public override Func>?
- [ ] Update logging context from `LoggingContext(e.Session)` to `args.Request`
- [ ] Test the migrated plugin thoroughly
-### For Guidance Plugins (OnRequestLogAsync):
+### For Request Guidance Plugins (OnRequestLogAsync):
- [ ] Update method signature from `BeforeRequestAsync` to `OnRequestLogAsync`
- [ ] Keep return type as `Task` (no PluginResponse needed)
- [ ] Update input parameter from `ProxyRequestArgs` to `RequestArguments`
@@ -451,6 +547,29 @@ public override Func>?
- [ ] Update logging context from `LoggingContext(e.Session)` to `args.Request`
- [ ] Test the migrated plugin thoroughly
+### For Response Guidance Plugins (OnResponseLogAsync):
+- [ ] Update method signature from `AfterResponseAsync` to `OnResponseLogAsync`
+- [ ] Keep return type as `Task` (no PluginResponse needed)
+- [ ] Update input parameter from `ProxyResponseArgs` to `ResponseArguments`
+- [ ] Replace `e.Session.HttpClient.Request` with `args.HttpRequestMessage`
+- [ ] Replace `e.Session.HttpClient.Response` with `args.HttpResponseMessage`
+- [ ] Replace `e.HasRequestUrlMatch()` with `ProxyUtils.MatchesUrlToWatch()`
+- [ ] Remove any response modification logic (not allowed in OnResponseLogAsync)
+- [ ] Update logging context from `LoggingContext(e.Session)` to `args.HttpRequestMessage`
+- [ ] Test the migrated plugin thoroughly
+
+### For Response Modifying Plugins (OnResponseAsync):
+- [ ] Update method signature from `BeforeResponseAsync` to `OnResponseAsync`
+- [ ] Change return type from `Task` to `Task`
+- [ ] Update input parameter from `ProxyResponseArgs` to `ResponseArguments`
+- [ ] Replace `e.Session.HttpClient.Request` with `args.HttpRequestMessage`
+- [ ] Replace `e.Session.HttpClient.Response` with `args.HttpResponseMessage`
+- [ ] Replace `e.HasRequestUrlMatch()` with `ProxyUtils.MatchesUrlToWatch()`
+- [ ] Remove `e.ResponseState.HasBeenSet` checks
+- [ ] Return `null` to continue or `PluginResponse` to modify
+- [ ] Update logging context from `LoggingContext(e.Session)` to `args.HttpRequestMessage`
+- [ ] Test the migrated plugin thoroughly
+
## Plugin Migration Categorization
Based on the inventory, here's how plugins should be migrated:
@@ -470,35 +589,34 @@ Based on the inventory, here's how plugins should be migrated:
12. RateLimitingPlugin
13. RetryAfterPlugin
-### OnRequestLogAsync (Guidance/Analysis - 20+ plugins):
+### OnRequestLogAsync (Request Guidance/Analysis - 20+ plugins):
1. ApiCenterMinimalPermissionsPlugin
2. ApiCenterOnboardingPlugin
3. ApiCenterProductionVersionPlugin
-4. CachingGuidancePlugin
+4. CachingGuidancePlugin (MIGRATED)
5. ExecutionSummaryPlugin
-6. GraphMinimalPermissionsGuidancePlugin
-7. GraphMinimalPermissionsPlugin
-8. HttpFileGeneratorPlugin
-9. MinimalCsomPermissionsPlugin
-10. MinimalPermissionsGuidancePlugin
-11. MinimalPermissionsPlugin
-12. OpenAITelemetryPlugin
-13. OpenApiSpecGeneratorPlugin
-14. TypeSpecGeneratorPlugin
-15. UrlDiscoveryPlugin
+6. GraphClientRequestIdGuidancePlugin (MIGRATED)
+7. GraphConnectorGuidancePlugin (MIGRATED)
+8. GraphMinimalPermissionsGuidancePlugin
+9. GraphMinimalPermissionsPlugin
+10. GraphSelectGuidancePlugin (MIGRATED)
+11. HttpFileGeneratorPlugin
+12. MinimalCsomPermissionsPlugin
+13. MinimalPermissionsGuidancePlugin
+14. MinimalPermissionsPlugin
+15. ~~ODSPSearchGuidancePlugin~~ (MIGRATED)
+16. OpenAITelemetryPlugin
+17. OpenApiSpecGeneratorPlugin
+18. TypeSpecGeneratorPlugin
+19. UrlDiscoveryPlugin
+
+### OnResponseLogAsync (Response Guidance/Analysis - plugins analyzing responses):
+1. ~~GraphSdkGuidancePlugin~~ (MIGRATED)
+2. ~~ODataPagingGuidancePlugin~~ (MIGRATED)
### Special Cases:
- **RewritePlugin**: Modifies requests before they proceed (may need custom handling)
- **DevToolsPlugin**: Uses multiple methods (BeforeRequestAsync, BeforeResponseAsync, AfterResponseAsync, AfterRequestLogAsync)
- **LatencyPlugin**: Adds delay but doesn't modify responses (could use OnRequestLogAsync)
-### Plugins using AfterResponseAsync (may migrate to OnResponseAsync):
-- GraphBetaSupportGuidancePlugin
-- GraphClientRequestIdGuidancePlugin
-- GraphConnectorGuidancePlugin
-- GraphSdkGuidancePlugin
-- GraphSelectGuidancePlugin
-- ODSPSearchGuidancePlugin
-- ODataPagingGuidancePlugin
-
-The `OnRequestLogAsync` method enables better control flow by allowing the proxy to respond quickly while still providing comprehensive guidance and analysis capabilities.
\ No newline at end of file
+The new API methods enable better control flow by allowing the proxy to handle modification and logging operations separately, improving both performance and code clarity.The new API methods enable better control flow by allowing the proxy to handle modification and logging operations separately, improving both performance and code clarity.
\ No newline at end of file
diff --git a/DevProxy/Proxy/ProxyEngine.cs b/DevProxy/Proxy/ProxyEngine.cs
index 67ccf3e4..c0bd8bd9 100755
--- a/DevProxy/Proxy/ProxyEngine.cs
+++ b/DevProxy/Proxy/ProxyEngine.cs
@@ -633,6 +633,24 @@ private async Task OnResponseAsync(object sender, Respons
var message = $"{e.Request.Method} {e.Response}";
_logger.LogRequest(message, MessageType.InterceptedResponse, e.Request);
HttpResponseMessage? response = null;
+ var logPlugins = _plugins.Where(p => p.Enabled && p.OnResponseLogAsync is not null);
+ if (logPlugins.Any())
+ {
+ // Call OnResponseLogAsync for all plugins at the same time and wait for all of them to complete
+ var logArguments = new Abstractions.Models.ResponseArguments(e.Request, e.Response, e.RequestId);
+ var logTasks = logPlugins
+ .Select(plugin => plugin.OnResponseLogAsync!(logArguments, cts.Token))
+ .ToArray();
+ try
+ {
+ await Task.WhenAll(logTasks);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "An error occurred in a plugin while logging response {ResponseStatusCode} for request {RequestMethod} {RequestUrl}",
+ e.Response.StatusCode, e.Request.Method, uri);
+ }
+ }
foreach (var plugin in _plugins.Where(p => p.Enabled && p.OnResponseAsync is not null))
{
@@ -640,12 +658,17 @@ private async Task OnResponseAsync(object sender, Respons
try
{
- var result = await plugin.OnResponseAsync!(e, cts.Token);
+ var result = await plugin.OnResponseAsync!(new Abstractions.Models.ResponseArguments(e.Request, response ?? e.Response, e.RequestId), cts.Token);
if (result is not null)
{
- if (result.ModifiedResponse is not null)
+ if (result.Request is not null)
+ {
+ // If the plugin modified the request, it is a mistake. Faulty behavior.
+ _logger.LogError("Plugin {PluginName} tried changing the request", plugin.Name);
+ }
+ if (result.Response is not null)
{
- response = result.ModifiedResponse;
+ response = result.Response;
// Maybe exit the loop here?
}
}
From 81f0ad627d046797a0bedef08538db52e5f9b420 Mon Sep 17 00:00:00 2001
From: Stephan van Rooij <1292510+svrooij@users.noreply.github.com>
Date: Tue, 19 Aug 2025 18:23:07 +0200
Subject: [PATCH 06/14] More plugins migrated
---
.../Models/RequestArguments.cs | 6 -
.../Models/ResponseArguments.cs | 7 -
DevProxy.Abstractions/Plugins/BasePlugin.cs | 1 -
DevProxy.Abstractions/Plugins/IPlugin.cs | 8 +-
.../Plugins/IProxyStorage.cs | 23 +++
.../{Models => Plugins}/PluginResponse.cs | 2 +-
.../Plugins/RequestArguments.cs | 26 ++++
.../Plugins/ResponseArguments.cs | 5 +
.../Behavior/GenericRandomErrorPlugin.cs | 126 ++++++++++-------
.../Behavior/GraphRandomErrorPlugin.cs | 1 -
.../Guidance/CachingGuidancePlugin.cs | 1 -
.../GraphBetaSupportGuidancePlugin.cs | 1 -
.../GraphClientRequestIdGuidancePlugin.cs | 1 -
.../Guidance/GraphConnectorGuidancePlugin.cs | 1 -
.../Guidance/GraphSdkGuidancePlugin.cs | 19 ++-
.../Guidance/GraphSelectGuidancePlugin.cs | 1 -
.../Guidance/ODSPSearchGuidancePlugin.cs | 1 -
.../Guidance/ODataPagingGuidancePlugin.cs | 27 ++--
DevProxy.Plugins/inventory.md | 6 +-
DevProxy.Plugins/migration.md | 132 +++++++++++++++++-
.../IServiceCollectionExtensions.cs | 3 +
DevProxy/Plugins/ProxyStorage.cs | 24 ++++
DevProxy/Proxy/ProxyEngine.cs | 35 ++---
23 files changed, 337 insertions(+), 120 deletions(-)
delete mode 100644 DevProxy.Abstractions/Models/RequestArguments.cs
delete mode 100644 DevProxy.Abstractions/Models/ResponseArguments.cs
create mode 100644 DevProxy.Abstractions/Plugins/IProxyStorage.cs
rename DevProxy.Abstractions/{Models => Plugins}/PluginResponse.cs (92%)
create mode 100644 DevProxy.Abstractions/Plugins/RequestArguments.cs
create mode 100644 DevProxy.Abstractions/Plugins/ResponseArguments.cs
create mode 100644 DevProxy/Plugins/ProxyStorage.cs
diff --git a/DevProxy.Abstractions/Models/RequestArguments.cs b/DevProxy.Abstractions/Models/RequestArguments.cs
deleted file mode 100644
index 8c2a232a..00000000
--- a/DevProxy.Abstractions/Models/RequestArguments.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace DevProxy.Abstractions.Models;
-public class RequestArguments(HttpRequestMessage request, string requestId)
-{
- public HttpRequestMessage Request { get; } = request;
- public string RequestId { get; } = requestId ?? throw new ArgumentNullException(nameof(requestId));
-}
diff --git a/DevProxy.Abstractions/Models/ResponseArguments.cs b/DevProxy.Abstractions/Models/ResponseArguments.cs
deleted file mode 100644
index e888299c..00000000
--- a/DevProxy.Abstractions/Models/ResponseArguments.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace DevProxy.Abstractions.Models;
-public class ResponseArguments(HttpRequestMessage httpRequestMessage, HttpResponseMessage httpResponseMessage, string requestId)
-{
- public HttpRequestMessage HttpRequestMessage { get; } = httpRequestMessage;
- public HttpResponseMessage HttpResponseMessage { get; } = httpResponseMessage;
- public string RequestId { get; } = requestId ?? throw new ArgumentNullException(nameof(requestId));
-}
diff --git a/DevProxy.Abstractions/Plugins/BasePlugin.cs b/DevProxy.Abstractions/Plugins/BasePlugin.cs
index c5b86fb0..3c09e224 100644
--- a/DevProxy.Abstractions/Plugins/BasePlugin.cs
+++ b/DevProxy.Abstractions/Plugins/BasePlugin.cs
@@ -5,7 +5,6 @@
using System.CommandLine;
using System.Globalization;
using System.Text.Json;
-using DevProxy.Abstractions.Models;
using DevProxy.Abstractions.Proxy;
using DevProxy.Abstractions.Utils;
using Microsoft.Extensions.Configuration;
diff --git a/DevProxy.Abstractions/Plugins/IPlugin.cs b/DevProxy.Abstractions/Plugins/IPlugin.cs
index 00383cc3..2757e82b 100644
--- a/DevProxy.Abstractions/Plugins/IPlugin.cs
+++ b/DevProxy.Abstractions/Plugins/IPlugin.cs
@@ -23,22 +23,22 @@ public interface IPlugin
///
/// Implement this to handle requests.
///
- Func>? OnRequestAsync { get; }
+ Func>? OnRequestAsync { get; }
///
/// Implement this to log requests, you cannot modify the request or response here.
///
- Func? OnRequestLogAsync { get; }
+ Func? OnRequestLogAsync { get; }
///
/// Implement this to modify responses from the remote server.
///
- Func>? OnResponseAsync { get; }
+ Func>? OnResponseAsync { get; }
///
/// Implement this to modify responses from the remote server.
///
- Func? OnResponseLogAsync { get; }
+ Func? OnResponseLogAsync { get; }
Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken cancellationToken);
Task BeforeResponseAsync(ProxyResponseArgs e, CancellationToken cancellationToken);
Task AfterResponseAsync(ProxyResponseArgs e, CancellationToken cancellationToken);
diff --git a/DevProxy.Abstractions/Plugins/IProxyStorage.cs b/DevProxy.Abstractions/Plugins/IProxyStorage.cs
new file mode 100644
index 00000000..e493b0ff
--- /dev/null
+++ b/DevProxy.Abstractions/Plugins/IProxyStorage.cs
@@ -0,0 +1,23 @@
+[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("DevProxy")]
+namespace DevProxy.Abstractions.Plugins;
+
+///
+/// If you need either global or request-specific storage, ask for this interface in your plugin.
+///
+public interface IProxyStorage
+{
+ ///
+ /// Access to global data shared across all requests.
+ ///
+ public Dictionary GlobalData { get; }
+
+ ///
+ /// Get request-specific data by its ID.
+ ///
+ ///
+ ///
+
+ public Dictionary GetRequestData(RequestId id);
+
+ internal void RemoveRequestData(RequestId id);
+}
diff --git a/DevProxy.Abstractions/Models/PluginResponse.cs b/DevProxy.Abstractions/Plugins/PluginResponse.cs
similarity index 92%
rename from DevProxy.Abstractions/Models/PluginResponse.cs
rename to DevProxy.Abstractions/Plugins/PluginResponse.cs
index 4bc1dc98..e05d4799 100644
--- a/DevProxy.Abstractions/Models/PluginResponse.cs
+++ b/DevProxy.Abstractions/Plugins/PluginResponse.cs
@@ -1,4 +1,4 @@
-namespace DevProxy.Abstractions.Models;
+namespace DevProxy.Abstractions.Plugins;
public class PluginResponse
{
public HttpRequestMessage? Request { get; private set; }
diff --git a/DevProxy.Abstractions/Plugins/RequestArguments.cs b/DevProxy.Abstractions/Plugins/RequestArguments.cs
new file mode 100644
index 00000000..6eda5dd8
--- /dev/null
+++ b/DevProxy.Abstractions/Plugins/RequestArguments.cs
@@ -0,0 +1,26 @@
+namespace DevProxy.Abstractions.Plugins;
+public class RequestArguments(HttpRequestMessage request, string requestId)
+{
+ public HttpRequestMessage Request { get; } = request;
+ public RequestId RequestId { get; } = requestId ?? throw new ArgumentNullException(nameof(requestId));
+}
+
+public record RequestId(string Id)
+{
+ private string Id { get; } = Id ?? throw new ArgumentNullException(nameof(Id));
+ public static implicit operator string(RequestId requestId)
+ {
+ ArgumentNullException.ThrowIfNull(requestId);
+ return requestId.Id;
+ }
+
+ public static implicit operator RequestId(string id)
+ {
+ return new(id);
+ }
+
+ public static RequestId FromString(string id)
+ {
+ return new RequestId(id);
+ }
+}
\ No newline at end of file
diff --git a/DevProxy.Abstractions/Plugins/ResponseArguments.cs b/DevProxy.Abstractions/Plugins/ResponseArguments.cs
new file mode 100644
index 00000000..3803d27e
--- /dev/null
+++ b/DevProxy.Abstractions/Plugins/ResponseArguments.cs
@@ -0,0 +1,5 @@
+namespace DevProxy.Abstractions.Plugins;
+public class ResponseArguments(HttpRequestMessage request, HttpResponseMessage response, string requestId) : RequestArguments(request, requestId)
+{
+ public HttpResponseMessage Response { get; } = response;
+}
diff --git a/DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs b/DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs
index ccda0309..bb3a5ce9 100644
--- a/DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs
+++ b/DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs
@@ -15,8 +15,7 @@
using System.Net;
using System.Text.Json;
using System.Text.RegularExpressions;
-using Unobtanium.Web.Proxy.Http;
-using Unobtanium.Web.Proxy.Models;
+using System.Text;
namespace DevProxy.Plugins.Behavior;
@@ -40,7 +39,8 @@ public sealed class GenericRandomErrorPlugin(
ILogger logger,
ISet urlsToWatch,
IProxyConfiguration proxyConfiguration,
- IConfigurationSection pluginConfigurationSection) :
+ IConfigurationSection pluginConfigurationSection,
+ IProxyStorage proxyStorage) :
BasePlugin(
httpClient,
logger,
@@ -108,62 +108,62 @@ public override void OptionsLoaded(OptionsLoadedArgs e)
}
}
- public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken cancellationToken)
+ public override Func>? OnRequestAsync => (args, cancellationToken) =>
{
- Logger.LogTrace("{Method} called", nameof(BeforeRequestAsync));
+ Logger.LogTrace("{Method} called", nameof(OnRequestAsync));
- ArgumentNullException.ThrowIfNull(e);
-
- if (!e.HasRequestUrlMatch(UrlsToWatch))
- {
- Logger.LogRequest("URL not matched", MessageType.Skipped, new(e.Session));
- return Task.CompletedTask;
- }
- if (e.ResponseState.HasBeenSet)
+ if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.Request.RequestUri))
{
- Logger.LogRequest("Response already set", MessageType.Skipped, new(e.Session));
- return Task.CompletedTask;
+ Logger.LogRequest("URL not matched", MessageType.Skipped, args.Request);
+ return Task.FromResult(PluginResponse.Continue());
}
var failMode = ShouldFail();
if (failMode == GenericRandomErrorFailMode.PassThru && Configuration.Rate != 100)
{
- Logger.LogRequest("Pass through", MessageType.Skipped, new(e.Session));
- return Task.CompletedTask;
+ Logger.LogRequest("Pass through", MessageType.Skipped, args.Request);
+ return Task.FromResult(PluginResponse.Continue());
}
- FailResponse(e);
- Logger.LogTrace("Left {Name}", nameof(BeforeRequestAsync));
- return Task.CompletedTask;
- }
+ var response = FailResponse(args.Request);
+ if (response != null)
+ {
+ Logger.LogTrace("Left {Name}", nameof(OnRequestAsync));
+ return Task.FromResult(PluginResponse.Respond(response));
+ }
+
+ Logger.LogTrace("Left {Name}", nameof(OnRequestAsync));
+ return Task.FromResult(PluginResponse.Continue());
+ };
// uses config to determine if a request should be failed
private GenericRandomErrorFailMode ShouldFail() => _random.Next(1, 100) <= Configuration.Rate ? GenericRandomErrorFailMode.Random : GenericRandomErrorFailMode.PassThru;
- private void FailResponse(ProxyRequestArgs e)
+ private HttpResponseMessage? FailResponse(HttpRequestMessage request)
{
- var matchingResponse = GetMatchingErrorResponse(e.Session.HttpClient.Request);
+ var matchingResponse = GetMatchingErrorResponse(request);
if (matchingResponse is not null &&
matchingResponse.Responses is not null)
{
// pick a random error response for the current request
var error = matchingResponse.Responses.ElementAt(_random.Next(0, matchingResponse.Responses.Count()));
- UpdateProxyResponse(e, error);
+ return UpdateProxyResponse(request, error);
}
else
{
- Logger.LogRequest("No matching error response found", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest("No matching error response found", MessageType.Skipped, request);
+ return null;
}
}
- private ThrottlingInfo ShouldThrottle(Request request, string throttlingKey)
+ private ThrottlingInfo ShouldThrottle(HttpRequestMessage request, string throttlingKey)
{
var throttleKeyForRequest = BuildThrottleKey(request);
return new(throttleKeyForRequest == throttlingKey ? Configuration.RetryAfterInSeconds : 0, "Retry-After");
}
- private GenericErrorResponse? GetMatchingErrorResponse(Request request)
+ private GenericErrorResponse? GetMatchingErrorResponse(HttpRequestMessage request)
{
if (Configuration.Errors is null ||
!Configuration.Errors.Any())
@@ -183,13 +183,13 @@ private ThrottlingInfo ShouldThrottle(Request request, string throttlingKey)
return false;
}
- if (errorResponse.Request.Method != request.Method)
+ if (errorResponse.Request.Method != request.Method.Method)
{
return false;
}
- if (errorResponse.Request.Url == request.Url &&
- HasMatchingBody(errorResponse, request))
+ if (errorResponse.Request.Url == request.RequestUri?.ToString() &&
+ HasMatchingBodyAsync(errorResponse, request).GetAwaiter().GetResult())
{
return true;
}
@@ -203,32 +203,34 @@ private ThrottlingInfo ShouldThrottle(Request request, string throttlingKey)
// turn mock URL with wildcard into a regex and match against the request URL
var errorResponseUrlRegex = Regex.Escape(errorResponse.Request.Url).Replace("\\*", ".*", StringComparison.OrdinalIgnoreCase);
- return Regex.IsMatch(request.Url, $"^{errorResponseUrlRegex}$") &&
- HasMatchingBody(errorResponse, request);
+ return request.RequestUri != null &&
+ Regex.IsMatch(request.RequestUri.ToString(), $"^{errorResponseUrlRegex}$") &&
+ HasMatchingBodyAsync(errorResponse, request).GetAwaiter().GetResult();
});
return errorResponse;
}
- private void UpdateProxyResponse(ProxyRequestArgs e, GenericErrorResponseResponse error)
+ private HttpResponseMessage UpdateProxyResponse(HttpRequestMessage request, GenericErrorResponseResponse error)
{
- var session = e.Session;
- var request = session.HttpClient.Request;
var headers = new List();
if (error.Headers is not null)
{
headers.AddRange(error.Headers);
}
+ // Note: Global data handling for throttling is temporarily disabled
+ // This needs to be addressed with a proper service for managing throttled requests
+ // TODO: Implement proper throttling service for the new API
if (error.StatusCode == (int)HttpStatusCode.TooManyRequests &&
error.Headers is not null &&
error.Headers.FirstOrDefault(h => h.Name is "Retry-After" or "retry-after")?.Value == "@dynamic")
{
var retryAfterDate = DateTime.Now.AddSeconds(Configuration.RetryAfterInSeconds);
- if (!e.GlobalData.TryGetValue(RetryAfterPlugin.ThrottledRequestsKey, out var value))
+ if (!proxyStorage.GlobalData.TryGetValue(RetryAfterPlugin.ThrottledRequestsKey, out var value))
{
value = new List();
- e.GlobalData.Add(RetryAfterPlugin.ThrottledRequestsKey, value);
+ proxyStorage.GlobalData.Add(RetryAfterPlugin.ThrottledRequestsKey, value);
}
var throttledRequests = value as List;
throttledRequests?.Add(new(BuildThrottleKey(request), ShouldThrottle, retryAfterDate));
@@ -240,6 +242,9 @@ error.Headers is not null &&
var statusCode = (HttpStatusCode)(error.StatusCode ?? 400);
var body = error.Body is null ? string.Empty : JsonSerializer.Serialize(error.Body, ProxyUtils.JsonSerializerOptions);
+
+ var response = new HttpResponseMessage(statusCode);
+
// we get a JSON string so need to start with the opening quote
if (body.StartsWith("\"@"))
{
@@ -252,20 +257,39 @@ error.Headers is not null &&
if (!File.Exists(filePath))
{
Logger.LogError("File {FilePath} not found. Serving file path in the mock response", (string?)filePath);
- session.GenericResponse(body, statusCode, headers.Select(h => new HttpHeader(h.Name, h.Value)));
+ response.Content = new StringContent(body, Encoding.UTF8, "application/json");
}
else
{
var bodyBytes = File.ReadAllBytes(filePath);
- session.GenericResponse(bodyBytes, statusCode, headers.Select(h => new HttpHeader(h.Name, h.Value)));
+ response.Content = new ByteArrayContent(bodyBytes);
}
}
else
{
- session.GenericResponse(body, statusCode, headers.Select(h => new HttpHeader(h.Name, h.Value)));
+ response.Content = new StringContent(body, Encoding.UTF8, "application/json");
+ }
+
+ // Add headers to response
+ foreach (var header in headers)
+ {
+ if (header.Name.Equals("Content-Type", StringComparison.OrdinalIgnoreCase))
+ {
+ // Content-Type header goes on the content, not the response
+ if (response.Content != null)
+ {
+ _ = response.Content.Headers.Remove("Content-Type");
+ response.Content.Headers.Add("Content-Type", header.Value);
+ }
+ }
+ else
+ {
+ response.Headers.Add(header.Name, header.Value);
+ }
}
- e.ResponseState.HasBeenSet = true;
- Logger.LogRequest($"{error.StatusCode} {statusCode}", MessageType.Chaos, new(e.Session));
+
+ Logger.LogRequest($"{error.StatusCode} {statusCode}", MessageType.Chaos, request);
+ return response;
}
private void ValidateErrors()
@@ -316,9 +340,9 @@ private void ValidateErrors()
);
}
- private static bool HasMatchingBody(GenericErrorResponse errorResponse, Request request)
+ private static async Task HasMatchingBodyAsync(GenericErrorResponse errorResponse, HttpRequestMessage request)
{
- if (request.Method == "GET")
+ if (request.Method == HttpMethod.Get)
{
// GET requests don't have a body so we can't match on it
return true;
@@ -330,16 +354,24 @@ private static bool HasMatchingBody(GenericErrorResponse errorResponse, Request
return true;
}
- if (!request.HasBody || string.IsNullOrEmpty(request.BodyString))
+ if (request.Content == null)
+ {
+ // error response defines a body fragment but the request has no body
+ // so it can't match
+ return false;
+ }
+
+ var requestBody = await request.Content.ReadAsStringAsync();
+ if (string.IsNullOrEmpty(requestBody))
{
// error response defines a body fragment but the request has no body
// so it can't match
return false;
}
- return request.BodyString.Contains(errorResponse.Request.BodyFragment, StringComparison.OrdinalIgnoreCase);
+ return requestBody.Contains(errorResponse.Request.BodyFragment, StringComparison.OrdinalIgnoreCase);
}
// throttle requests per host
- private static string BuildThrottleKey(Request r) => r.RequestUri.Host;
+ private static string BuildThrottleKey(HttpRequestMessage request) => request.RequestUri?.Host ?? "";
}
diff --git a/DevProxy.Plugins/Behavior/GraphRandomErrorPlugin.cs b/DevProxy.Plugins/Behavior/GraphRandomErrorPlugin.cs
index 4579c9bb..f19336a7 100644
--- a/DevProxy.Plugins/Behavior/GraphRandomErrorPlugin.cs
+++ b/DevProxy.Plugins/Behavior/GraphRandomErrorPlugin.cs
@@ -4,7 +4,6 @@
using DevProxy.Abstractions.Proxy;
using DevProxy.Abstractions.Plugins;
-using DevProxy.Abstractions.Models;
using DevProxy.Abstractions.Utils;
using DevProxy.Plugins.Utils;
using Microsoft.Extensions.Configuration;
diff --git a/DevProxy.Plugins/Guidance/CachingGuidancePlugin.cs b/DevProxy.Plugins/Guidance/CachingGuidancePlugin.cs
index 42508ec5..f596649e 100644
--- a/DevProxy.Plugins/Guidance/CachingGuidancePlugin.cs
+++ b/DevProxy.Plugins/Guidance/CachingGuidancePlugin.cs
@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using DevProxy.Abstractions.Models;
using DevProxy.Abstractions.Proxy;
using DevProxy.Abstractions.Plugins;
using DevProxy.Abstractions.Utils;
diff --git a/DevProxy.Plugins/Guidance/GraphBetaSupportGuidancePlugin.cs b/DevProxy.Plugins/Guidance/GraphBetaSupportGuidancePlugin.cs
index 905dbe81..6b95696c 100644
--- a/DevProxy.Plugins/Guidance/GraphBetaSupportGuidancePlugin.cs
+++ b/DevProxy.Plugins/Guidance/GraphBetaSupportGuidancePlugin.cs
@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using DevProxy.Abstractions.Models;
using DevProxy.Abstractions.Proxy;
using DevProxy.Abstractions.Plugins;
using DevProxy.Abstractions.Utils;
diff --git a/DevProxy.Plugins/Guidance/GraphClientRequestIdGuidancePlugin.cs b/DevProxy.Plugins/Guidance/GraphClientRequestIdGuidancePlugin.cs
index cddc5941..8f43ce9e 100644
--- a/DevProxy.Plugins/Guidance/GraphClientRequestIdGuidancePlugin.cs
+++ b/DevProxy.Plugins/Guidance/GraphClientRequestIdGuidancePlugin.cs
@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using DevProxy.Abstractions.Models;
using DevProxy.Abstractions.Plugins;
using DevProxy.Abstractions.Proxy;
using DevProxy.Abstractions.Utils;
diff --git a/DevProxy.Plugins/Guidance/GraphConnectorGuidancePlugin.cs b/DevProxy.Plugins/Guidance/GraphConnectorGuidancePlugin.cs
index a5aab005..8041b760 100644
--- a/DevProxy.Plugins/Guidance/GraphConnectorGuidancePlugin.cs
+++ b/DevProxy.Plugins/Guidance/GraphConnectorGuidancePlugin.cs
@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using DevProxy.Abstractions.Models;
using DevProxy.Abstractions.Proxy;
using DevProxy.Abstractions.Plugins;
using DevProxy.Abstractions.Utils;
diff --git a/DevProxy.Plugins/Guidance/GraphSdkGuidancePlugin.cs b/DevProxy.Plugins/Guidance/GraphSdkGuidancePlugin.cs
index a3625732..be224f0e 100644
--- a/DevProxy.Plugins/Guidance/GraphSdkGuidancePlugin.cs
+++ b/DevProxy.Plugins/Guidance/GraphSdkGuidancePlugin.cs
@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using DevProxy.Abstractions.Models;
using DevProxy.Abstractions.Plugins;
using DevProxy.Abstractions.Proxy;
using DevProxy.Abstractions.Utils;
@@ -21,32 +20,32 @@ public sealed class GraphSdkGuidancePlugin(
{
Logger.LogTrace("{Method} called", nameof(OnResponseLogAsync));
- if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.HttpRequestMessage.RequestUri))
+ if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.Request.RequestUri))
{
- Logger.LogRequest("URL not matched", MessageType.Skipped, args.HttpRequestMessage);
+ Logger.LogRequest("URL not matched", MessageType.Skipped, args.Request);
return Task.CompletedTask;
}
- if (args.HttpRequestMessage.Method == HttpMethod.Options)
+ if (args.Request.Method == HttpMethod.Options)
{
- Logger.LogRequest("Skipping OPTIONS request", MessageType.Skipped, args.HttpRequestMessage);
+ Logger.LogRequest("Skipping OPTIONS request", MessageType.Skipped, args.Request);
return Task.CompletedTask;
}
// only show the message if there is an error.
- if ((int)args.HttpResponseMessage.StatusCode >= 400)
+ if ((int)args.Response.StatusCode >= 400)
{
- if (WarnNoSdk(args.HttpRequestMessage))
+ if (WarnNoSdk(args.Request))
{
- Logger.LogRequest(MessageUtils.BuildUseSdkForErrorsMessage(), MessageType.Tip, args.HttpRequestMessage);
+ Logger.LogRequest(MessageUtils.BuildUseSdkForErrorsMessage(), MessageType.Tip, args.Request);
}
else
{
- Logger.LogRequest("Request issued using SDK", MessageType.Skipped, args.HttpRequestMessage);
+ Logger.LogRequest("Request issued using SDK", MessageType.Skipped, args.Request);
}
}
else
{
- Logger.LogRequest("Skipping non-error response", MessageType.Skipped, args.HttpRequestMessage);
+ Logger.LogRequest("Skipping non-error response", MessageType.Skipped, args.Request);
}
Logger.LogTrace("Left {Name}", nameof(OnResponseLogAsync));
diff --git a/DevProxy.Plugins/Guidance/GraphSelectGuidancePlugin.cs b/DevProxy.Plugins/Guidance/GraphSelectGuidancePlugin.cs
index bd6cbfd2..01fe1019 100644
--- a/DevProxy.Plugins/Guidance/GraphSelectGuidancePlugin.cs
+++ b/DevProxy.Plugins/Guidance/GraphSelectGuidancePlugin.cs
@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information.
using DevProxy.Abstractions.Data;
-using DevProxy.Abstractions.Models;
using DevProxy.Abstractions.Proxy;
using DevProxy.Abstractions.Plugins;
using DevProxy.Abstractions.Utils;
diff --git a/DevProxy.Plugins/Guidance/ODSPSearchGuidancePlugin.cs b/DevProxy.Plugins/Guidance/ODSPSearchGuidancePlugin.cs
index 20727a6f..e27b95df 100644
--- a/DevProxy.Plugins/Guidance/ODSPSearchGuidancePlugin.cs
+++ b/DevProxy.Plugins/Guidance/ODSPSearchGuidancePlugin.cs
@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using DevProxy.Abstractions.Models;
using DevProxy.Abstractions.Proxy;
using DevProxy.Abstractions.Plugins;
using DevProxy.Abstractions.Utils;
diff --git a/DevProxy.Plugins/Guidance/ODataPagingGuidancePlugin.cs b/DevProxy.Plugins/Guidance/ODataPagingGuidancePlugin.cs
index 8f7968b4..be48df49 100644
--- a/DevProxy.Plugins/Guidance/ODataPagingGuidancePlugin.cs
+++ b/DevProxy.Plugins/Guidance/ODataPagingGuidancePlugin.cs
@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using DevProxy.Abstractions.Models;
using DevProxy.Abstractions.Proxy;
using DevProxy.Abstractions.Plugins;
using DevProxy.Abstractions.Utils;
@@ -59,42 +58,42 @@ public sealed class ODataPagingGuidancePlugin(
{
Logger.LogTrace("{Method} called", nameof(OnResponseLogAsync));
- if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.HttpRequestMessage.RequestUri))
+ if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.Request.RequestUri))
{
- Logger.LogRequest("URL not matched", MessageType.Skipped, args.HttpRequestMessage);
+ Logger.LogRequest("URL not matched", MessageType.Skipped, args.Request);
return;
}
- if (args.HttpRequestMessage.Method != HttpMethod.Get)
+ if (args.Request.Method != HttpMethod.Get)
{
- Logger.LogRequest("Skipping non-GET request", MessageType.Skipped, args.HttpRequestMessage);
+ Logger.LogRequest("Skipping non-GET request", MessageType.Skipped, args.Request);
return;
}
- if ((int)args.HttpResponseMessage.StatusCode >= 300)
+ if ((int)args.Response.StatusCode >= 300)
{
- Logger.LogRequest("Skipping non-success response", MessageType.Skipped, args.HttpRequestMessage);
+ Logger.LogRequest("Skipping non-success response", MessageType.Skipped, args.Request);
return;
}
- var mediaType = args.HttpResponseMessage.Content?.Headers?.ContentType?.MediaType;
+ var mediaType = args.Response.Content?.Headers?.ContentType?.MediaType;
if (mediaType is null ||
(!mediaType.Contains("json", StringComparison.OrdinalIgnoreCase) &&
!mediaType.Contains("application/atom+xml", StringComparison.OrdinalIgnoreCase)))
{
- Logger.LogRequest("Skipping response with unsupported body type", MessageType.Skipped, args.HttpRequestMessage);
+ Logger.LogRequest("Skipping response with unsupported body type", MessageType.Skipped, args.Request);
return;
}
- if (args.HttpResponseMessage.Content is null)
+ if (args.Response.Content is null)
{
- Logger.LogRequest("Skipping response with no content", MessageType.Skipped, args.HttpRequestMessage);
+ Logger.LogRequest("Skipping response with no content", MessageType.Skipped, args.Request);
return;
}
var nextLink = string.Empty;
- var bodyString = await args.HttpResponseMessage.Content.ReadAsStringAsync(cancellationToken);
+ var bodyString = await args.Response.Content.ReadAsStringAsync(cancellationToken);
if (string.IsNullOrEmpty(bodyString))
{
- Logger.LogRequest("Skipping empty response body", MessageType.Skipped, args.HttpRequestMessage);
+ Logger.LogRequest("Skipping empty response body", MessageType.Skipped, args.Request);
return;
}
@@ -113,7 +112,7 @@ public sealed class ODataPagingGuidancePlugin(
}
else
{
- Logger.LogRequest("No next link found in the response", MessageType.Skipped, args.HttpRequestMessage);
+ Logger.LogRequest("No next link found in the response", MessageType.Skipped, args.Request);
}
Logger.LogTrace("Left {Name}", nameof(OnResponseLogAsync));
diff --git a/DevProxy.Plugins/inventory.md b/DevProxy.Plugins/inventory.md
index 50ff166a..66faccde 100644
--- a/DevProxy.Plugins/inventory.md
+++ b/DevProxy.Plugins/inventory.md
@@ -100,7 +100,7 @@ This document provides an inventory of all plugins in the DevProxy.Plugins proje
- GetOptions
- OptionsLoaded
- InitializeAsync
-- BeforeRequestAsync
+- OnRequestAsync (NEW API - MIGRATED)
**Behavior:** Modifies responses (generates random error responses)
@@ -374,11 +374,11 @@ This document provides an inventory of all plugins in the DevProxy.Plugins proje
## Summary
- **Total Plugins:** 38
-- **Plugins using BeforeRequestAsync:** 27 (decreased by 3 due to ODataPagingGuidancePlugin and ODSPSearchGuidancePlugin migrations)
+- **Plugins using BeforeRequestAsync:** 26 (decreased by 4 due to ODataPagingGuidancePlugin, ODSPSearchGuidancePlugin, and GenericRandomErrorPlugin migrations)
- **Plugins using BeforeResponseAsync:** 2 (DevToolsPlugin, RateLimitingPlugin)
- **Plugins using AfterResponseAsync:** 5 (decreased by 2 due to ODataPagingGuidancePlugin and GraphSdkGuidancePlugin migrations: GraphSelectGuidancePlugin, OpenAITelemetryPlugin)
- **Plugins using AfterRequestLogAsync:** 1 (DevToolsPlugin)
- **Plugins using AfterRecordingStopAsync:** 8 (ApiCenterMinimalPermissionsPlugin, ExecutionSummaryPlugin, GraphMinimalPermissionsGuidancePlugin, HttpFileGeneratorPlugin, MockGeneratorPlugin, OpenAITelemetryPlugin, OpenApiSpecGeneratorPlugin, TypeSpecGeneratorPlugin)
-- **Plugins using OnRequestAsync (NEW API):** 1 (GraphRandomErrorPlugin - already migrated)
+- **Plugins using OnRequestAsync (NEW API):** 2 (GraphRandomErrorPlugin, GenericRandomErrorPlugin - migrated)
- **Plugins using OnRequestLogAsync (NEW API):** 6 (GraphBetaSupportGuidancePlugin, CachingGuidancePlugin, GraphClientRequestIdGuidancePlugin, GraphConnectorGuidancePlugin, ODataPagingGuidancePlugin, ODSPSearchGuidancePlugin - migrated)
- **Plugins using OnResponseLogAsync (NEW API):** 2 (ODataPagingGuidancePlugin, GraphSdkGuidancePlugin - migrated)
\ No newline at end of file
diff --git a/DevProxy.Plugins/migration.md b/DevProxy.Plugins/migration.md
index 03457984..545d2052 100644
--- a/DevProxy.Plugins/migration.md
+++ b/DevProxy.Plugins/migration.md
@@ -458,6 +458,75 @@ public override Func? OnResponseLogA
};
```
+### Example 4: Plugin with Storage Requirements
+
+**Before (Old API):**
+```csharp
+public sealed class MyStoragePlugin(
+ ILogger logger,
+ ISet urlsToWatch) : BasePlugin(logger, urlsToWatch)
+{
+ public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken cancellationToken)
+ {
+ // Access global data
+ e.GlobalData["RequestCount"] = (int)(e.GlobalData.GetValueOrDefault("RequestCount", 0)) + 1;
+
+ // Access session data
+ e.SessionData["RequestStartTime"] = DateTime.UtcNow;
+
+ return Task.CompletedTask;
+ }
+
+ public override Task AfterResponseAsync(ProxyResponseArgs e, CancellationToken cancellationToken)
+ {
+ // Use session data
+ if (e.SessionData.TryGetValue("RequestStartTime", out var startTime))
+ {
+ var duration = DateTime.UtcNow - (DateTime)startTime;
+ Logger.LogInformation("Request took {Duration}ms", duration.TotalMilliseconds);
+ }
+
+ return Task.CompletedTask;
+ }
+}
+```
+
+**After (New API):**
+```csharp
+public sealed class MyStoragePlugin(
+ ILogger logger,
+ ISet urlsToWatch,
+ IProxyStorage proxyStorage) : BasePlugin(logger, urlsToWatch)
+{
+ private readonly IProxyStorage _proxyStorage = proxyStorage;
+
+ public override Func? OnRequestLogAsync => (args, cancellationToken) =>
+ {
+ // Access global data
+ _proxyStorage.GlobalData["RequestCount"] = (int)(_proxyStorage.GlobalData.GetValueOrDefault("RequestCount", 0)) + 1;
+
+ // Access request-specific data
+ var requestData = _proxyStorage.GetRequestData(args.RequestId);
+ requestData["RequestStartTime"] = DateTime.UtcNow;
+
+ return Task.CompletedTask;
+ };
+
+ public override Func? OnResponseLogAsync => (args, cancellationToken) =>
+ {
+ // Use request-specific data
+ var requestData = _proxyStorage.GetRequestData(args.RequestId);
+ if (requestData.TryGetValue("RequestStartTime", out var startTime))
+ {
+ var duration = DateTime.UtcNow - (DateTime)startTime;
+ Logger.LogInformation("Request took {Duration}ms", duration.TotalMilliseconds);
+ }
+
+ return Task.CompletedTask;
+ };
+}
+```
+
## Important Notes
### 1. Logging Context
@@ -474,7 +543,60 @@ Logger.LogRequest("Message", MessageType.Info, args.HttpRequestMessage);
```
### 2. Global Data and Session Data
-Global data and session data access patterns will need to be reviewed as they may not be available in the new API. These features may be handled differently or through dependency injection.
+Global data and session data access patterns will need to be reviewed as they may not be available in the new API. These features are now handled through dependency injection using the `IProxyStorage` interface.
+
+**For plugins that need global or request-specific storage:**
+
+Use constructor injection to access the `IProxyStorage` interface:
+
+```csharp
+public sealed class MyPlugin(
+ ILogger logger,
+ ISet urlsToWatch,
+ IProxyStorage proxyStorage) : BasePlugin(logger, urlsToWatch)
+{
+ private readonly IProxyStorage _proxyStorage = proxyStorage;
+
+ public override Func>? OnRequestAsync => (args, cancellationToken) =>
+ {
+ // Access global data (shared across all requests)
+ _proxyStorage.GlobalData["MyKey"] = "MyValue";
+
+ // Access request-specific data using the request ID
+ var requestData = _proxyStorage.GetRequestData(args.RequestId);
+ requestData["RequestSpecificKey"] = "RequestSpecificValue";
+
+ return Task.FromResult(PluginResponse.Continue());
+ };
+}
+```
+
+**Migration patterns:**
+
+```csharp
+// Old API - Global Data
+e.GlobalData["MyKey"] = "MyValue";
+var globalValue = e.GlobalData.GetValueOrDefault("MyKey");
+
+// New API - Global Data
+_proxyStorage.GlobalData["MyKey"] = "MyValue";
+var globalValue = _proxyStorage.GlobalData.GetValueOrDefault("MyKey");
+
+// Old API - Session Data
+e.SessionData["MyKey"] = "MyValue";
+var sessionValue = e.SessionData.GetValueOrDefault("MyKey");
+
+// New API - Request Data
+var requestData = _proxyStorage.GetRequestData(args.RequestId);
+requestData["MyKey"] = "MyValue";
+var requestValue = requestData.GetValueOrDefault("MyKey");
+```
+
+**Important notes about storage:**
+- **Global data** persists across all requests and is shared between all plugins
+- **Request data** is specific to a single request and is automatically cleaned up when the request completes
+- Request data is accessed using the `RequestId` from the `RequestArguments` or `ResponseArguments`
+- For reporting plugins that need to store reports, use global data as shown in `BaseReportingPlugin.StoreReport()`
### 3. New API Benefits
The new API methods provide several advantages:
@@ -535,6 +657,7 @@ public override Func>?
- [ ] Replace `e.ResponseState.HasBeenSet = true` with `PluginResponse.Respond()`
- [ ] Replace `return Task.CompletedTask` with `PluginResponse.Continue()`
- [ ] Update logging context from `LoggingContext(e.Session)` to `args.Request`
+- [ ] Add `IProxyStorage` to constructor if plugin needs global or request-specific data storage
- [ ] Test the migrated plugin thoroughly
### For Request Guidance Plugins (OnRequestLogAsync):
@@ -545,6 +668,7 @@ public override Func>?
- [ ] Replace `e.HasRequestUrlMatch()` with `ProxyUtils.MatchesUrlToWatch()`
- [ ] Remove any response modification logic (not allowed in OnRequestLogAsync)
- [ ] Update logging context from `LoggingContext(e.Session)` to `args.Request`
+- [ ] Add `IProxyStorage` to constructor if plugin needs global or request-specific data storage
- [ ] Test the migrated plugin thoroughly
### For Response Guidance Plugins (OnResponseLogAsync):
@@ -556,6 +680,7 @@ public override Func>?
- [ ] Replace `e.HasRequestUrlMatch()` with `ProxyUtils.MatchesUrlToWatch()`
- [ ] Remove any response modification logic (not allowed in OnResponseLogAsync)
- [ ] Update logging context from `LoggingContext(e.Session)` to `args.HttpRequestMessage`
+- [ ] Add `IProxyStorage` to constructor if plugin needs global or request-specific data storage
- [ ] Test the migrated plugin thoroughly
### For Response Modifying Plugins (OnResponseAsync):
@@ -568,6 +693,7 @@ public override Func>?
- [ ] Remove `e.ResponseState.HasBeenSet` checks
- [ ] Return `null` to continue or `PluginResponse` to modify
- [ ] Update logging context from `LoggingContext(e.Session)` to `args.HttpRequestMessage`
+- [ ] Add `IProxyStorage` to constructor if plugin needs global or request-specific data storage
- [ ] Test the migrated plugin thoroughly
## Plugin Migration Categorization
@@ -578,7 +704,7 @@ Based on the inventory, here's how plugins should be migrated:
1. AuthPlugin
2. CrudApiPlugin
3. EntraMockResponsePlugin
-4. GenericRandomErrorPlugin
+4. ~~GenericRandomErrorPlugin~~ (MIGRATED)
5. GraphMockResponsePlugin
6. GraphRandomErrorPlugin (already migrated)
7. LanguageModelFailurePlugin
@@ -619,4 +745,4 @@ Based on the inventory, here's how plugins should be migrated:
- **DevToolsPlugin**: Uses multiple methods (BeforeRequestAsync, BeforeResponseAsync, AfterResponseAsync, AfterRequestLogAsync)
- **LatencyPlugin**: Adds delay but doesn't modify responses (could use OnRequestLogAsync)
-The new API methods enable better control flow by allowing the proxy to handle modification and logging operations separately, improving both performance and code clarity.The new API methods enable better control flow by allowing the proxy to handle modification and logging operations separately, improving both performance and code clarity.
\ No newline at end of file
+The new API methods enable better control flow by allowing the proxy to handle modification and logging operations separately, improving both performance and code clarity.
\ No newline at end of file
diff --git a/DevProxy/Extensions/IServiceCollectionExtensions.cs b/DevProxy/Extensions/IServiceCollectionExtensions.cs
index 6290856a..d0664e5b 100644
--- a/DevProxy/Extensions/IServiceCollectionExtensions.cs
+++ b/DevProxy/Extensions/IServiceCollectionExtensions.cs
@@ -5,8 +5,10 @@
using DevProxy;
using DevProxy.Abstractions.Data;
using DevProxy.Abstractions.LanguageModel;
+using DevProxy.Abstractions.Plugins;
using DevProxy.Abstractions.Proxy;
using DevProxy.Commands;
+using DevProxy.Plugins;
using DevProxy.Proxy;
using OpenTelemetry;
using OpenTelemetry.Metrics;
@@ -61,6 +63,7 @@ static IServiceCollection AddApplicationServices(
.AddSingleton()
.AddSingleton()
.AddSingleton()
+ .AddSingleton()
// TODO: Removed the injected certificate
//.AddSingleton(sp => ProxyEngine.Certificate!) // Why is this injected?
//.AddSingleton(sp => sp.GetRequiredService().CertificateManager.RootCertificate!)
diff --git a/DevProxy/Plugins/ProxyStorage.cs b/DevProxy/Plugins/ProxyStorage.cs
new file mode 100644
index 00000000..08835c72
--- /dev/null
+++ b/DevProxy/Plugins/ProxyStorage.cs
@@ -0,0 +1,24 @@
+using DevProxy.Abstractions.Plugins;
+using DevProxy.Proxy;
+using System.Collections.Concurrent;
+
+namespace DevProxy.Plugins;
+
+///
+/// Default implementation of .
+///
+internal class ProxyStorage : IProxyStorage
+{
+ internal ProxyStorage(IProxyState proxyState)
+ {
+ GlobalData = proxyState.GlobalData ?? throw new ArgumentException("GlobalData cannot be null.", nameof(proxyState));
+ }
+ public Dictionary GlobalData { get; private set; }
+
+ Dictionary IProxyStorage.GlobalData => throw new NotImplementedException();
+
+ public Dictionary GetRequestData(RequestId id) => _requestData.TryGetValue(id, out var data) ? data : [];
+ public void RemoveRequestData(RequestId id) => _requestData.Remove(id, out _);
+
+ private readonly ConcurrentDictionary> _requestData = [];
+}
diff --git a/DevProxy/Proxy/ProxyEngine.cs b/DevProxy/Proxy/ProxyEngine.cs
index c0bd8bd9..3be69cd5 100755
--- a/DevProxy/Proxy/ProxyEngine.cs
+++ b/DevProxy/Proxy/ProxyEngine.cs
@@ -29,7 +29,8 @@ sealed class ProxyEngine(
IProxyStateController proxyController,
ILogger logger,
ProxyServerEvents proxyEvents,
- ICertificateManager certificateManager) : BackgroundService, IDisposable
+ ICertificateManager certificateManager,
+ IProxyStorage proxyStorage) : BackgroundService, IDisposable
{
internal const string ACTIVITY_SOURCE_NAME = "DevProxy.Proxy.ProxyEngine";
public static readonly ActivitySource ActivitySource = new(ACTIVITY_SOURCE_NAME);
@@ -47,7 +48,7 @@ sealed class ProxyEngine(
private readonly IProxyStateController _proxyController = proxyController;
// Dictionary for plugins to store data between requests
// the key is HashObject of the SessionEventArgs object
- private readonly ConcurrentDictionary> _pluginData = [];
+ //private readonly ConcurrentDictionary> _pluginData = [];
private InactivityTimer? _inactivityTimer;
private CancellationToken? _cancellationToken;
@@ -380,21 +381,21 @@ async Task OnRequestAsync(object _, RequestEventArguments
if (IsProxiedHost(requestEventArguments.Request.RequestUri!.Host) &&
IsIncludedByHeaders(requestEventArguments.Request.Headers))
{
- if (!_pluginData.TryAdd(requestEventArguments.RequestId, []))
- {
- throw new InvalidOperationException($"Unable to initialize the plugin data storage for hash key {requestEventArguments.RequestId}");
- }
+ //if (!_pluginData.TryAdd(requestEventArguments.RequestId, []))
+ //{
+ // throw new InvalidOperationException($"Unable to initialize the plugin data storage for hash key {requestEventArguments.RequestId}");
+ //}
if (!ProxyUtils.MatchesUrlToWatch(_urlsToWatch, requestEventArguments.Request.RequestUri.AbsoluteUri))
{
return RequestEventResponse.ContinueResponse();
}
- if (!_pluginData.TryAdd(requestEventArguments.RequestId, []))
- {
- // Throwing here will break the request....
- throw new InvalidOperationException($"Unable to initialize the plugin data storage for hash key {requestEventArguments.RequestId}");
- }
+ //if (!_pluginData.TryAdd(requestEventArguments.RequestId, []))
+ //{
+ // // Throwing here will break the request....
+ // throw new InvalidOperationException($"Unable to initialize the plugin data storage for hash key {requestEventArguments.RequestId}");
+ //}
using var scope = _logger.BeginRequestScope(requestEventArguments.Request.Method, requestEventArguments.Request.RequestUri, requestEventArguments.RequestId);
@@ -418,7 +419,7 @@ private async Task HandleRequestAsync(RequestEventArgument
var logPlugins = _plugins.Where(p => p.Enabled && p.OnRequestLogAsync is not null);
if (logPlugins.Any())
{
- var logArguments = new Abstractions.Models.RequestArguments(arguments.Request, arguments.RequestId);
+ var logArguments = new RequestArguments(arguments.Request, arguments.RequestId);
// Call OnRequestLogAsync for all plugins at the same time and wait for all of them to complete
var logTasks = logPlugins
.Select(plugin => plugin.OnRequestLogAsync!(logArguments, cts.Token))
@@ -444,7 +445,7 @@ private async Task HandleRequestAsync(RequestEventArgument
cts.Token.ThrowIfCancellationRequested();
try
{
- var result = await plugin.OnRequestAsync!(new Abstractions.Models.RequestArguments(arguments.Request, arguments.RequestId), cts.Token);
+ var result = await plugin.OnRequestAsync!(new RequestArguments(arguments.Request, arguments.RequestId), cts.Token);
if (result is not null)
{
if (result.Request is not null)
@@ -472,7 +473,7 @@ private async Task HandleRequestAsync(RequestEventArgument
// We only need to set the proxy header if the proxy has not set a response and the request is going to be sent to the target.
if (response is not null)
{
- _ = _pluginData.Remove(arguments.RequestId, out _);
+ proxyStorage.RemoveRequestData(arguments.RequestId);
return RequestEventResponse.EarlyResponse(response);
}
else if (request is not null)
@@ -637,7 +638,7 @@ private async Task OnResponseAsync(object sender, Respons
if (logPlugins.Any())
{
// Call OnResponseLogAsync for all plugins at the same time and wait for all of them to complete
- var logArguments = new Abstractions.Models.ResponseArguments(e.Request, e.Response, e.RequestId);
+ var logArguments = new ResponseArguments(e.Request, e.Response, e.RequestId);
var logTasks = logPlugins
.Select(plugin => plugin.OnResponseLogAsync!(logArguments, cts.Token))
.ToArray();
@@ -658,7 +659,7 @@ private async Task OnResponseAsync(object sender, Respons
try
{
- var result = await plugin.OnResponseAsync!(new Abstractions.Models.ResponseArguments(e.Request, response ?? e.Response, e.RequestId), cts.Token);
+ var result = await plugin.OnResponseAsync!(new ResponseArguments(e.Request, response ?? e.Response, e.RequestId), cts.Token);
if (result is not null)
{
if (result.Request is not null)
@@ -680,7 +681,7 @@ private async Task OnResponseAsync(object sender, Respons
}
}
_logger.LogRequest(message, MessageType.FinishedProcessingRequest, e.Request);
- _ = _pluginData.Remove(e.RequestId, out _);
+ proxyStorage.RemoveRequestData(e.RequestId);
return response is not null
? ResponseEventResponse.ModifyResponse(response)
: ResponseEventResponse.ContinueResponse();
From 874f43754dc685910a22270bf6963c3441febcaf Mon Sep 17 00:00:00 2001
From: Stephan van Rooij <1292510+svrooij@users.noreply.github.com>
Date: Tue, 19 Aug 2025 22:17:55 +0200
Subject: [PATCH 07/14] Two more plugins migrated
---
.../LanguageModelRateLimitingPlugin.cs | 188 +++++++-------
.../Behavior/RateLimitingPlugin.cs | 232 +++++++++---------
DevProxy.Plugins/migration.md | 28 +--
3 files changed, 222 insertions(+), 226 deletions(-)
diff --git a/DevProxy.Plugins/Behavior/LanguageModelRateLimitingPlugin.cs b/DevProxy.Plugins/Behavior/LanguageModelRateLimitingPlugin.cs
index 25d63f2e..ff044326 100644
--- a/DevProxy.Plugins/Behavior/LanguageModelRateLimitingPlugin.cs
+++ b/DevProxy.Plugins/Behavior/LanguageModelRateLimitingPlugin.cs
@@ -12,9 +12,8 @@
using Microsoft.Extensions.Logging;
using System.Globalization;
using System.Net;
+using System.Text;
using System.Text.Json;
-using Unobtanium.Web.Proxy.Http;
-using Unobtanium.Web.Proxy.Models;
namespace DevProxy.Plugins.Behavior;
@@ -40,7 +39,8 @@ public sealed class LanguageModelRateLimitingPlugin(
ILogger logger,
ISet urlsToWatch,
IProxyConfiguration proxyConfiguration,
- IConfigurationSection pluginConfigurationSection) :
+ IConfigurationSection pluginConfigurationSection,
+ IProxyStorage proxyStorage) :
BasePlugin(
httpClient,
logger,
@@ -48,8 +48,7 @@ public sealed class LanguageModelRateLimitingPlugin(
proxyConfiguration,
pluginConfigurationSection)
{
- // initial values so that we know when we intercept the
- // first request and can set the initial values
+ private readonly IProxyStorage _proxyStorage = proxyStorage;
private int _promptTokensRemaining = -1;
private int _completionTokensRemaining = -1;
private DateTime _resetTime = DateTime.MinValue;
@@ -71,38 +70,27 @@ public override async Task InitializeAsync(InitArgs e, CancellationToken cancell
}
}
- public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken cancellationToken)
+ public override Func>? OnRequestAsync => async (args, cancellationToken) =>
{
- Logger.LogTrace("{Method} called", nameof(BeforeRequestAsync));
+ Logger.LogTrace("{Method} called", nameof(OnRequestAsync));
- ArgumentNullException.ThrowIfNull(e);
-
- var session = e.Session;
- var state = e.ResponseState;
- if (state.HasBeenSet)
- {
- Logger.LogRequest("Response already set", MessageType.Skipped, new(e.Session));
- return Task.CompletedTask;
- }
- if (!e.HasRequestUrlMatch(UrlsToWatch))
+ if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.Request.RequestUri))
{
- Logger.LogRequest("URL not matched", MessageType.Skipped, new(e.Session));
- return Task.CompletedTask;
+ Logger.LogRequest("URL not matched", MessageType.Skipped, args.Request);
+ return PluginResponse.Continue();
}
- var request = e.Session.HttpClient.Request;
- if (request.Method is null ||
- !request.Method.Equals("POST", StringComparison.OrdinalIgnoreCase) ||
- !request.HasBody)
+ if (args.Request.Method != HttpMethod.Post || args.Request.Content == null)
{
- Logger.LogRequest("Request is not a POST request with a body", MessageType.Skipped, new(e.Session));
- return Task.CompletedTask;
+ Logger.LogRequest("Request is not a POST request with a body", MessageType.Skipped, args.Request);
+ return PluginResponse.Continue();
}
- if (!TryGetOpenAIRequest(request.BodyString, out var openAiRequest))
+ var bodyString = await args.Request.Content.ReadAsStringAsync(cancellationToken);
+ if (!TryGetOpenAIRequest(bodyString, out var openAiRequest))
{
- Logger.LogRequest("Skipping non-OpenAI request", MessageType.Skipped, new(e.Session));
- return Task.CompletedTask;
+ Logger.LogRequest("Skipping non-OpenAI request", MessageType.Skipped, args.Request);
+ return PluginResponse.Continue();
}
// set the initial values for the first request
@@ -127,71 +115,83 @@ public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken ca
// check if we have tokens available
if (_promptTokensRemaining <= 0 || _completionTokensRemaining <= 0)
{
- Logger.LogRequest($"Exceeded token limit when calling {request.Url}. Request will be throttled", MessageType.Failed, new(e.Session));
+ Logger.LogRequest($"Exceeded token limit when calling {args.Request.RequestUri}. Request will be throttled", MessageType.Failed, args.Request);
if (Configuration.WhenLimitExceeded == TokenLimitResponseWhenExceeded.Throttle)
{
- if (!e.GlobalData.TryGetValue(RetryAfterPlugin.ThrottledRequestsKey, out var value))
+ // Add throttling info to global data for RetryAfterPlugin coordination
+ if (!_proxyStorage.GlobalData.TryGetValue(RetryAfterPlugin.ThrottledRequestsKey, out var value))
{
value = new List();
- e.GlobalData.Add(RetryAfterPlugin.ThrottledRequestsKey, value);
+ _proxyStorage.GlobalData.Add(RetryAfterPlugin.ThrottledRequestsKey, value);
}
var throttledRequests = value as List;
throttledRequests?.Add(new(
- BuildThrottleKey(request),
+ BuildThrottleKey(args.Request),
ShouldThrottle,
_resetTime
));
- ThrottleResponse(e);
- state.HasBeenSet = true;
+
+ return PluginResponse.Respond(BuildThrottleResponse(args.Request));
}
else
{
if (Configuration.CustomResponse is not null)
{
- var headersList = Configuration.CustomResponse.Headers is not null ?
- Configuration.CustomResponse.Headers.Select(h => new HttpHeader(h.Name, h.Value)).ToList() :
- [];
-
- var retryAfterHeader = headersList.FirstOrDefault(h => h.Name.Equals(Configuration.HeaderRetryAfter, StringComparison.OrdinalIgnoreCase));
- if (retryAfterHeader is not null && retryAfterHeader.Value == "@dynamic")
- {
- headersList.Add(new(Configuration.HeaderRetryAfter, ((int)(_resetTime - DateTime.Now).TotalSeconds).ToString(CultureInfo.InvariantCulture)));
- _ = headersList.Remove(retryAfterHeader);
- }
-
- var headers = headersList.ToArray();
-
- // allow custom throttling response
var responseCode = (HttpStatusCode)(Configuration.CustomResponse.StatusCode ?? 200);
+
+ // Add throttling info for TooManyRequests responses
if (responseCode == HttpStatusCode.TooManyRequests)
{
- if (!e.GlobalData.TryGetValue(RetryAfterPlugin.ThrottledRequestsKey, out var value))
+ if (!_proxyStorage.GlobalData.TryGetValue(RetryAfterPlugin.ThrottledRequestsKey, out var value))
{
value = new List();
- e.GlobalData.Add(RetryAfterPlugin.ThrottledRequestsKey, value);
+ _proxyStorage.GlobalData.Add(RetryAfterPlugin.ThrottledRequestsKey, value);
}
var throttledRequests = value as List;
throttledRequests?.Add(new(
- BuildThrottleKey(request),
+ BuildThrottleKey(args.Request),
ShouldThrottle,
_resetTime
));
}
- string body = Configuration.CustomResponse.Body is not null ?
- JsonSerializer.Serialize(Configuration.CustomResponse.Body, ProxyUtils.JsonSerializerOptions) :
- "";
- e.Session.GenericResponse(body, responseCode, headers);
- state.HasBeenSet = true;
+ var response = new HttpResponseMessage(responseCode)
+ {
+ Content = new StringContent(
+ Configuration.CustomResponse.Body is not null ?
+ JsonSerializer.Serialize(Configuration.CustomResponse.Body, ProxyUtils.JsonSerializerOptions) :
+ string.Empty,
+ Encoding.UTF8,
+ "application/json")
+ };
+
+ // Add headers
+ if (Configuration.CustomResponse.Headers is not null)
+ {
+ foreach (var header in Configuration.CustomResponse.Headers)
+ {
+ var headerValue = header.Value;
+ if (header.Name.Equals(Configuration.HeaderRetryAfter, StringComparison.OrdinalIgnoreCase) && headerValue == "@dynamic")
+ {
+ headerValue = ((int)(_resetTime - DateTime.Now).TotalSeconds).ToString(CultureInfo.InvariantCulture);
+ }
+ _ = response.Headers.TryAddWithoutValidation(header.Name, headerValue);
+ }
+ }
+
+ return PluginResponse.Respond(response);
}
else
{
- Logger.LogRequest($"Custom behavior not set. {Configuration.CustomResponseFile} not found.", MessageType.Failed, new(e.Session));
- e.Session.GenericResponse("Custom response file not found.", HttpStatusCode.InternalServerError, []);
- state.HasBeenSet = true;
+ Logger.LogRequest($"Custom behavior not set. {Configuration.CustomResponseFile} not found.", MessageType.Failed, args.Request);
+ var response = new HttpResponseMessage(HttpStatusCode.InternalServerError)
+ {
+ Content = new StringContent("Custom response file not found.", Encoding.UTF8, "text/plain")
+ };
+ return PluginResponse.Respond(response);
}
}
}
@@ -200,41 +200,37 @@ public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken ca
Logger.LogDebug("Tokens remaining - Prompt: {PromptTokensRemaining}, Completion: {CompletionTokensRemaining}", _promptTokensRemaining, _completionTokensRemaining);
}
- return Task.CompletedTask;
- }
+ return PluginResponse.Continue();
+ };
- public override Task BeforeResponseAsync(ProxyResponseArgs e, CancellationToken cancellationToken)
+ public override Func>? OnResponseAsync => async (args, cancellationToken) =>
{
- Logger.LogTrace("{Method} called", nameof(BeforeResponseAsync));
-
- ArgumentNullException.ThrowIfNull(e);
+ Logger.LogTrace("{Method} called", nameof(OnResponseAsync));
- if (!e.HasRequestUrlMatch(UrlsToWatch))
+ if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.Request.RequestUri))
{
- Logger.LogRequest("URL not matched", MessageType.Skipped, new(e.Session));
- return Task.CompletedTask;
+ Logger.LogRequest("URL not matched", MessageType.Skipped, args.Request);
+ return null;
}
- var request = e.Session.HttpClient.Request;
- if (request.Method is null ||
- !request.Method.Equals("POST", StringComparison.OrdinalIgnoreCase) ||
- !request.HasBody)
+ if (args.Request.Method != HttpMethod.Post || args.Request.Content == null)
{
Logger.LogDebug("Skipping non-POST request");
- return Task.CompletedTask;
+ return null;
}
- if (!TryGetOpenAIRequest(request.BodyString, out var openAiRequest))
+ var bodyString = await args.Request.Content.ReadAsStringAsync(cancellationToken);
+ if (!TryGetOpenAIRequest(bodyString, out var openAiRequest))
{
Logger.LogDebug("Skipping non-OpenAI request");
- return Task.CompletedTask;
+ return null;
}
// Read the response body to get token usage
- var response = e.Session.HttpClient.Response;
- if (response.HasBody)
+ var httpResponse = args.Response;
+ if (httpResponse.Content != null)
{
- var responseBody = response.BodyString;
+ var responseBody = await httpResponse.Content.ReadAsStringAsync(cancellationToken);
if (!string.IsNullOrEmpty(responseBody))
{
try
@@ -257,7 +253,7 @@ public override Task BeforeResponseAsync(ProxyResponseArgs e, CancellationToken
_completionTokensRemaining = 0;
}
- Logger.LogRequest($"Consumed {promptTokens} prompt tokens and {completionTokens} completion tokens. Remaining - Prompt: {_promptTokensRemaining}, Completion: {_completionTokensRemaining}", MessageType.Processed, new(e.Session));
+ Logger.LogRequest($"Consumed {promptTokens} prompt tokens and {completionTokens} completion tokens. Remaining - Prompt: {_promptTokensRemaining}, Completion: {_completionTokensRemaining}", MessageType.Processed, args.Request);
}
}
catch (JsonException ex)
@@ -267,9 +263,9 @@ public override Task BeforeResponseAsync(ProxyResponseArgs e, CancellationToken
}
}
- Logger.LogTrace("Left {Name}", nameof(BeforeResponseAsync));
- return Task.CompletedTask;
- }
+ Logger.LogTrace("Left {Name}", nameof(OnResponseAsync));
+ return null;
+ };
private bool TryGetOpenAIRequest(string content, out OpenAIRequest? request)
{
@@ -310,7 +306,7 @@ private bool TryGetOpenAIRequest(string content, out OpenAIRequest? request)
}
}
- private ThrottlingInfo ShouldThrottle(Request request, string throttlingKey)
+ private ThrottlingInfo ShouldThrottle(HttpRequestMessage request, string throttlingKey)
{
var throttleKeyForRequest = BuildThrottleKey(request);
return new(throttleKeyForRequest == throttlingKey ?
@@ -318,12 +314,8 @@ private ThrottlingInfo ShouldThrottle(Request request, string throttlingKey)
Configuration.HeaderRetryAfter);
}
- private void ThrottleResponse(ProxyRequestArgs e)
+ private HttpResponseMessage BuildThrottleResponse(HttpRequestMessage request)
{
- var headers = new List();
- var body = string.Empty;
- var request = e.Session.HttpClient.Request;
-
// Build standard OpenAI error response for token limit exceeded
var openAiError = new
{
@@ -335,17 +327,23 @@ private void ThrottleResponse(ProxyRequestArgs e)
code = "insufficient_quota"
}
};
- body = JsonSerializer.Serialize(openAiError, ProxyUtils.JsonSerializerOptions);
+ var body = JsonSerializer.Serialize(openAiError, ProxyUtils.JsonSerializerOptions);
+
+ var response = new HttpResponseMessage(HttpStatusCode.TooManyRequests)
+ {
+ Content = new StringContent(body, Encoding.UTF8, "application/json")
+ };
+
+ _ = response.Headers.TryAddWithoutValidation(Configuration.HeaderRetryAfter, ((int)(_resetTime - DateTime.Now).TotalSeconds).ToString(CultureInfo.InvariantCulture));
- headers.Add(new(Configuration.HeaderRetryAfter, ((int)(_resetTime - DateTime.Now).TotalSeconds).ToString(CultureInfo.InvariantCulture)));
- if (request.Headers.Any(h => h.Name.Equals("Origin", StringComparison.OrdinalIgnoreCase)))
+ if (request.Headers.TryGetValues("Origin", out var _))
{
- headers.Add(new("Access-Control-Allow-Origin", "*"));
- headers.Add(new("Access-Control-Expose-Headers", Configuration.HeaderRetryAfter));
+ _ = response.Headers.TryAddWithoutValidation("Access-Control-Allow-Origin", "*");
+ _ = response.Headers.TryAddWithoutValidation("Access-Control-Expose-Headers", Configuration.HeaderRetryAfter);
}
- e.Session.GenericResponse(body, HttpStatusCode.TooManyRequests, [.. headers.Select(h => new HttpHeader(h.Name, h.Value))]);
+ return response;
}
- private static string BuildThrottleKey(Request r) => r.RequestUri.Host;
+ private static string BuildThrottleKey(HttpRequestMessage r) => r.RequestUri?.Host ?? string.Empty;
}
diff --git a/DevProxy.Plugins/Behavior/RateLimitingPlugin.cs b/DevProxy.Plugins/Behavior/RateLimitingPlugin.cs
index 824a60d0..58875175 100644
--- a/DevProxy.Plugins/Behavior/RateLimitingPlugin.cs
+++ b/DevProxy.Plugins/Behavior/RateLimitingPlugin.cs
@@ -13,10 +13,9 @@
using Microsoft.Extensions.Logging;
using System.Globalization;
using System.Net;
+using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
-using Unobtanium.Web.Proxy.Http;
-using Unobtanium.Web.Proxy.Models;
namespace DevProxy.Plugins.Behavior;
@@ -53,7 +52,8 @@ public sealed class RateLimitingPlugin(
ILogger logger,
ISet urlsToWatch,
IProxyConfiguration proxyConfiguration,
- IConfigurationSection pluginConfigurationSection) :
+ IConfigurationSection pluginConfigurationSection,
+ IProxyStorage proxyStorage) :
BasePlugin(
httpClient,
logger,
@@ -61,8 +61,7 @@ public sealed class RateLimitingPlugin(
proxyConfiguration,
pluginConfigurationSection)
{
- // initial values so that we know when we intercept the
- // first request and can set the initial values
+ private readonly IProxyStorage _proxyStorage = proxyStorage;
private int _resourcesRemaining = -1;
private DateTime _resetTime = DateTime.MinValue;
private RateLimitingCustomResponseLoader? _loader;
@@ -83,23 +82,14 @@ public override async Task InitializeAsync(InitArgs e, CancellationToken cancell
}
}
- public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken cancellationToken)
+ public override Func>? OnRequestAsync => (args, cancellationToken) =>
{
- Logger.LogTrace("{Method} called", nameof(BeforeRequestAsync));
+ Logger.LogTrace("{Method} called", nameof(OnRequestAsync));
- ArgumentNullException.ThrowIfNull(e);
-
- var session = e.Session;
- var state = e.ResponseState;
- if (state.HasBeenSet)
- {
- Logger.LogRequest("Response already set", MessageType.Skipped, new(e.Session));
- return Task.CompletedTask;
- }
- if (!e.HasRequestUrlMatch(UrlsToWatch))
+ if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.Request.RequestUri))
{
- Logger.LogRequest("URL not matched", MessageType.Skipped, new(e.Session));
- return Task.CompletedTask;
+ Logger.LogRequest("URL not matched", MessageType.Skipped, args.Request);
+ return Task.FromResult(PluginResponse.Continue());
}
// set the initial values for the first request
@@ -124,106 +114,122 @@ public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken ca
if (_resourcesRemaining < 0)
{
_resourcesRemaining = 0;
- var request = e.Session.HttpClient.Request;
- Logger.LogRequest($"Exceeded resource limit when calling {request.Url}. Request will be throttled", MessageType.Failed, new(e.Session));
+ Logger.LogRequest($"Exceeded resource limit when calling {args.Request.RequestUri}. Request will be throttled", MessageType.Failed, args.Request);
if (Configuration.WhenLimitExceeded == RateLimitResponseWhenLimitExceeded.Throttle)
{
- if (!e.GlobalData.TryGetValue(RetryAfterPlugin.ThrottledRequestsKey, out var value))
+ // Add throttling info to global data for RetryAfterPlugin coordination
+ if (!_proxyStorage.GlobalData.TryGetValue(RetryAfterPlugin.ThrottledRequestsKey, out var value))
{
value = new List();
- e.GlobalData.Add(RetryAfterPlugin.ThrottledRequestsKey, value);
+ _proxyStorage.GlobalData.Add(RetryAfterPlugin.ThrottledRequestsKey, value);
}
var throttledRequests = value as List;
throttledRequests?.Add(new(
- BuildThrottleKey(request),
+ BuildThrottleKey(args.Request),
ShouldThrottle,
_resetTime
));
- ThrottleResponse(e);
- state.HasBeenSet = true;
+
+ return Task.FromResult(PluginResponse.Respond(BuildThrottleResponse(args.Request)));
}
else
{
if (Configuration.CustomResponse is not null)
{
- var headersList = Configuration.CustomResponse.Headers is not null ?
- Configuration.CustomResponse.Headers.Select(h => new HttpHeader(h.Name, h.Value)).ToList() :
- [];
-
- var retryAfterHeader = headersList.FirstOrDefault(h => h.Name.Equals(Configuration.HeaderRetryAfter, StringComparison.OrdinalIgnoreCase));
- if (retryAfterHeader is not null && retryAfterHeader.Value == "@dynamic")
- {
- headersList.Add(new(Configuration.HeaderRetryAfter, ((int)(_resetTime - DateTime.Now).TotalSeconds).ToString(CultureInfo.InvariantCulture)));
- _ = headersList.Remove(retryAfterHeader);
- }
-
- var headers = headersList.ToArray();
-
- // allow custom throttling response
var responseCode = (HttpStatusCode)(Configuration.CustomResponse.StatusCode ?? 200);
+
+ // Add throttling info for TooManyRequests responses
if (responseCode == HttpStatusCode.TooManyRequests)
{
- if (!e.GlobalData.TryGetValue(RetryAfterPlugin.ThrottledRequestsKey, out var value))
+ if (!_proxyStorage.GlobalData.TryGetValue(RetryAfterPlugin.ThrottledRequestsKey, out var value))
{
value = new List();
- e.GlobalData.Add(RetryAfterPlugin.ThrottledRequestsKey, value);
+ _proxyStorage.GlobalData.Add(RetryAfterPlugin.ThrottledRequestsKey, value);
}
var throttledRequests = value as List;
throttledRequests?.Add(new(
- BuildThrottleKey(request),
+ BuildThrottleKey(args.Request),
ShouldThrottle,
_resetTime
));
}
- string body = Configuration.CustomResponse.Body is not null ?
- JsonSerializer.Serialize(Configuration.CustomResponse.Body, ProxyUtils.JsonSerializerOptions) :
- "";
- e.Session.GenericResponse(body, responseCode, headers);
- state.HasBeenSet = true;
+ var response = new HttpResponseMessage(responseCode)
+ {
+ Content = new StringContent(
+ Configuration.CustomResponse.Body is not null ?
+ JsonSerializer.Serialize(Configuration.CustomResponse.Body, ProxyUtils.JsonSerializerOptions) :
+ string.Empty,
+ Encoding.UTF8,
+ "application/json")
+ };
+
+ // Add headers
+ if (Configuration.CustomResponse.Headers is not null)
+ {
+ foreach (var header in Configuration.CustomResponse.Headers)
+ {
+ var headerValue = header.Value;
+ if (header.Name.Equals(Configuration.HeaderRetryAfter, StringComparison.OrdinalIgnoreCase) && headerValue == "@dynamic")
+ {
+ headerValue = ((int)(_resetTime - DateTime.Now).TotalSeconds).ToString(CultureInfo.InvariantCulture);
+ }
+ _ = response.Headers.TryAddWithoutValidation(header.Name, headerValue);
+ }
+ }
+
+ return Task.FromResult(PluginResponse.Respond(response));
}
else
{
- Logger.LogRequest($"Custom behavior not set. {Configuration.CustomResponseFile} not found.", MessageType.Failed, new(e.Session));
+ Logger.LogRequest($"Custom behavior not set. {Configuration.CustomResponseFile} not found.", MessageType.Failed, args.Request);
+ var response = new HttpResponseMessage(HttpStatusCode.InternalServerError)
+ {
+ Content = new StringContent("Custom response file not found.", Encoding.UTF8, "text/plain")
+ };
+ return Task.FromResult(PluginResponse.Respond(response));
}
}
}
else
{
- Logger.LogRequest($"Resources remaining: {_resourcesRemaining}", MessageType.Skipped, new(e.Session));
+ Logger.LogRequest($"Resources remaining: {_resourcesRemaining}", MessageType.Skipped, args.Request);
}
- StoreRateLimitingHeaders(e);
- return Task.CompletedTask;
- }
+ StoreRateLimitingHeaders(args);
+ return Task.FromResult(PluginResponse.Continue());
+ };
- public override Task BeforeResponseAsync(ProxyResponseArgs e, CancellationToken cancellationToken)
+ public override Func>? OnResponseAsync => (args, cancellationToken) =>
{
- Logger.LogTrace("{Method} called", nameof(BeforeResponseAsync));
+ Logger.LogTrace("{Method} called", nameof(OnResponseAsync));
- ArgumentNullException.ThrowIfNull(e);
-
- if (!e.HasRequestUrlMatch(UrlsToWatch))
+ if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.Request.RequestUri))
{
- Logger.LogRequest("URL not matched", MessageType.Skipped, new(e.Session));
- return Task.CompletedTask;
+ Logger.LogRequest("URL not matched", MessageType.Skipped, args.Request);
+ return Task.FromResult(null);
}
- if (e.ResponseState.HasBeenSet)
+
+ // Add rate limiting headers to the response if we have them stored
+ var requestData = _proxyStorage.GetRequestData(args.RequestId);
+ if (requestData.TryGetValue(Name, out var pluginData) &&
+ pluginData is List rateLimitingHeaders)
{
- Logger.LogRequest("Response already set", MessageType.Skipped, new(e.Session));
- return Task.CompletedTask;
+ var response = args.Response;
+ foreach (var header in rateLimitingHeaders)
+ {
+ _ = response.Headers.TryAddWithoutValidation(header.Name, header.Value);
+ }
}
- UpdateProxyResponse(e, HttpStatusCode.OK);
+ Logger.LogTrace("Left {Name}", nameof(OnResponseAsync));
+ return Task.FromResult(null);
+ };
- Logger.LogTrace("Left {Name}", nameof(BeforeResponseAsync));
- return Task.CompletedTask;
- }
-
- private ThrottlingInfo ShouldThrottle(Request request, string throttlingKey)
+ private ThrottlingInfo ShouldThrottle(HttpRequestMessage request, string throttlingKey)
{
var throttleKeyForRequest = BuildThrottleKey(request);
return new(throttleKeyForRequest == throttlingKey ?
@@ -231,62 +237,54 @@ private ThrottlingInfo ShouldThrottle(Request request, string throttlingKey)
Configuration.HeaderRetryAfter);
}
- private void ThrottleResponse(ProxyRequestArgs e) => UpdateProxyResponse(e, HttpStatusCode.TooManyRequests);
-
- private void UpdateProxyResponse(ProxyHttpEventArgsBase e, HttpStatusCode errorStatus)
+ private HttpResponseMessage BuildThrottleResponse(HttpRequestMessage request)
{
var headers = new List();
var body = string.Empty;
- var request = e.Session.HttpClient.Request;
- var response = e.Session.HttpClient.Response;
// resources exceeded
- if (errorStatus == HttpStatusCode.TooManyRequests)
+ if (ProxyUtils.IsGraphRequest(request))
{
- if (ProxyUtils.IsGraphRequest(request))
- {
- var requestId = Guid.NewGuid().ToString();
- var requestDate = DateTime.Now.ToString(CultureInfo.CurrentCulture);
- headers.AddRange(ProxyUtils.BuildGraphResponseHeaders(request, requestId, requestDate));
+ var requestId = Guid.NewGuid().ToString();
+ var requestDate = DateTime.Now.ToString(CultureInfo.CurrentCulture);
+ headers.AddRange(ProxyUtils.BuildGraphResponseHeaders(request, requestId, requestDate));
- body = JsonSerializer.Serialize(new GraphErrorResponseBody(
- new()
+ body = JsonSerializer.Serialize(new GraphErrorResponseBody(
+ new()
+ {
+ Code = new Regex("([A-Z])").Replace(HttpStatusCode.TooManyRequests.ToString(), m => { return $" {m.Groups[1]}"; }).Trim(),
+ Message = BuildApiErrorMessage(request),
+ InnerError = new()
{
- Code = new Regex("([A-Z])").Replace(errorStatus.ToString(), m => { return $" {m.Groups[1]}"; }).Trim(),
- Message = BuildApiErrorMessage(request),
- InnerError = new()
- {
- RequestId = requestId,
- Date = requestDate
- }
- }),
- ProxyUtils.JsonSerializerOptions
- );
- }
-
- headers.Add(new(Configuration.HeaderRetryAfter, ((int)(_resetTime - DateTime.Now).TotalSeconds).ToString(CultureInfo.InvariantCulture)));
- if (request.Headers.Any(h => h.Name.Equals("Origin", StringComparison.OrdinalIgnoreCase)))
- {
- headers.Add(new("Access-Control-Allow-Origin", "*"));
- headers.Add(new("Access-Control-Expose-Headers", Configuration.HeaderRetryAfter));
- }
+ RequestId = requestId,
+ Date = requestDate
+ }
+ }),
+ ProxyUtils.JsonSerializerOptions
+ );
+ }
- e.Session.GenericResponse(body ?? string.Empty, errorStatus, [.. headers.Select(h => new HttpHeader(h.Name, h.Value))]);
- return;
+ headers.Add(new(Configuration.HeaderRetryAfter, ((int)(_resetTime - DateTime.Now).TotalSeconds).ToString(CultureInfo.InvariantCulture)));
+ if (request.Headers.TryGetValues("Origin", out var _))
+ {
+ headers.Add(new("Access-Control-Allow-Origin", "*"));
+ headers.Add(new("Access-Control-Expose-Headers", Configuration.HeaderRetryAfter));
}
- if (e.SessionData.TryGetValue(Name, out var pluginData) &&
- pluginData is List rateLimitingHeaders)
+ var response = new HttpResponseMessage(HttpStatusCode.TooManyRequests)
+ {
+ Content = new StringContent(body ?? string.Empty, Encoding.UTF8, "application/json")
+ };
+
+ foreach (var header in headers)
{
- ProxyUtils.MergeHeaders(headers, rateLimitingHeaders);
+ _ = response.Headers.TryAddWithoutValidation(header.Name, header.Value);
}
- // add headers to the original API response, avoiding duplicates
- headers.ForEach(h => e.Session.HttpClient.Response.Headers.RemoveHeader(h.Name));
- e.Session.HttpClient.Response.Headers.AddHeaders(headers.Select(h => new HttpHeader(h.Name, h.Value)).ToArray());
+ return response;
}
- private void StoreRateLimitingHeaders(ProxyRequestArgs e)
+ private void StoreRateLimitingHeaders(RequestArguments args)
{
// add rate limiting headers if reached the threshold percentage
if (_resourcesRemaining > Configuration.RateLimit - (Configuration.RateLimit * Configuration.WarningThresholdPercent / 100))
@@ -305,15 +303,15 @@ private void StoreRateLimitingHeaders(ProxyRequestArgs e)
new(Configuration.HeaderReset, reset)
]);
- ExposeRateLimitingForCors(headers, e);
+ ExposeRateLimitingForCors(headers, args.Request);
- e.SessionData.Add(Name, headers);
+ var requestData = _proxyStorage.GetRequestData(args.RequestId);
+ requestData.Add(Name, headers);
}
- private void ExposeRateLimitingForCors(List headers, ProxyRequestArgs e)
+ private void ExposeRateLimitingForCors(List headers, HttpRequestMessage request)
{
- var request = e.Session.HttpClient.Request;
- if (request.Headers.FirstOrDefault((h) => h.Name.Equals("Origin", StringComparison.OrdinalIgnoreCase)) is null)
+ if (!request.Headers.TryGetValues("Origin", out var _))
{
return;
}
@@ -322,9 +320,9 @@ private void ExposeRateLimitingForCors(List headers, ProxyRe
headers.Add(new("Access-Control-Expose-Headers", $"{Configuration.HeaderLimit}, {Configuration.HeaderRemaining}, {Configuration.HeaderReset}, {Configuration.HeaderRetryAfter}"));
}
- private static string BuildApiErrorMessage(Request r) => $"Some error was generated by the proxy. {(ProxyUtils.IsGraphRequest(r) ? ProxyUtils.IsSdkRequest(r) ? "" : string.Join(' ', MessageUtils.BuildUseSdkForErrorsMessage()) : "")}";
+ private static string BuildApiErrorMessage(HttpRequestMessage r) => $"Some error was generated by the proxy. {(ProxyUtils.IsGraphRequest(r) ? ProxyUtils.IsSdkRequest(r) ? "" : string.Join(' ', MessageUtils.BuildUseSdkForErrorsMessage()) : "")}";
- private static string BuildThrottleKey(Request r)
+ private static string BuildThrottleKey(HttpRequestMessage r)
{
if (ProxyUtils.IsGraphRequest(r))
{
@@ -332,7 +330,7 @@ private static string BuildThrottleKey(Request r)
}
else
{
- return r.RequestUri.Host;
+ return r.RequestUri?.Host ?? string.Empty;
}
}
}
diff --git a/DevProxy.Plugins/migration.md b/DevProxy.Plugins/migration.md
index 545d2052..d60c08b1 100644
--- a/DevProxy.Plugins/migration.md
+++ b/DevProxy.Plugins/migration.md
@@ -203,8 +203,8 @@ var responseBody = response.BodyString;
**After (Response):**
```csharp
-var request = args.HttpRequestMessage;
-var response = args.HttpResponseMessage;
+var request = args.Request;
+var response = args.Response;
var statusCode = response.StatusCode;
var responseBody = await response.Content.ReadAsStringAsync();
```
@@ -231,9 +231,9 @@ if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.Request.RequestUri))
**After (OnRequestLogAsync/OnResponseLogAsync):**
```csharp
-if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.HttpRequestMessage.RequestUri))
+if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.Request.RequestUri))
{
- Logger.LogRequest("URL not matched", MessageType.Skipped, args.HttpRequestMessage);
+ Logger.LogRequest("URL not matched", MessageType.Skipped, args.Request);
return;
}
```
@@ -443,15 +443,15 @@ public override Task AfterResponseAsync(ProxyResponseArgs e, CancellationToken c
```csharp
public override Func? OnResponseLogAsync => (args, cancellationToken) =>
{
- if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.HttpRequestMessage.RequestUri))
+ if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, args.Request.RequestUri))
{
- Logger.LogRequest("URL not matched", MessageType.Skipped, args.HttpRequestMessage);
+ Logger.LogRequest("URL not matched", MessageType.Skipped, args.Request);
return Task.CompletedTask;
}
if (ShouldProvideGuidance(args.HttpResponseMessage))
{
- Logger.LogRequest("Consider optimizing your API queries", MessageType.Tip, args.HttpRequestMessage);
+ Logger.LogRequest("Consider optimizing your API queries", MessageType.Tip, args.Request);
}
return Task.CompletedTask;
@@ -539,7 +539,7 @@ Logger.LogRequest("Message", MessageType.Info, new LoggingContext(e.Session));
Logger.LogRequest("Message", MessageType.Info, args.Request);
// New (Response-based methods)
-Logger.LogRequest("Message", MessageType.Info, args.HttpRequestMessage);
+Logger.LogRequest("Message", MessageType.Info, args.Request);
```
### 2. Global Data and Session Data
@@ -675,11 +675,11 @@ public override Func>?
- [ ] Update method signature from `AfterResponseAsync` to `OnResponseLogAsync`
- [ ] Keep return type as `Task` (no PluginResponse needed)
- [ ] Update input parameter from `ProxyResponseArgs` to `ResponseArguments`
-- [ ] Replace `e.Session.HttpClient.Request` with `args.HttpRequestMessage`
-- [ ] Replace `e.Session.HttpClient.Response` with `args.HttpResponseMessage`
+- [ ] Replace `e.Session.HttpClient.Request` with `args.Request`
+- [ ] Replace `e.Session.HttpClient.Response` with `args.Response`
- [ ] Replace `e.HasRequestUrlMatch()` with `ProxyUtils.MatchesUrlToWatch()`
- [ ] Remove any response modification logic (not allowed in OnResponseLogAsync)
-- [ ] Update logging context from `LoggingContext(e.Session)` to `args.HttpRequestMessage`
+- [ ] Update logging context from `LoggingContext(e.Session)` to `args.Request`
- [ ] Add `IProxyStorage` to constructor if plugin needs global or request-specific data storage
- [ ] Test the migrated plugin thoroughly
@@ -687,12 +687,12 @@ public override Func>?
- [ ] Update method signature from `BeforeResponseAsync` to `OnResponseAsync`
- [ ] Change return type from `Task` to `Task`
- [ ] Update input parameter from `ProxyResponseArgs` to `ResponseArguments`
-- [ ] Replace `e.Session.HttpClient.Request` with `args.HttpRequestMessage`
-- [ ] Replace `e.Session.HttpClient.Response` with `args.HttpResponseMessage`
+- [ ] Replace `e.Session.HttpClient.Request` with `args.REquest`
+- [ ] Replace `e.Session.HttpClient.Response` with `args.Response`
- [ ] Replace `e.HasRequestUrlMatch()` with `ProxyUtils.MatchesUrlToWatch()`
- [ ] Remove `e.ResponseState.HasBeenSet` checks
- [ ] Return `null` to continue or `PluginResponse` to modify
-- [ ] Update logging context from `LoggingContext(e.Session)` to `args.HttpRequestMessage`
+- [ ] Update logging context from `LoggingContext(e.Session)` to `args.Request`
- [ ] Add `IProxyStorage` to constructor if plugin needs global or request-specific data storage
- [ ] Test the migrated plugin thoroughly
From e4ad26fadcaff7e7419448eeeb6c234b4a68aa87 Mon Sep 17 00:00:00 2001
From: Stephan van Rooij <1292510+svrooij@users.noreply.github.com>
Date: Wed, 20 Aug 2025 11:08:29 +0200
Subject: [PATCH 08/14] Reporters fixed
---
DevProxy.Abstractions/Plugins/BasePlugin.cs | 4 +++-
DevProxy.Abstractions/Plugins/IPlugin.cs | 22 ++++++++++++++++++-
DevProxy.Abstractions/Proxy/ProxyEvents.cs | 2 +-
DevProxy.Plugins/Reporters/BaseReporter.cs | 5 +++--
DevProxy.Plugins/Reporters/JsonReporter.cs | 3 ++-
.../Reporters/MarkdownReporter.cs | 3 ++-
.../Reporters/PlainTextReporter.cs | 3 ++-
7 files changed, 34 insertions(+), 8 deletions(-)
diff --git a/DevProxy.Abstractions/Plugins/BasePlugin.cs b/DevProxy.Abstractions/Plugins/BasePlugin.cs
index 3c09e224..8bddf2ab 100644
--- a/DevProxy.Abstractions/Plugins/BasePlugin.cs
+++ b/DevProxy.Abstractions/Plugins/BasePlugin.cs
@@ -10,7 +10,6 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
-using Unobtanium.Web.Proxy.Events;
namespace DevProxy.Abstractions.Plugins;
@@ -27,16 +26,19 @@ public abstract class BasePlugin(
///
/// Implement this to handle requests, if you won't be modifying requests or respond, use .
///
+ /// This is by default, so we can filter plugins based on implementation.
public virtual Func>? OnRequestAsync { get; }
///
/// Implement this to log requests, you cannot modify the request or response here.
///
+ /// This is by default, so we can filter plugins based on implementation.
public virtual Func? OnRequestLogAsync { get; }
///
/// Implement this to modify responses from the remote server.
///
+ /// This is by default, so we can filter plugins based on implementation.
public virtual Func>? OnResponseAsync { get; }
///
diff --git a/DevProxy.Abstractions/Plugins/IPlugin.cs b/DevProxy.Abstractions/Plugins/IPlugin.cs
index 2757e82b..62d48431 100644
--- a/DevProxy.Abstractions/Plugins/IPlugin.cs
+++ b/DevProxy.Abstractions/Plugins/IPlugin.cs
@@ -23,26 +23,46 @@ public interface IPlugin
///
/// Implement this to handle requests.
///
+ /// This is by default, so we can filter plugins based on implementation.
Func>? OnRequestAsync { get; }
///
/// Implement this to log requests, you cannot modify the request or response here.
///
+ /// This is by default, so we can filter plugins based on implementation.
Func? OnRequestLogAsync { get; }
///
/// Implement this to modify responses from the remote server.
///
+ /// This is by default, so we can filter plugins based on implementation.
Func>? OnResponseAsync { get; }
///
- /// Implement this to modify responses from the remote server.
+ /// Implement this to log responses from the remote server.
///
+ /// Think caching after the fact, combined with . This is by default, so we can filter plugins based on implementation.
Func? OnResponseLogAsync { get; }
+
Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken cancellationToken);
Task BeforeResponseAsync(ProxyResponseArgs e, CancellationToken cancellationToken);
Task AfterResponseAsync(ProxyResponseArgs e, CancellationToken cancellationToken);
+
+ ///
+ /// Receiving RequestLog messages for each call.
+ ///
+ /// This is for collecting log messages not requests itself
+ ///
+ ///
+ ///
Task AfterRequestLogAsync(RequestLogArgs e, CancellationToken cancellationToken);
+
+ ///
+ /// Executes post-processing tasks after a recording has stopped.
+ ///
+ /// The arguments containing details about the recording that has stopped.
+ /// A token to monitor for cancellation requests.
+ /// A task that represents the asynchronous operation.
Task AfterRecordingStopAsync(RecordingArgs e, CancellationToken cancellationToken);
Task MockRequestAsync(EventArgs e, CancellationToken cancellationToken);
}
diff --git a/DevProxy.Abstractions/Proxy/ProxyEvents.cs b/DevProxy.Abstractions/Proxy/ProxyEvents.cs
index 35076ec6..7c190deb 100644
--- a/DevProxy.Abstractions/Proxy/ProxyEvents.cs
+++ b/DevProxy.Abstractions/Proxy/ProxyEvents.cs
@@ -98,7 +98,7 @@ private RequestLog(string message, MessageType messageType, string? method, stri
//}
}
-public class RecordingArgs(IEnumerable requestLogs) : ProxyEventArgsBase
+public class RecordingArgs(IEnumerable requestLogs)// : ProxyEventArgsBase
{
public IEnumerable RequestLogs { get; set; } = requestLogs ??
throw new ArgumentNullException(nameof(requestLogs));
diff --git a/DevProxy.Plugins/Reporters/BaseReporter.cs b/DevProxy.Plugins/Reporters/BaseReporter.cs
index 84fc5866..ab540c0b 100644
--- a/DevProxy.Plugins/Reporters/BaseReporter.cs
+++ b/DevProxy.Plugins/Reporters/BaseReporter.cs
@@ -11,7 +11,8 @@ namespace DevProxy.Plugins.Reporters;
public abstract class BaseReporter(
ILogger logger,
- ISet urlsToWatch) : BasePlugin(logger, urlsToWatch)
+ ISet urlsToWatch,
+ IProxyStorage proxyStorage) : BasePlugin(logger, urlsToWatch)
{
public abstract string FileExtension { get; }
@@ -23,7 +24,7 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation
await base.AfterRecordingStopAsync(e, cancellationToken);
- if (!e.GlobalData.TryGetValue(ProxyUtils.ReportsKey, out var value) ||
+ if (!proxyStorage.GlobalData.TryGetValue(ProxyUtils.ReportsKey, out var value) ||
value is not Dictionary reports ||
reports.Count == 0)
{
diff --git a/DevProxy.Plugins/Reporters/JsonReporter.cs b/DevProxy.Plugins/Reporters/JsonReporter.cs
index 8bc79b30..4539b158 100644
--- a/DevProxy.Plugins/Reporters/JsonReporter.cs
+++ b/DevProxy.Plugins/Reporters/JsonReporter.cs
@@ -12,7 +12,8 @@ namespace DevProxy.Plugins.Reporters;
public class JsonReporter(
ILogger logger,
- ISet urlsToWatch) : BaseReporter(logger, urlsToWatch)
+ ISet urlsToWatch,
+ IProxyStorage proxyStorage) : BaseReporter(logger, urlsToWatch, proxyStorage)
{
private string _fileExtension = ".json";
diff --git a/DevProxy.Plugins/Reporters/MarkdownReporter.cs b/DevProxy.Plugins/Reporters/MarkdownReporter.cs
index fb9e9069..6f886963 100644
--- a/DevProxy.Plugins/Reporters/MarkdownReporter.cs
+++ b/DevProxy.Plugins/Reporters/MarkdownReporter.cs
@@ -10,7 +10,8 @@ namespace DevProxy.Plugins.Reporters;
public class MarkdownReporter(
ILogger logger,
- ISet urlsToWatch) : BaseReporter(logger, urlsToWatch)
+ ISet urlsToWatch,
+ IProxyStorage proxyStorage) : BaseReporter(logger, urlsToWatch, proxyStorage)
{
public override string Name => nameof(MarkdownReporter);
public override string FileExtension => ".md";
diff --git a/DevProxy.Plugins/Reporters/PlainTextReporter.cs b/DevProxy.Plugins/Reporters/PlainTextReporter.cs
index d5d51681..6c206ee9 100644
--- a/DevProxy.Plugins/Reporters/PlainTextReporter.cs
+++ b/DevProxy.Plugins/Reporters/PlainTextReporter.cs
@@ -10,7 +10,8 @@ namespace DevProxy.Plugins.Reporters;
public class PlainTextReporter(
ILogger logger,
- ISet urlsToWatch) : BaseReporter(logger, urlsToWatch)
+ ISet urlsToWatch,
+ IProxyStorage proxyStorage) : BaseReporter(logger, urlsToWatch, proxyStorage)
{
public override string Name => nameof(PlainTextReporter);
public override string FileExtension => ".txt";
From 754480703bef4883da534f38582efa489708f50d Mon Sep 17 00:00:00 2001
From: Stephan van Rooij <1292510+svrooij@users.noreply.github.com>
Date: Wed, 20 Aug 2025 12:55:36 +0200
Subject: [PATCH 09/14] Almost there
---
.../Plugins/BaseReportingPlugin.cs | 18 +-
DevProxy.Abstractions/Proxy/ProxyEvents.cs | 7 +-
DevProxy.Plugins/Behavior/RetryAfterPlugin.cs | 88 +++---
.../Generation/MockGeneratorPlugin.cs | 39 +--
.../Generation/OpenApiSpecGeneratorPlugin.cs | 117 ++++----
DevProxy.Plugins/Mocking/AuthPlugin.cs | 156 +++++------
DevProxy.Plugins/Mocking/CrudApiPlugin.cs | 261 +++++++++---------
.../Mocking/MockResponsePlugin.cs | 163 ++++++-----
.../Reporting/UrlDiscoveryPlugin.cs | 9 +-
9 files changed, 450 insertions(+), 408 deletions(-)
diff --git a/DevProxy.Abstractions/Plugins/BaseReportingPlugin.cs b/DevProxy.Abstractions/Plugins/BaseReportingPlugin.cs
index 3439e13f..4b3006a5 100644
--- a/DevProxy.Abstractions/Plugins/BaseReportingPlugin.cs
+++ b/DevProxy.Abstractions/Plugins/BaseReportingPlugin.cs
@@ -11,18 +11,19 @@ namespace DevProxy.Abstractions.Plugins;
public abstract class BaseReportingPlugin(
ILogger logger,
- ISet urlsToWatch) : BasePlugin(logger, urlsToWatch)
+ ISet urlsToWatch,
+ IProxyStorage proxyStorage) : BasePlugin(logger, urlsToWatch)
{
- protected virtual void StoreReport(object report, ProxyEventArgsBase e)
+ protected IProxyStorage ProxyStorage => proxyStorage;
+ protected virtual void StoreReport(object report)
{
- ArgumentNullException.ThrowIfNull(e);
if (report is null)
{
return;
}
- ((Dictionary)e.GlobalData[ProxyUtils.ReportsKey])[Name] = report;
+ ((Dictionary)ProxyStorage.GlobalData[ProxyUtils.ReportsKey])[Name] = report;
}
}
@@ -31,7 +32,8 @@ public abstract class BaseReportingPlugin(
ILogger logger,
ISet urlsToWatch,
IProxyConfiguration proxyConfiguration,
- IConfigurationSection configurationSection) :
+ IConfigurationSection configurationSection,
+ IProxyStorage proxyStorage) :
BasePlugin(
httpClient,
logger,
@@ -39,15 +41,15 @@ public abstract class BaseReportingPlugin(
proxyConfiguration,
configurationSection) where TConfiguration : new()
{
- protected virtual void StoreReport(object report, ProxyEventArgsBase e)
+ protected IProxyStorage ProxyStorage => proxyStorage;
+ protected virtual void StoreReport(object report)
{
- ArgumentNullException.ThrowIfNull(e);
if (report is null)
{
return;
}
- ((Dictionary)e.GlobalData[ProxyUtils.ReportsKey])[Name] = report;
+ ((Dictionary)ProxyStorage.GlobalData[ProxyUtils.ReportsKey])[Name] = report;
}
}
diff --git a/DevProxy.Abstractions/Proxy/ProxyEvents.cs b/DevProxy.Abstractions/Proxy/ProxyEvents.cs
index 7c190deb..410f0b01 100644
--- a/DevProxy.Abstractions/Proxy/ProxyEvents.cs
+++ b/DevProxy.Abstractions/Proxy/ProxyEvents.cs
@@ -57,6 +57,10 @@ public class RequestLog
//public LoggingContext? Context { get; set; }
[JsonIgnore]
public HttpRequestMessage? Request { get; internal set; }
+
+ [JsonIgnore]
+ public HttpResponseMessage? Response { get; internal set; }
+
public string Message { get; set; }
public MessageType MessageType { get; set; }
public string? Method { get; init; }
@@ -68,10 +72,11 @@ public RequestLog(string message, MessageType messageType, object? context)
throw new NotImplementedException("This constructor is not implemented. Use the other constructors instead.");
}
- public RequestLog(string message, MessageType messageType, HttpRequestMessage requestMessage) :
+ public RequestLog(string message, MessageType messageType, HttpRequestMessage requestMessage, HttpResponseMessage? responseMessage = null) :
this(message, messageType, requestMessage?.Method.Method, requestMessage?.RequestUri!.AbsoluteUri, _: null)
{
Request = requestMessage;
+ Response = responseMessage;
}
public RequestLog(string message, MessageType messageType, string method, string url) :
diff --git a/DevProxy.Plugins/Behavior/RetryAfterPlugin.cs b/DevProxy.Plugins/Behavior/RetryAfterPlugin.cs
index 56f0562e..52f146c9 100644
--- a/DevProxy.Plugins/Behavior/RetryAfterPlugin.cs
+++ b/DevProxy.Plugins/Behavior/RetryAfterPlugin.cs
@@ -11,62 +11,60 @@
using Microsoft.Extensions.Logging;
using System.Globalization;
using System.Net;
+using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
-using Unobtanium.Web.Proxy.Http;
-using Unobtanium.Web.Proxy.Models;
namespace DevProxy.Plugins.Behavior;
public sealed class RetryAfterPlugin(
ILogger