// 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); } }
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); } } }
/// <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);
/// <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; }
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);
// 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);
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); }
/// <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);
/// <inheritdoc/> public override ValueTask ApplyAsync(ResponseTrailersTransformContext context) { return(_func(context)); }