diff --git a/README.md b/README.md index 6caa1a6c..b93106ab 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ By leveraging a self-hosted architecture on Azure Container Apps, organizations ## Capabilities: ### Security -- ** Virtual Network Injection:** Secure mission-critical workloads with native **VNET Integration** and identity-based access through [Microsoft Entra ID (Managed Identity)](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) across public and sovereign regions. +- **Virtual Network Injection:** Secure mission-critical workloads with native **VNET Integration** and identity-based access through [Microsoft Entra ID (Managed Identity)](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) across public and sovereign regions. - **Identity-Driven Edge Security:** Enforce **Zero Trust** principles with integrated **OAuth2 authentication** and customizable **Header Policy Enforcement** to validate or restrict inbound request structures. - **Dynamic Access Governance:** Centrally manage user and group permissions via **External Configuration Providers**, enabling real-time suspension or restriction of access without code changes. diff --git a/ReleaseNotes/version2.2.md b/ReleaseNotes/version2.2.md index 0154eda6..662cb659 100644 --- a/ReleaseNotes/version2.2.md +++ b/ReleaseNotes/version2.2.md @@ -1,5 +1,7 @@ # Release Notes # +## 2.2.9 + Proxy: * Added CheckingBackgroundRequestStatus and BackgroundRequestSubmitted as a status for background requests * Fix bugs related to background request lifecycle @@ -27,12 +29,24 @@ Proxy: * Add streamlined probe server with ability to run as side car * Reworked ProxyWroker to match the response flow from the 2.1.x release * Track HTTP response in proxy data and dispose after delivery to client +* Updated to dotnet 10 and latest stable libs +* Updated Dockerimage for dotnet 10 build Policy: * Update backend logs for better readability * Created a V2 policy for readability improvements * Added section to return 408 on timeout, 429 on concurrencyLimits, and on 400 return response body +## 2.2.8.p1 + +Proxy: +* Added CheckingBackgroundRequestStatus and BackgroundRequestSubmitted as a status for background requests +* Fix bugs related to background request lifecycle + +Policy: +* Update backend logs for better readability +* Created a V2 policy for readability improvements + ## 2.2.8 Policy: diff --git a/src/SimpleL7Proxy/Backend/Backends.cs b/src/SimpleL7Proxy/Backend/Backends.cs index a18f0958..f2a2d957 100644 --- a/src/SimpleL7Proxy/Backend/Backends.cs +++ b/src/SimpleL7Proxy/Backend/Backends.cs @@ -32,7 +32,6 @@ public class Backends : IBackendService private static double _successRate; private static DateTime _lastStatusDisplay = DateTime.Now - TimeSpan.FromMinutes(10); // Force display on first run private static DateTime _lastGCTime = DateTime.Now; - private static bool _isRunning = false; private readonly ICircuitBreaker _circuitBreaker; private CancellationTokenSource _cancellationTokenSource; @@ -308,7 +307,6 @@ private async Task GetHostStatus(BaseHostHealth host, HttpClient client) using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, _cancellationToken); _probeEvent["Code"] = response.StatusCode.ToString(); - _isRunning = true; // CRITICAL: Drain the response body to allow connection reuse // Without this, undrained responses accumulate and leak memory diff --git a/src/SimpleL7Proxy/Config/BackendHostConfigurationExtensions.cs b/src/SimpleL7Proxy/Config/BackendHostConfigurationExtensions.cs index 1e7f76b2..6b794842 100644 --- a/src/SimpleL7Proxy/Config/BackendHostConfigurationExtensions.cs +++ b/src/SimpleL7Proxy/Config/BackendHostConfigurationExtensions.cs @@ -629,7 +629,6 @@ private static BackendOptions LoadBackendOptions() UseOAuthGov = ReadEnvironmentVariableOrDefault("UseOAuthGov", false), UseProfiles = ReadEnvironmentVariableOrDefault("UseProfiles", false), UserConfigUrl = ReadEnvironmentVariableOrDefault("UserConfigUrl", "file:config.json"), - UserConfigRequired = ReadEnvironmentVariableOrDefault("UserConfigRequired", false), UserIDFieldName = ReadEnvironmentVariableOrDefault("LookupHeaderName", "UserIDFieldName", "userId"), // migrate from LookupHeaderName UserPriorityThreshold = ReadEnvironmentVariableOrDefault("UserPriorityThreshold", 0.1f), UserProfileHeader = ReadEnvironmentVariableOrDefault("UserProfileHeader", "X-UserProfile"), diff --git a/src/SimpleL7Proxy/Config/BackendOptions.cs b/src/SimpleL7Proxy/Config/BackendOptions.cs index 9a45c10d..33e56f0d 100644 --- a/src/SimpleL7Proxy/Config/BackendOptions.cs +++ b/src/SimpleL7Proxy/Config/BackendOptions.cs @@ -76,7 +76,6 @@ public class BackendOptions public bool UseProfiles { get; set; } = false; public string UserProfileHeader { get; set; } = ""; public string UserConfigUrl { get; set; } = ""; - public bool UserConfigRequired { get; set; } = false; public float UserPriorityThreshold { get; set; } public Dictionary ValidateHeaders { get; set; } = []; public bool ValidateAuthAppID { get; set; } = false; diff --git a/src/SimpleL7Proxy/Constants.cs b/src/SimpleL7Proxy/Constants.cs index 1c7fcb89..626b5275 100644 --- a/src/SimpleL7Proxy/Constants.cs +++ b/src/SimpleL7Proxy/Constants.cs @@ -11,7 +11,7 @@ public static class Constants public const string RoundRobin = "roundrobin"; public const string Random = "random"; public const string Server = "simplel7proxy"; - public const string VERSION = "2.3.8.dnet10.3"; + public const string VERSION = "2.2.9"; public const int AnyPriority = -1; diff --git a/src/SimpleL7Proxy/Dockerfile b/src/SimpleL7Proxy/Dockerfile index 8ded3f80..e73c2f4a 100644 --- a/src/SimpleL7Proxy/Dockerfile +++ b/src/SimpleL7Proxy/Dockerfile @@ -1,24 +1,29 @@ # Use the official image as a parent image. -FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build-env +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build-env # Set the working directory. WORKDIR /app -# Copy csproj and restore dependencies -COPY *.csproj ./ -RUN dotnet restore +# Copy the Shared project first +COPY Shared/Shared.csproj ./Shared/ +# Copy SimpleL7Proxy project file +COPY SimpleL7Proxy/SimpleL7Proxy.csproj ./SimpleL7Proxy/ -# Copy the rest of the working directory contents into the container at /app. -COPY . ./ +# Restore dependencies for SimpleL7Proxy (which will also restore Shared) +WORKDIR /app/SimpleL7Proxy +RUN dotnet restore -# Ensure /app/out is a directory -RUN rm -f /app/out && mkdir -p /app/out +# Copy the rest of the source code +WORKDIR /app +COPY Shared/ ./Shared/ +COPY SimpleL7Proxy/ ./SimpleL7Proxy/ -# Build the application. -RUN dotnet publish -c Release -o out +# Build the application from SimpleL7Proxy directory +WORKDIR /app/SimpleL7Proxy +RUN dotnet publish -c Release -o /app/out # Use ASP.NET Core runtime image -FROM mcr.microsoft.com/dotnet/runtime:9.0 +FROM mcr.microsoft.com/dotnet/aspnet:10.0 # Set the working directory. WORKDIR /app @@ -26,8 +31,13 @@ WORKDIR /app # Copy the build output from the build image COPY --from=build-env /app/out . +# Copy config.json to the working directory +COPY SimpleL7Proxy/config.json . +COPY SimpleL7Proxy/auth.json . + # Expose port 443 for the application. EXPOSE 443 +EXPOSE 9000 # Define the command to run your app using `dotnet` ENTRYPOINT ["dotnet", "SimpleL7Proxy.dll", "--urls", "http://*:443"] \ No newline at end of file diff --git a/src/SimpleL7Proxy/Events/ProxyEvent.cs b/src/SimpleL7Proxy/Events/ProxyEvent.cs index aa16eae4..35b362c6 100644 --- a/src/SimpleL7Proxy/Events/ProxyEvent.cs +++ b/src/SimpleL7Proxy/Events/ProxyEvent.cs @@ -171,7 +171,7 @@ public void SendEvent() this["Type"] = "S7P-" + Type.ToString(); this["MID"] = MID ?? "N/A"; this["Ver"] = Constants.VERSION; - this["Revision"] = _options.Value.Revision; + this["Revision"] = _options!.Value.Revision; this["ContainerApp"] = _options.Value.ContainerApp; // Send the event to Event Hub _eventHubClient.SendData(this); diff --git a/src/SimpleL7Proxy/Proxy/HealthCheckService.cs b/src/SimpleL7Proxy/Proxy/HealthCheckService.cs index fac0e943..f50f297d 100644 --- a/src/SimpleL7Proxy/Proxy/HealthCheckService.cs +++ b/src/SimpleL7Proxy/Proxy/HealthCheckService.cs @@ -24,7 +24,7 @@ namespace SimpleL7Proxy.Proxy; public class HealthCheckService { private readonly IBackendService _backends; - private static BackendOptions _options; + private static BackendOptions _options=null!; private readonly IConcurrentPriQueue? _requestsQueue; private readonly IUserPriorityService? _userPriority; private readonly IEventClient? _eventClient; diff --git a/src/SimpleL7Proxy/Proxy/NullAsyncWorkerFactory.cs b/src/SimpleL7Proxy/Proxy/NullAsyncWorkerFactory.cs index 21508f19..03184839 100644 --- a/src/SimpleL7Proxy/Proxy/NullAsyncWorkerFactory.cs +++ b/src/SimpleL7Proxy/Proxy/NullAsyncWorkerFactory.cs @@ -5,6 +5,6 @@ public class NullAsyncWorkerFactory: IAsyncWorkerFactory public AsyncWorker CreateAsync(RequestData requestData, int AsyncTriggerTimeout) { //NOP - return null; + return null!; } } \ No newline at end of file diff --git a/src/SimpleL7Proxy/Proxy/ProxyWorker.cs b/src/SimpleL7Proxy/Proxy/ProxyWorker.cs index 553ecd3a..f3f06e96 100644 --- a/src/SimpleL7Proxy/Proxy/ProxyWorker.cs +++ b/src/SimpleL7Proxy/Proxy/ProxyWorker.cs @@ -521,8 +521,8 @@ public async Task TaskRunnerAsync() { _logger.LogDebug("[Worker:{Id}] Performing cleanup for request {Guid}", _id, incomingRequest.Guid); - // if (workerState != "Cleanup") - eventData["WorkerState"] = workerState; + if (workerState != "Cleanup") + eventData["WorkerState"] = workerState; incomingRequest.Cleanup(); @@ -582,9 +582,14 @@ public async Task TaskRunnerAsync() private async Task WriteResponseAsync(RequestData request, ProxyData pr) { + ArgumentNullException.ThrowIfNull(pr); + ArgumentNullException.ThrowIfNull(request, "Request context is null."); + ArgumentNullException.ThrowIfNull(request.Context, "Request context is null."); + var context = request.Context; + // Set the response status code - context.Response.StatusCode = (int)pr.StatusCode!; + context.Response.StatusCode = (int)pr.StatusCode; // Copy headers to the response //ProxyHelperUtils.CopyHeaders(request.Headers, proxyRequest, true, _options.StripRequestHeaders); @@ -1582,7 +1587,7 @@ private async Task StreamResponseAsync(RequestData request, ProxyData pr) processor.GetStats(request.EventData, proxyResponse.Headers); } - await _lifecycleManager.HandleBackgroundRequestLifecycle(request, processor).ConfigureAwait(false); + await _lifecycleManager.HandleBackgroundRequestLifecycle(request, processor!).ConfigureAwait(false); } catch (Exception ex) { @@ -1615,9 +1620,9 @@ private async Task HandleBackgroundCheckResultAsync( var pr = new ProxyData(); ProxyHelperUtils.CopyResponseHeaders(proxyResponse, pr); - if (pr.Headers != null) + if (pr.Headers != null && request.asyncWorker != null) { - await request.asyncWorker.WriteHeaders(proxyResponse.StatusCode, pr.Headers); + await request.asyncWorker.WriteHeaders(proxyResponse.StatusCode!, pr.Headers); } if (request.asyncWorker != null) diff --git a/src/SimpleL7Proxy/SimpleL7Proxy.csproj b/src/SimpleL7Proxy/SimpleL7Proxy.csproj index e98fc4f4..6f4b0209 100644 --- a/src/SimpleL7Proxy/SimpleL7Proxy.csproj +++ b/src/SimpleL7Proxy/SimpleL7Proxy.csproj @@ -19,15 +19,15 @@ - - - + + + - + diff --git a/src/SimpleL7Proxy/User/AsyncClientInfo.cs b/src/SimpleL7Proxy/User/AsyncClientInfo.cs index 8b533b0b..f20a841b 100644 --- a/src/SimpleL7Proxy/User/AsyncClientInfo.cs +++ b/src/SimpleL7Proxy/User/AsyncClientInfo.cs @@ -16,7 +16,6 @@ public AsyncClientInfo(string userId, string containerName, string sbTopicName, UserId = userId; ContainerName = containerName; SBTopicName = sbTopicName; - AsyncAllowed = asyncAllowed; AsyncBlobAccessTimeoutSecs = asyncBlobAccessTimeoutSecs; GenerateSasTokens = generateSasTokens; } diff --git a/src/SimpleL7Proxy/User/IUserProfileService.cs b/src/SimpleL7Proxy/User/IUserProfileService.cs index c99ae33c..d8672e8b 100644 --- a/src/SimpleL7Proxy/User/IUserProfileService.cs +++ b/src/SimpleL7Proxy/User/IUserProfileService.cs @@ -8,6 +8,5 @@ public interface IUserProfileService public bool IsAuthAppIDValid(string? authAppId); //public bool AsyncAllowed(string UserId); public AsyncClientInfo? GetAsyncParams(string UserId); - public bool ServiceIsReady(); } \ No newline at end of file diff --git a/src/SimpleL7Proxy/User/UserProfile.cs b/src/SimpleL7Proxy/User/UserProfile.cs index f86cf440..df8ed4e4 100644 --- a/src/SimpleL7Proxy/User/UserProfile.cs +++ b/src/SimpleL7Proxy/User/UserProfile.cs @@ -22,14 +22,12 @@ public class UserProfile : BackgroundService, IUserProfileService private List suspendedUserProfiles = new List(); private List authAppIDs = new List(); - private static bool _isInitialized = false; - public UserProfile(IOptions options, ILogger logger) { _options = options.Value; _logger = logger; _UserIDFieldName = _options.UserIDFieldName; - _userInformation[Constants.Server] = new AsyncClientInfo(Constants.Server, Constants.Server, Constants.Server, false, 3600); + _userInformation[Constants.Server] = new AsyncClientInfo(Constants.Server, Constants.Server, Constants.Server, 3600); _logger.LogDebug("[INIT] UserProfile service starting"); } @@ -67,49 +65,24 @@ protected override Task ExecuteAsync(CancellationToken stoppingToken) public async Task ConfigReader(CancellationToken cancellationToken) { - while (!cancellationToken.IsCancellationRequested) { - DateTime startTime = DateTime.UtcNow; - const int NormalDelayMs = 3600000; // 1 hour - const int ErrorDelayMs = 3000; // 3 seconds - try { await ReadUserConfigAsync(_options.UserConfigUrl, ParsingMode.profileMode).ConfigureAwait(false); await ReadUserConfigAsync(_options.SuspendedUserConfigUrl, ParsingMode.SuspendedUserMode).ConfigureAwait(false); await ReadUserConfigAsync(_options.ValidateAuthAppIDUrl, ParsingMode.AuthAppIDMode).ConfigureAwait(false); - - // Count users, initialized when at least one user profile is loaded - if (_options.UserConfigRequired && userProfiles.Count > 0 && authAppIDs.Count > 0) - { - _isInitialized = true; - } - else if (!_options.UserConfigRequired) - { - _isInitialized = true; - } } catch (Exception e) { - _logger.LogError($"Error reading user config: {e.Message}"); - _isInitialized = false; + // Log error + _logger.LogInformation($"Error reading user config: {e.Message}"); } - _logger.LogInformation($"[DATA] ✓ User profiles loaded - {userProfiles.Count} users found, {suspendedUserProfiles.Count} suspended users found, {authAppIDs.Count} auth app IDs found Initialized: {_isInitialized} " ); - - int baseDelay = _isInitialized ? NormalDelayMs : ErrorDelayMs; - int elapsedMs = (int)(DateTime.UtcNow - startTime).TotalMilliseconds; - int remainingDelay = Math.Max(0, baseDelay - elapsedMs); - await Task.Delay(remainingDelay, cancellationToken); + await Task.Delay(3600000, cancellationToken); } } - public bool ServiceIsReady() - { - return _isInitialized; - } - public async Task ReadUserConfigAsync(string config, ParsingMode mode) { if (string.IsNullOrEmpty(_options.UserConfigUrl)) @@ -368,7 +341,7 @@ public bool IsAuthAppIDValid(string? authAppId) if (!bool.TryParse(value, out asyncEnabled) || !asyncEnabled) { _logger.LogWarning($"User profile: async mode not allowed for user {userId}."); - asyncEnabled = false; + return null; } } else if (field == "containername") diff --git a/src/SimpleL7Proxy/config.json b/src/SimpleL7Proxy/config.json index a6b75e99..d0f7cdf9 100644 --- a/src/SimpleL7Proxy/config.json +++ b/src/SimpleL7Proxy/config.json @@ -13,7 +13,7 @@ "S7PPriorityKey": "12345", "Header1": "Value1", "Header2": "Value2", - "async-config": "enabled=true, containername=user123455, topic=status-123455" + "async-config": "enabled=true, containername=user123455, topic=status-12355" }, { "userId": "123457", diff --git a/src/SimpleL7Proxy/server.cs b/src/SimpleL7Proxy/server.cs index 87627729..29aaae42 100644 --- a/src/SimpleL7Proxy/server.cs +++ b/src/SimpleL7Proxy/server.cs @@ -190,12 +190,16 @@ public async Task Run(CancellationToken cancellationToken) if (completedTask == getContextTask) { var lc = await getContextTask.ConfigureAwait(false); + if (lc == null || lc.Request == null) + { + continue; + } // if it's a probe, then bypass all the below checks and enqueue the request - if (Constants.probes.Contains(lc?.Request?.Url?.PathAndQuery)) + if (Constants.probes.Contains(lc.Request.Url?.PathAndQuery)) { // Get ProbeData from pool using modulo rotation - var probePath = lc!.Request.Url!.PathAndQuery; + var probePath = lc.Request.Url!.PathAndQuery; var fallthrough = false; // Fast-path for probes to avoid queue and worker latency @@ -390,7 +394,7 @@ public async Task Run(CancellationToken cancellationToken) if (doAsync && bool.TryParse(rd.Headers[_options.AsyncClientRequestHeader], out var asyncEnabled) && asyncEnabled) { var clientInfo = _userProfile.GetAsyncParams(rd.profileUserId); - if (clientInfo != null && clientInfo.AsyncAllowed) + if (clientInfo != null) { rd.runAsync = true; rd.AsyncBlobAccessTimeoutSecs = clientInfo.AsyncBlobAccessTimeoutSecs;