File: Http\HttpRouteFormatter.cs
Web Access
Project: src\src\Libraries\Microsoft.Extensions.Telemetry\Microsoft.Extensions.Telemetry.csproj (Microsoft.Extensions.Telemetry)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Extensions.Compliance.Classification;
using Microsoft.Extensions.Compliance.Redaction;
using Microsoft.Extensions.Http.Diagnostics;
using Microsoft.Shared.Pools;
 
namespace Microsoft.Extensions.Http.Diagnostics;
 
internal sealed class HttpRouteFormatter : IHttpRouteFormatter
{
    private const char ForwardSlashSymbol = '/';
 
#if NET6_0_OR_GREATER
    private const char ForwardSlash = ForwardSlashSymbol;
#else
#pragma warning disable IDE1006 // Naming Styles
    private static readonly char[] ForwardSlash = new[] { ForwardSlashSymbol };
#pragma warning restore IDE1006 // Naming Styles
#endif
 
    private readonly IHttpRouteParser _httpRouteParser;
    private readonly IRedactorProvider _redactorProvider;
 
    public HttpRouteFormatter(IHttpRouteParser httpRouteParser, IRedactorProvider redactorProvider)
    {
        _httpRouteParser = httpRouteParser;
        _redactorProvider = redactorProvider;
    }
 
    public string Format(string httpRoute, string httpPath, HttpRouteParameterRedactionMode redactionMode, IReadOnlyDictionary<string, DataClassification> parametersToRedact)
    {
        var routeSegments = _httpRouteParser.ParseRoute(httpRoute);
        return Format(routeSegments, httpPath, redactionMode, parametersToRedact);
    }
 
    public string Format(
        in ParsedRouteSegments routeSegments,
        string httpPath,
        HttpRouteParameterRedactionMode redactionMode,
        IReadOnlyDictionary<string, DataClassification> parametersToRedact)
    {
        if (routeSegments.ParameterCount == 0 ||
            !IsRedactionRequired(routeSegments, redactionMode, parametersToRedact))
        {
            return httpPath.Trim(ForwardSlash);
        }
 
        var httpPathAsSpan = httpPath.AsSpan().TrimStart(ForwardSlash);
        var pathStringBuilder = PoolFactory.SharedStringBuilderPool.Get();
 
        try
        {
            int offset = 0;
 
            for (var i = 0; i < routeSegments.Segments.Length; i++)
            {
                var segment = routeSegments.Segments[i];
 
                if (segment.IsParam)
                {
                    var parameterContent = segment.Content;
                    var parameterTemplateLength = parameterContent.Length + 2;
 
                    var startIndex = segment.Start + offset;
 
                    // If we exceed a length of the http path it means that the appropriate http route
                    // has optional parameters or parameters with default values, and these parameters
                    // are omitted in the http path. In this case we stop processing and return resulting
                    // http path.
                    if (startIndex >= httpPathAsSpan.Length)
                    {
                        break;
                    }
 
                    int length;
 
                    if (i < routeSegments.Segments.Length - 1)
                    {
                        length = httpPathAsSpan.Slice(startIndex).IndexOf(routeSegments.Segments[i + 1].Content[0]);
                    }
                    else
                    {
                        length = httpPathAsSpan.Slice(startIndex).IndexOf(ForwardSlash);
                    }
 
                    if (length == -1)
                    {
                        length = httpPathAsSpan.Slice(startIndex).Length;
                    }
 
                    offset += length - parameterTemplateLength;
 
                    FormatParameter(httpPathAsSpan, segment, startIndex, length, redactionMode, parametersToRedact, pathStringBuilder);
                }
                else
                {
                    _ = pathStringBuilder.Append(segment.Content);
                }
            }
 
            RemoveTrailingForwardSlash(pathStringBuilder);
 
            return pathStringBuilder.ToString();
        }
        finally
        {
            PoolFactory.SharedStringBuilderPool.Return(pathStringBuilder);
        }
    }
 
