File: Http\HttpRouteParser.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.Concurrent;
using System.Collections.Generic;
using Microsoft.Extensions.Compliance.Classification;
using Microsoft.Extensions.Compliance.Redaction;
using Microsoft.Extensions.Http.Diagnostics;
 
namespace Microsoft.Extensions.Http.Diagnostics;
 
internal sealed class HttpRouteParser : IHttpRouteParser
{
#if NETCOREAPP3_1_OR_GREATER
    private const char ForwardSlash = '/';
#else
#pragma warning disable IDE1006 // Naming Styles
    private static readonly char[] ForwardSlash = new[] { '/' };
#pragma warning restore IDE1006 // Naming Styles
#endif
 
    private readonly IRedactorProvider _redactorProvider;
    private readonly ConcurrentDictionary<string, ParsedRouteSegments> _routeTemplateSegmentsCache = new();
 
    public HttpRouteParser(IRedactorProvider redactorProvider)
    {
        _redactorProvider = redactorProvider;
    }
 
    public bool TryExtractParameters(
        string httpPath,
        in ParsedRouteSegments routeSegments,
        HttpRouteParameterRedactionMode redactionMode,
        IReadOnlyDictionary<string, DataClassification> parametersToRedact,
        ref HttpRouteParameter[] httpRouteParameters)
    {
        int paramCount = routeSegments.ParameterCount;
 
        if (httpRouteParameters == null || httpRouteParameters.Length < paramCount)
        {
            return false;
        }
 
        var httpPathAsSpan = httpPath.AsSpan();
        httpPathAsSpan = httpPathAsSpan.TrimStart(ForwardSlash);
 
        if (paramCount > 0)
        {
            int offset = 0;
            int index = 0;
 
            foreach (Segment segment in routeSegments.Segments)
            {
                if (segment.IsParam)
                {
                    var startIndex = segment.Start + offset;
 
                    string parameterValue;
                    bool isRedacted = false;
 
                    if (startIndex < httpPathAsSpan.Length)
                    {
                        var parameterContent = segment.Content;
                        var parameterTemplateLength = parameterContent.Length + 2;
                        var length = httpPathAsSpan.Slice(startIndex).IndexOf(ForwardSlash);
 
                        if (length == -1)
                        {
                            length = httpPathAsSpan.Slice(startIndex).Length;
                        }
 
                        offset += length - parameterTemplateLength;
 
                        parameterValue = GetRedactedParameterValue(httpPathAsSpan, segment, startIndex, length, redactionMode, parametersToRedact, ref isRedacted);
                    }
 
                    // 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 return a default value of the
                    // omitted parameter.
                    else
                    {
                        parameterValue = segment.DefaultValue;
                    }
 
                    httpRouteParameters[index++] = new HttpRouteParameter(segment.ParamName, parameterValue, isRedacted);
                }
            }
        }
 
        return true;
    }
 
    public ParsedRouteSegments ParseRoute(string httpRoute)
    {
        return _routeTemplateSegmentsCache.GetOrAdd(httpRoute, httpRoute =>
        {
            httpRoute = httpRoute.TrimStart(ForwardSlash);
 
            var pos = 0;
            var len = httpRoute.Length;
            var start = 0;
            char ch;
 
            var segments = new List<Segment>();
 
            while (pos < len)
            {
                ch = httpRoute[pos];
 
                // Start of a parameter segment.
                if (ch == '{')
                {
                    // End of the current text segment.
                    if (pos > start)
                    {
                        segments.Add(new Segment(
                            start: start,
                            end: pos,
                            content: GetSegmentContent(httpRoute, start, pos),
                            isParam: false));
                    }
 
                    segments.Add(GetParameterSegment(httpRoute, ref pos));
                    start = pos + 1;
                }
 
                // Start of the query parameters sections.
                else if (ch == '?')
                {
                    // Remove the query parameters from the template.
                    httpRoute = httpRoute.Substring(0, pos);
                    break;
                }
 
                pos++;
            }
 
            // End the last text segment if any.
            if (start < pos)
            {
                segments.Add(new Segment(
                    start: start,
                    end: pos,
                    content: GetSegmentContent(httpRoute, start, pos),
                    isParam: false));
            }
 
            return new ParsedRouteSegments(httpRoute, segments.ToArray());
        });
    }
 
