// See https://github.com/microsoft/reverse-proxy/blob/51d797986b1fea03500a1ad173d13a1176fb5552/src/ReverseProxy/Forwarder/RequestUtilities.cs#L61-L83
    public async Task RestrictedHeaders_CopiedIfAllowed(string names, int expected)
    {
        var httpContext    = new DefaultHttpContext();
        var trailerFeature = new TestTrailersFeature();

        httpContext.Features.Set <IHttpResponseTrailersFeature>(trailerFeature);
        var proxyResponse = new HttpResponseMessage();

        proxyResponse.TrailingHeaders.TryAddWithoutValidation(HeaderNames.Connection, "value1");
        proxyResponse.TrailingHeaders.TryAddWithoutValidation(HeaderNames.TransferEncoding, "value2");
        proxyResponse.TrailingHeaders.TryAddWithoutValidation(HeaderNames.KeepAlive, "value3");

        var allowed          = names.Split(';');
        var transform        = new ResponseTrailersAllowedTransform(allowed);
        var transformContext = new ResponseTrailersTransformContext()
        {
            HttpContext   = httpContext,
            ProxyResponse = proxyResponse,
            HeadersCopied = false,
        };
        await transform.ApplyAsync(transformContext);

        Assert.True(transformContext.HeadersCopied);

        Assert.Equal(expected, trailerFeature.Trailers.Count());
        foreach (var header in trailerFeature.Trailers)
        {
            Assert.Contains(header.Key, allowed, StringComparer.OrdinalIgnoreCase);
        }
    }
Exemple #2
0
        public override async ValueTask TransformResponseTrailersAsync(HttpContext httpContext, HttpResponseMessage proxyResponse)
        {
            if (ShouldCopyResponseTrailers.GetValueOrDefault(true))
            {
                await base.TransformResponseTrailersAsync(httpContext, proxyResponse);
            }

            if (ResponseTrailerTransforms.Count == 0)
            {
                return;
            }

            // Only run the transforms if trailers are actually supported by the client response.
            var responseTrailersFeature = httpContext.Features.Get <IHttpResponseTrailersFeature>();
            var outgoingTrailers        = responseTrailersFeature?.Trailers;

            if (outgoingTrailers != null && !outgoingTrailers.IsReadOnly)
            {
                var transformContext = new ResponseTrailersTransformContext()
                {
                    HttpContext   = httpContext,
                    ProxyResponse = proxyResponse,
                    HeadersCopied = ShouldCopyResponseTrailers.GetValueOrDefault(true),
                };

                foreach (var responseTrailerTransform in ResponseTrailerTransforms)
                {
                    await responseTrailerTransform.ApplyAsync(transformContext);
                }
            }
        }