    private static bool IsRedactionRequired(
        in ParsedRouteSegments routeSegments, HttpRouteParameterRedactionMode redactionMode, IReadOnlyDictionary<string, DataClassification> parametersToRedact)
    {
        if (redactionMode == HttpRouteParameterRedactionMode.None)
        {
            return false;
        }
 
        foreach (var segment in routeSegments.Segments)
        {
            if (!segment.IsParam)
            {
                continue;
            }
 
            if (redactionMode == HttpRouteParameterRedactionMode.Strict)
            {
                // If no data class exists for a parameter, and the parameter is not a well known parameter, then we redact it.
                // If data class exists and it's anything other than DataClassification.None, then also we redact it.
                if ((!parametersToRedact.TryGetValue(segment.ParamName, out DataClassification classification) &&
                    !Segment.IsKnownUnredactableParameter(segment.ParamName)) ||
                    classification != DataClassification.None)
                {
                    return true;
                }
            }
            else if (redactionMode == HttpRouteParameterRedactionMode.Loose)
            {
                // If data class exists for a parameter and it's anything other than DataClassification.None, then we redact it.
                if (parametersToRedact.TryGetValue(segment.ParamName, out DataClassification classification) && classification != DataClassification.None)
                {
                    return true;
                }
            }
            else
            {
                throw new InvalidOperationException(TelemetryCommonExtensions.UnsupportedEnumValueExceptionMessage);
            }
        }
 
        return false;
    }
 
    private static void RemoveTrailingForwardSlash(StringBuilder formattedHttpPath)
    {
        if (formattedHttpPath.Length > 1)
        {
            int index = formattedHttpPath.Length - 1;
 
            if (formattedHttpPath[index] == ForwardSlashSymbol)
            {
                _ = formattedHttpPath.Remove(index, 1);
            }
        }
    }
 
    private void FormatParameter(
        ReadOnlySpan<char> httpPath,
        in Segment segment,
        int startIndex,
        int length,
        HttpRouteParameterRedactionMode redactionMode,
        IReadOnlyDictionary<string, DataClassification> parametersToRedact,
        StringBuilder outputBuffer)
    {
        if (redactionMode == HttpRouteParameterRedactionMode.Strict)
        {
            FormatParameterInStrictMode(httpPath, segment, startIndex, length, parametersToRedact, outputBuffer);
            return;
        }
 
        if (redactionMode == HttpRouteParameterRedactionMode.Loose)
        {
            FormatParameterInLooseMode(httpPath, segment, startIndex, length, parametersToRedact, outputBuffer);
        }
    }
 
    private void FormatParameterInStrictMode(
        ReadOnlySpan<char> httpPath,
        Segment httpRouteSegment,
        int startIndex,
        int length,
        IReadOnlyDictionary<string, DataClassification> parametersToRedact,
        StringBuilder outputBuffer)
    {
        if (parametersToRedact.TryGetValue(httpRouteSegment.ParamName, out var classification))
        {
            if (classification != DataClassification.None)
            {
                var redactor = _redactorProvider.GetRedactor(classification);
                _ = outputBuffer.AppendRedacted(redactor, httpPath.Slice(startIndex, length));
            }
            else
            {
#if NETCOREAPP3_1_OR_GREATER
                _ = outputBuffer.Append(httpPath.Slice(startIndex, length));
#else
                _ = outputBuffer.Append(httpPath.Slice(startIndex, length).ToString());
#endif
            }
        }
        else if (Segment.IsKnownUnredactableParameter(httpRouteSegment.ParamName))
        {
#if NETCOREAPP3_1_OR_GREATER
            _ = outputBuffer.Append(httpPath.Slice(startIndex, length));
#else
            _ = outputBuffer.Append(httpPath.Slice(startIndex, length).ToString());
#endif
        }
        else
        {
#if NETCOREAPP3_1_OR_GREATER
            _ = outputBuffer.Append(TelemetryConstants.Redacted.AsSpan());
#else
            _ = outputBuffer.Append(TelemetryConstants.Redacted);
#endif
        }
    }
 
    private void FormatParameterInLooseMode(
        ReadOnlySpan<char> httpPath,
        Segment httpRouteSegment,
        int startIndex,
        int length,
        IReadOnlyDictionary<string, DataClassification> parametersToRedact,
        StringBuilder outputBuffer)
    {
        if (parametersToRedact.TryGetValue(httpRouteSegment.ParamName, out DataClassification classification)
            && classification != DataClassification.None)
        {
            var redactor = _redactorProvider.GetRedactor(classification);
            _ = outputBuffer.AppendRedacted(redactor, httpPath.Slice(startIndex, length));
        }
        else
        {
#if NETCOREAPP3_1_OR_GREATER
            _ = outputBuffer.Append(httpPath.Slice(startIndex, length));
#else
            _ = outputBuffer.Append(httpPath.Slice(startIndex, length).ToString());
#endif
        }
    }
}