    private static Segment GetParameterSegment(string httpRoute, ref int pos)
    {
        const int PositionNotFound = -1;
 
        int start = pos++;
        int paramNameEnd = PositionNotFound;
        int defaultValueStart = PositionNotFound;
 
        char ch;
 
        while ((ch = httpRoute[pos]) != '}')
        {
            // The segment has a default value '='. The character indicates
            // that we've met the end of the segment's parameter name and
            // the start of the default value.
            if (ch == '=')
            {
                if (paramNameEnd == PositionNotFound)
                {
                    paramNameEnd = pos;
                }
 
                defaultValueStart = pos + 1;
            }
 
            // The segment is optional '?' or has a constraint ':'.
            // When we meet one of the above characters it indicates
            // that we've met the end of the segment's parameter name.
            else if (ch == '?' || ch == ':')
            {
                if (paramNameEnd == PositionNotFound)
                {
                    paramNameEnd = pos;
                }
            }
 
            pos++;
        }
 
        string content = GetSegmentContent(httpRoute, start + 1, pos);
        string paramName = paramNameEnd == PositionNotFound
            ? content
            : GetSegmentContent(httpRoute, start + 1, paramNameEnd);
        string defaultValue = defaultValueStart == PositionNotFound
            ? string.Empty
            : GetSegmentContent(httpRoute, defaultValueStart, pos);
 
        // Remove the opening and closing curly braces when getting content.
        return new Segment(
            start: start,
            end: pos + 1,
            content: content,
            isParam: true,
            paramName: paramName,
            defaultValue: defaultValue);
    }
 
    private static string GetSegmentContent(string httpRoute, int start, int end)
    {
        return httpRoute.Substring(start, end - start);
    }
 
    private string GetRedactedParameterValue(
        ReadOnlySpan<char> httpPath,
        in Segment segment,
        int startIndex,
        int length,
        HttpRouteParameterRedactionMode redactionMode,
        IReadOnlyDictionary<string, DataClassification> parametersToRedact,
        ref bool isRedacted)
    {
        return redactionMode switch
        {
            HttpRouteParameterRedactionMode.None => httpPath.Slice(startIndex, length).ToString(),
            HttpRouteParameterRedactionMode.Strict => GetRedactedParameterInStrictMode(httpPath, segment, startIndex, length, parametersToRedact, ref isRedacted),
            HttpRouteParameterRedactionMode.Loose => GetRedactedParameterInLooseMode(httpPath, segment, startIndex, length, parametersToRedact, ref isRedacted),
            _ => throw new InvalidOperationException(TelemetryCommonExtensions.UnsupportedEnumValueExceptionMessage)
        };
    }
 
    private string GetRedactedParameterInStrictMode(
        ReadOnlySpan<char> httpPathAsSpan,
        Segment segment,
        int startIndex,
        int length,
        IReadOnlyDictionary<string, DataClassification> parametersToRedact,
        ref bool isRedacted)
    {
        if (parametersToRedact.TryGetValue(segment.ParamName, out DataClassification classification))
        {
            if (classification != DataClassification.None)
            {
                var redactor = _redactorProvider.GetRedactor(classification);
                isRedacted = true;
 
                return redactor.Redact(httpPathAsSpan.Slice(startIndex, length));
            }
 
            return httpPathAsSpan.Slice(startIndex, length).ToString();
        }
 
        if (Segment.IsKnownUnredactableParameter(segment.ParamName))
        {
            return httpPathAsSpan.Slice(startIndex, length).ToString();
        }
 
        isRedacted = true;
 
        return TelemetryConstants.Redacted;
    }
 
    private string GetRedactedParameterInLooseMode(
        ReadOnlySpan<char> httpPathAsSpan,
        Segment segment,
        int startIndex,
        int length,
        IReadOnlyDictionary<string, DataClassification> parametersToRedact,
        ref bool isRedacted)
    {
        if (parametersToRedact.TryGetValue(segment.ParamName, out DataClassification classification)
            && classification != DataClassification.None)
        {
            var redactor = _redactorProvider.GetRedactor(classification);
            isRedacted = true;
 
            return redactor.Redact(httpPathAsSpan.Slice(startIndex, length));
        }
 
        return httpPathAsSpan.Slice(startIndex, length).ToString();
    }
}