Exemple #3
0
    /// <summary>
    /// Removes and returns the current trailer value by first checking the HttpResponse
    /// and falling back to the value from HttpResponseMessage only if
    /// <see cref="ResponseTrailersTransformContext.HeadersCopied"/> is not set.
    /// This ordering allows multiple transforms to mutate the same header.
    /// </summary>
    /// <param name="context">The transform context.</param>
    /// <param name="headerName">The name of the header to take.</param>
    /// <returns>The response header value, or StringValues.Empty if none.</returns>
    public static StringValues TakeHeader(ResponseTrailersTransformContext context, string headerName)
    {
        if (context is null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (string.IsNullOrEmpty(headerName))
        {
            throw new ArgumentException($"'{nameof(headerName)}' cannot be null or empty.", nameof(headerName));
        }

        Debug.Assert(context.ProxyResponse is not null);

        var responseTrailersFeature = context.HttpContext.Features.Get <IHttpResponseTrailersFeature>();
        var responseTrailers        = responseTrailersFeature?.Trailers;

        // Support should have already been checked by the caller.
        Debug.Assert(responseTrailers is not null);
        Debug.Assert(!responseTrailers.IsReadOnly);

        if (responseTrailers.TryGetValue(headerName, out var existingValues))
        {
            responseTrailers.Remove(headerName);
        }
        else if (!context.HeadersCopied)
        {
            RequestUtilities.TryGetValues(context.ProxyResponse.TrailingHeaders, headerName, out existingValues);
        }

        return(existingValues);
    }
    /// <inheritdoc/>
    public override ValueTask ApplyAsync(ResponseTrailersTransformContext context)
    {
        if (context is null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        Debug.Assert(context.ProxyResponse != null);
        Debug.Assert(!context.HeadersCopied);

        // See https://github.com/microsoft/reverse-proxy/blob/51d797986b1fea03500a1ad173d13a1176fb5552/src/ReverseProxy/Forwarder/HttpTransformer.cs#L85-L99
        // NOTE: Deliberately not using `context.Response.SupportsTrailers()`, `context.Response.AppendTrailer(...)`
        // because they lookup `IHttpResponseTrailersFeature` for every call. Here we do it just once instead.
        var responseTrailersFeature = context.HttpContext.Features.Get <IHttpResponseTrailersFeature>();
        var outgoingTrailers        = responseTrailersFeature?.Trailers;

        if (outgoingTrailers != null && !outgoingTrailers.IsReadOnly)
        {
            // Note that trailers, if any, should already have been declared in Proxy's response
            CopyResponseHeaders(context.ProxyResponse.TrailingHeaders, outgoingTrailers);
        }

        context.HeadersCopied = true;

        return(default);
Exemple #5
0
    /// <summary>
    /// Sets the given trailer on the HttpResponse.
    /// </summary>
    public static void SetHeader(ResponseTrailersTransformContext context, string headerName, StringValues values)
    {
        var responseTrailersFeature = context.HttpContext.Features.Get <IHttpResponseTrailersFeature>();
        var responseTrailers        = responseTrailersFeature?.Trailers;

        // Support should have already been checked by the caller.
        Debug.Assert(responseTrailers is not null);
        Debug.Assert(!responseTrailers.IsReadOnly);

        responseTrailers[headerName] = values;
    }
Exemple #6
0
        public async Task AddResponseTrailer_Success(string startValue, int status, string value, bool append, bool always, string expected)
        {
            var httpContext    = new DefaultHttpContext();
            var trailerFeature = new TestTrailersFeature();

            httpContext.Features.Set <IHttpResponseTrailersFeature>(trailerFeature);
            trailerFeature.Trailers["name"] = startValue.Split(";", System.StringSplitOptions.RemoveEmptyEntries);
            httpContext.Response.StatusCode = status;
            var transformContext = new ResponseTrailersTransformContext()
            {
                HttpContext   = httpContext,
                ProxyResponse = new HttpResponseMessage(),
                HeadersCopied = true,
            };
            var transform = new ResponseTrailerValueTransform("name", value, append, always);
            await transform.ApplyAsync(transformContext);

            Assert.Equal(expected.Split(";", System.StringSplitOptions.RemoveEmptyEntries), trailerFeature.Trailers["name"]);
        }
    // Assumes the response status code has been set on the HttpContext already.
    /// <inheritdoc/>
    public override ValueTask ApplyAsync(ResponseTrailersTransformContext context)
    {
        if (context is null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        Debug.Assert(context.ProxyResponse is not null);

        if (Condition == ResponseCondition.Always ||
            Success(context) == (Condition == ResponseCondition.Success))
        {
            var responseTrailersFeature = context.HttpContext.Features.Get <IHttpResponseTrailersFeature>();
            var responseTrailers        = responseTrailersFeature?.Trailers;
            // Support should have already been checked by the caller.
            Debug.Assert(responseTrailers is not null);
            Debug.Assert(!responseTrailers.IsReadOnly);

            responseTrailers.Remove(HeaderName);
        }

        return(default);
Exemple #8
0
    // Assumes the response status code has been set on the HttpContext already.
    /// <inheritdoc/>
    public override ValueTask ApplyAsync(ResponseTrailersTransformContext context)
    {
        if (context is null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (Condition == ResponseCondition.Always ||
            Success(context) == (Condition == ResponseCondition.Success))
        {
            if (Append)
            {
                var existingHeader = TakeHeader(context, HeaderName);
                var value          = StringValues.Concat(existingHeader, Value);
                SetHeader(context, HeaderName, value);
            }
            else
            {
                SetHeader(context, HeaderName, Value);
            }
        }

        return(default);
Exemple #9
0
 internal static bool Success(ResponseTrailersTransformContext context)
 {
     // TODO: How complex should this get? Compare with http://nginx.org/en/docs/http/ngx_http_headers_module.html#add_header
     return(context.HttpContext.Response.StatusCode < 400);
 }
Exemple #10
0
 /// <summary>
 /// Transforms the given response trailers. The trailers will have (optionally) already been
 /// copied to the <see cref="HttpResponse"/> and any changes should be made there.
 /// </summary>
 public abstract ValueTask ApplyAsync(ResponseTrailersTransformContext context);
Exemple #11
0
 /// <inheritdoc/>
 public override ValueTask ApplyAsync(ResponseTrailersTransformContext context)
 {
     return(_func(context));
 }