public StreamCopyHttpContent(Stream source, StreamCopier streamCopier, bool autoFlushHttpClientOutgoingStream, CancellationToken cancellation) { _source = source ?? throw new ArgumentNullException(nameof(source)); _streamCopier = streamCopier ?? throw new ArgumentNullException(nameof(streamCopier)); _autoFlushHttpClientOutgoingStream = autoFlushHttpClientOutgoingStream; _cancellation = cancellation; }
private StreamCopyHttpContent SetupCopyBodyUpstream(HttpRequest request, HttpRequestMessage upstreamRequest, bool isStreamingRequest, CancellationToken cancellation) { // If we generate an HttpContent without a Content-Length then for HTTP/1.1 HttpClient will add a Transfer-Encoding: chunked header // even if it's a GET request. Some servers reject requests containing a Transfer-Encoding header if they're not expecting a body. // Try to be as specific as possible about the client's intent to send a body. The one thing we don't want to do is to start // reading the body early because that has side-effects like 100-continue. var hasBody = true; var contentLength = request.Headers.ContentLength; var method = request.Method; // https://tools.ietf.org/html/rfc7231#section-4.3.8 // A client MUST NOT send a message body in a TRACE request. if (HttpMethods.IsTrace(method)) { hasBody = false; } // https://tools.ietf.org/html/rfc7230#section-3.3.3 // All HTTP/1.1 requests should have Transfer-Encoding or Content-Length. // Http.Sys/IIS will even add a Transfer-Encoding header to HTTP/2 requests with bodies for back-compat. // HTTP/1.0 Connection: close bodies are only allowed on responses, not requests. // https://tools.ietf.org/html/rfc1945#section-7.2.2 // // Transfer-Encoding overrides Content-Length per spec else if (request.Headers.TryGetValue(HeaderNames.TransferEncoding, out var transferEncoding) && transferEncoding.Count == 1 && string.Equals("chunked", transferEncoding.ToString(), StringComparison.OrdinalIgnoreCase)) { hasBody = true; } else if (contentLength.HasValue) { hasBody = contentLength > 0; } // Kestrel HTTP/2: There are no required headers that indicate if there is a request body so we need to sniff other fields. else if (!ProtocolHelper.IsHttp2OrGreater(request.Protocol)) { hasBody = false; } // https://tools.ietf.org/html/rfc7231#section-5.1.1 // A client MUST NOT generate a 100-continue expectation in a request that does not include a message body. else if (request.Headers.TryGetValue(HeaderNames.Expect, out var expect) && expect.Count == 1 && string.Equals("100-continue", expect.ToString(), StringComparison.OrdinalIgnoreCase)) { hasBody = true; } // https://tools.ietf.org/html/rfc7231#section-4.3.1 // A payload within a GET/HEAD/DELETE/CONNECT request message has no defined semantics; sending a payload body on a // GET/HEAD/DELETE/CONNECT request might cause some existing implementations to reject the request. else if (HttpMethods.IsGet(method) || HttpMethods.IsHead(method) || HttpMethods.IsDelete(method) || HttpMethods.IsConnect(method)) { hasBody = false; } // else hasBody defaults to true StreamCopyHttpContent contentToUpstream = null; if (hasBody) { ////this.logger.LogInformation($" Setting up downstream --> Proxy --> upstream body proxying"); // Note on `autoFlushHttpClientOutgoingStream: isStreamingRequest`: // The.NET Core HttpClient stack keeps its own buffers on top of the underlying outgoing connection socket. // We flush those buffers down to the socket on every write when this is set, // but it does NOT result in calls to flush on the underlying socket. // This is necessary because we proxy http2 transparently, // and we are deliberately unaware of packet structure used e.g. in gRPC duplex channels. // Because the sockets aren't flushed, the perf impact of this choice is expected to be small. // Future: It may be wise to set this to true for *all* http2 incoming requests, // but for now, out of an abundance of caution, we only do it for requests that look like gRPC. var streamCopier = new StreamCopier(); contentToUpstream = new StreamCopyHttpContent( source: request.Body, streamCopier: streamCopier, autoFlushHttpClientOutgoingStream: isStreamingRequest, cancellation: cancellation); upstreamRequest.Content = contentToUpstream; } return(contentToUpstream); }
/// <summary> /// Proxies an upgradable request to the upstream server, treating the upgraded stream as an opaque duplex channel. /// </summary> /// <remarks> /// Upgradable request proxying comprises the following steps: /// (1) Create outgoing HttpRequestMessage /// (2) Copy request headers Downstream ---► Proxy ---► Upstream /// (3) Send the outgoing request using HttpMessageInvoker Downstream ---► Proxy ---► Upstream /// (4) Copy response status line Downstream ◄--- Proxy ◄--- Upstream /// (5) Copy response headers Downstream ◄--- Proxy ◄--- Upstream /// Scenario A: upgrade with upstream worked (got 101 response) /// (A-6) Upgrade downstream channel (also sends response headers) Downstream ◄--- Proxy ◄--- Upstream /// (A-7) Copy duplex streams Downstream ◄--► Proxy ◄--► Upstream /// ---- or ---- /// Scenario B: upgrade with upstream failed (got non-101 response) /// (B-6) Send response headers Downstream ◄--- Proxy ◄--- Upstream /// (B-7) Copy response body Downstream ◄--- Proxy ◄--- Upstream /// /// This takes care of WebSockets as well as any other upgradable protocol. /// </remarks> private async Task UpgradableProxyAsync( HttpContext context, IHttpUpgradeFeature upgradeFeature, HttpRequestMessage upstreamRequest, HttpMessageInvoker httpClient, CancellationToken shortCancellation, CancellationToken longCancellation, Action <HttpRequestMessage> requestAction = null) { _ = context ?? throw new ArgumentNullException(nameof(context)); _ = upgradeFeature ?? throw new ArgumentNullException(nameof(upgradeFeature)); _ = upstreamRequest ?? throw new ArgumentNullException(nameof(upstreamRequest)); _ = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 2: Copy request headers Downstream --► Proxy --► Upstream CopyHeadersToUpstream(context, upstreamRequest); if (requestAction != null) { requestAction(upstreamRequest); } // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 3: Send the outgoing request using HttpMessageInvoker var upstreamResponse = await httpClient.SendAsync(upstreamRequest, shortCancellation); var upgraded = upstreamResponse.StatusCode == HttpStatusCode.SwitchingProtocols && upstreamResponse.Content != null; // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 4: Copy response status line Downstream ◄-- Proxy ◄-- Upstream context.Response.StatusCode = (int)upstreamResponse.StatusCode; context.Features.Get <IHttpResponseFeature>().ReasonPhrase = upstreamResponse.ReasonPhrase; // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 5: Copy response headers Downstream ◄-- Proxy ◄-- Upstream CopyHeadersToDownstream(upstreamResponse, context); if (!upgraded) { // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step B-6: Send response headers Downstream ◄-- Proxy ◄-- Upstream // This is important to avoid any extra delays in sending response headers // e.g. if the upstream server is slow to provide its response body. await context.Response.StartAsync(shortCancellation); // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step B-7: Copy response body Downstream ◄-- Proxy ◄-- Upstream await CopyBodyDownstreamAsync(upstreamResponse.Content, context.Response.Body, longCancellation); return; } // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step A-6: Upgrade the downstream channel. This will send all response headers too. using var downstreamStream = await upgradeFeature.UpgradeAsync(); // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step A-7: Copy duplex streams var upstreamStream = await upstreamResponse.Content.ReadAsStreamAsync(); var upstreamCopier = new StreamCopier(); var upstreamTask = upstreamCopier.CopyAsync(downstreamStream, upstreamStream, longCancellation); var downstreamCopier = new StreamCopier(); var downstreamTask = downstreamCopier.CopyAsync(upstreamStream, downstreamStream, longCancellation); await Task.WhenAll(upstreamTask, downstreamTask); }