// 处理protocol; private void handleProtocol(object obj) { string pro = obj as string; ProtocolHelper helper = new ProtocolHelper(pro); FileProtocol protocol = helper.GetProtocol(); if (protocol.Mode == FileRequestMode.Send) { // 客户端发送文件,对服务端来说则是接收文件; receiveFile(protocol); } else if (protocol.Mode == FileRequestMode.Receive) { // 客户端接收文件,对服务端来说则是发送文件; // sendFile(protocol); } }
/// <summary> /// Implementation of the test itself. /// </summary> /// <returns>true if the test passes, false otherwise.</returns> public override async Task <bool> RunAsync() { IPAddress NodeIp = (IPAddress)ArgumentValues["Node IP"]; int PrimaryPort = (int)ArgumentValues["primary Port"]; log.Trace("(NodeIp:'{0}',PrimaryPort:{1})", NodeIp, PrimaryPort); bool res = false; Passed = false; ProtocolClient client = new ProtocolClient(); try { MessageBuilder mb = client.MessageBuilder; // Step 1 await client.ConnectAsync(NodeIp, PrimaryPort, false); byte[] payload = Encoding.UTF8.GetBytes("test"); Message requestMessage = mb.CreatePingRequest(payload); byte[] messageData = ProtocolHelper.GetMessageBytes(requestMessage); byte[] part1 = new byte[6]; byte[] part2 = new byte[messageData.Length - part1.Length]; Array.Copy(messageData, 0, part1, 0, part1.Length); Array.Copy(messageData, part1.Length, part2, 0, part2.Length); await client.SendRawAsync(part1); log.Trace("Entering 500 seconds wait..."); await Task.Delay(500 * 1000); log.Trace("Wait completed."); // We should be disconnected by now, so sending or receiving should throw. bool disconnectedOk = false; try { await client.SendRawAsync(part2); await client.ReceiveMessageAsync(); } catch { log.Trace("Expected exception occurred."); disconnectedOk = true; } // Step 1 Acceptance Passed = disconnectedOk; res = true; } catch (Exception e) { log.Error("Exception occurred: {0}", e.ToString()); } client.Dispose(); log.Trace("(-):{0}", res); return(res); }
protected override void OnInitializingHandler() { base.OnInitializingHandler(); if (!base.ClientRequest.IsAuthenticated) { base.IsWsSecurityRequest = base.ClientRequest.IsAnyWsSecurityRequest(); if (base.IsWsSecurityRequest && !AutodiscoverEwsWebConfiguration.WsSecurityEndpointEnabled) { throw new HttpException(404, "WS-Security endpoint is not supported"); } } if (base.ClientRequest.Url.ToString().EndsWith("autodiscover.xml", StringComparison.OrdinalIgnoreCase) || ProtocolHelper.IsAutodiscoverV2Request(base.ClientRequest.Url.AbsolutePath)) { base.PreferAnchorMailboxHeader = true; } }
/// <summary> /// Proxies a normal (i.e. non-upgradable) request to the upstream server, and the response back to our client. /// </summary> /// <remarks> /// Normal proxying comprises the following steps: /// (0) Disable ASP .NET Core limits for streaming requests /// (1) Create outgoing HttpRequestMessage /// (2) Setup copy of request body (background) Downstream --► Proxy --► Upstream /// (3) Copy request headers Downstream --► Proxy --► Upstream /// (4) Send the outgoing request using HttpMessageInvoker Downstream --► Proxy --► Upstream /// (5) Copy response status line Downstream ◄-- Proxy ◄-- Upstream /// (6) Copy response headers Downstream ◄-- Proxy ◄-- Upstream /// (7) Copy response body Downstream ◄-- Proxy ◄-- Upstream /// (8) Copy response trailer headers and finish response Downstream ◄-- Proxy ◄-- Upstream /// (9) Wait for completion of step 2: copying request body Downstream --► Proxy --► Upstream /// /// ASP .NET Core (Kestrel) will finally send response trailers (if any) /// after we complete the steps above and relinquish control. /// </remarks> private async Task NormalProxyAsync( HttpContext context, HttpRequestMessage upstreamRequest, Transforms transforms, HttpMessageInvoker httpClient, ProxyTelemetryContext proxyTelemetryContext, CancellationToken shortCancellation, CancellationToken longCancellation) { _ = context ?? throw new ArgumentNullException(nameof(context)); _ = upstreamRequest ?? throw new ArgumentNullException(nameof(upstreamRequest)); _ = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 0: Disable ASP .NET Core limits for streaming requests var isIncomingHttp2 = ProtocolHelper.IsHttp2(context.Request.Protocol); // NOTE: We heuristically assume gRPC-looking requests may require streaming semantics. // See https://github.com/microsoft/reverse-proxy/issues/118 for design discussion. var isStreamingRequest = isIncomingHttp2 && ProtocolHelper.IsGrpcContentType(context.Request.ContentType); if (isStreamingRequest) { DisableMinRequestBodyDataRateAndMaxRequestBodySize(context); } // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 2: Setup copy of request body (background) Downstream --► Proxy --► Upstream // Note that we must do this before step (3) because step (3) may also add headers to the HttpContent that we set up here. var bodyToUpstreamContent = SetupCopyBodyUpstream(context.Request.Body, upstreamRequest, in proxyTelemetryContext, isStreamingRequest, longCancellation); // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 3: Copy request headers Downstream --► Proxy --► Upstream CopyHeadersToUpstream(context, upstreamRequest, transforms.CopyRequestHeaders, transforms.RequestHeaderTransforms); // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 4: Send the outgoing request using HttpClient ////this.logger.LogInformation($" Starting Proxy --> upstream request"); var upstreamResponse = await httpClient.SendAsync(upstreamRequest, shortCancellation); // Detect connection downgrade, which may be problematic for e.g. gRPC. if (isIncomingHttp2 && upstreamResponse.Version.Major != 2) { // TODO: Do something on connection downgrade... Log.HttpDowngradeDeteced(_logger); } // Assert that, if we are proxying content upstream, it must have started by now // (since HttpClient.SendAsync has already completed asynchronously). // If this check fails, there is a coding defect which would otherwise // cause us to wait forever in step 9, so fail fast here. if (bodyToUpstreamContent != null && !bodyToUpstreamContent.Started) { // TODO: bodyToUpstreamContent is never null. HttpClient might would not need to read the body in some scenarios, such as an early auth failure with Expect: 100-continue. throw new InvalidOperationException("Proxying the downstream request body to the upstream server hasn't started. This is a coding defect."); } // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 5: Copy response status line Downstream ◄-- Proxy ◄-- Upstream ////this.logger.LogInformation($" Setting downstream <-- Proxy status: {(int)upstreamResponse.StatusCode} {upstreamResponse.ReasonPhrase}"); context.Response.StatusCode = (int)upstreamResponse.StatusCode; context.Features.Get <IHttpResponseFeature>().ReasonPhrase = upstreamResponse.ReasonPhrase; // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 6: Copy response headers Downstream ◄-- Proxy ◄-- Upstream CopyHeadersToDownstream(upstreamResponse, context, transforms.ResponseHeaderTransforms); // NOTE: it may *seem* wise to call `context.Response.StartAsync()` at this point // since it looks like we are ready to send back response headers // (and this might help reduce extra delays while we wait to receive the body from upstream). // HOWEVER, this would produce the wrong result if it turns out that there is no content // from the upstream -- instead of sending headers and terminating the stream at once, // we would send headers thinking a body may be coming, and there is none. // This is problematic on gRPC connections when the upstream server encounters an error, // in which case it immediately returns the response headers and trailing headers, but no content, // and clients misbehave if the initial headers response does not indicate stream end. // TODO: Some of the tasks in steps (7) - (9) may go unobserved depending on what fails first. Needs more consideration. // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 7: Copy response body Downstream ◄-- Proxy ◄-- Upstream await CopyBodyDownstreamAsync(upstreamResponse.Content, context.Response.Body, proxyTelemetryContext, longCancellation); // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 8: Copy response trailer headers and finish response Downstream ◄-- Proxy ◄-- Upstream CopyTrailingHeadersToDownstream(upstreamResponse, context, transforms.ResponseTrailerTransforms); if (isStreamingRequest) { // NOTE: We must call `CompleteAsync` so that Kestrel will flush all bytes to the client. // In the case where there was no response body, // this is also when headers and trailing headers are sent to the client. // Without this, the client might wait forever waiting for response bytes, // while we might wait forever waiting for request bytes, // leading to a stuck connection and no way to make progress. await context.Response.CompleteAsync(); } // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 9: Wait for completion of step 2: copying request body Downstream --► Proxy --► Upstream if (bodyToUpstreamContent != null) { ////this.logger.LogInformation($" Waiting for downstream --> Proxy --> upstream body proxying to complete"); await bodyToUpstreamContent.ConsumptionTask; } }
/// <summary> /// Implementation of the test itself. /// </summary> /// <returns>true if the test passes, false otherwise.</returns> public override async Task <bool> RunAsync() { IPAddress ServerIp = (IPAddress)ArgumentValues["Server IP"]; int PrimaryPort = (int)ArgumentValues["primary Port"]; int BasePort = (int)ArgumentValues["Base Port"]; int LocPort = (int)ArgumentValues["LOC Port"]; log.Trace("(ServerIp:'{0}',PrimaryPort:{1},BasePort:{2},LocPort:{3})", ServerIp, PrimaryPort, BasePort, LocPort); bool res = false; Passed = false; ProtocolClient client = new ProtocolClient(); ProfileServer profileServer = null; LocServer locServer = null; try { MessageBuilder mb = client.MessageBuilder; // Step 1 log.Trace("Step 1"); // Get port list. await client.ConnectAsync(ServerIp, PrimaryPort, false); Dictionary <ServerRoleType, uint> rolePorts = new Dictionary <ServerRoleType, uint>(); bool listPortsOk = await client.ListServerPorts(rolePorts); client.CloseConnection(); // Create identities. ProfilePublicKeys = new List <byte[]>(); for (int i = 0; i < ProfileNames.Count; i++) { ProtocolClient protocolClient = new ProtocolClient(); ProfilePublicKeys.Add(protocolClient.GetIdentityKeys().PublicKey); protocolClient.Dispose(); } // Start simulated profile server. profileServer = new ProfileServer("TestProfileServer", ServerIp, BasePort, client.GetIdentityKeys(), new GpsLocation(1, 2)); bool profileServerStartOk = profileServer.Start(); // Start simulated LOC server. locServer = new LocServer("TestLocServer", ServerIp, LocPort); bool locServerStartOk = locServer.Start(); await locServer.WaitForProfileServerConnectionAsync(); bool step1Ok = profileServerStartOk && locServerStartOk; log.Trace("Step 1: {0}", step1Ok ? "PASSED" : "FAILED"); // Step 2 log.Trace("Step 2"); // Initialize the original set of update messages update. List <SharedProfileUpdateItem> originalUpdateItems = new List <SharedProfileUpdateItem>(); for (int i = 0; i < ProfileNames.Count; i++) { SharedProfileUpdateItem updateItem = new SharedProfileUpdateItem() { Add = new SharedProfileAddItem() { Version = SemVer.V100.ToByteString(), Name = ProfileNames[i], Type = ProfileTypes[i], ExtraData = ProfileExtraData[i] != null ? ProfileExtraData[i] : "", Latitude = ProfileLocations[i].GetLocationTypeLatitude(), Longitude = ProfileLocations[i].GetLocationTypeLongitude(), IdentityPublicKey = ProtocolHelper.ByteArrayToByteString(ProfilePublicKeys[i]), SetThumbnailImage = ProfileImages[i] != null, ThumbnailImage = ProtocolHelper.ByteArrayToByteString(ProfileImages[i] != null ? File.ReadAllBytes(ProfileImages[i]) : new byte[0]) } }; originalUpdateItems.Add(updateItem); } List <SharedProfileUpdateItem> updateItems = new List <SharedProfileUpdateItem>(originalUpdateItems); updateItems[2] = new SharedProfileUpdateItem(updateItems[2]); updateItems[2].Add.Version = ProtocolHelper.ByteArrayToByteString(new byte[] { 1, 0 }); bool initOk = await PerformInitializationProcessWithUpdateItemsAsync(updateItems, profileServer, locServer, "2.add.version"); bool step2Ok = initOk; log.Trace("Step 2: {0}", step2Ok ? "PASSED" : "FAILED"); // Step 3 log.Trace("Step 3"); updateItems = new List <SharedProfileUpdateItem>(originalUpdateItems); updateItems[2] = new SharedProfileUpdateItem(updateItems[2]); updateItems[2].Add.Version = ProtocolHelper.ByteArrayToByteString(new byte[] { 0, 0, 0 }); initOk = await PerformInitializationProcessWithUpdateItemsAsync(updateItems, profileServer, locServer, "2.add.version"); bool step3Ok = initOk; log.Trace("Step 3: {0}", step3Ok ? "PASSED" : "FAILED"); // Step 4 log.Trace("Step 4"); updateItems = new List <SharedProfileUpdateItem>(originalUpdateItems); updateItems[2] = new SharedProfileUpdateItem(updateItems[2]); updateItems[2].Add.IdentityPublicKey = ProtocolHelper.ByteArrayToByteString(Encoding.UTF8.GetBytes(new string ('a', 300))); initOk = await PerformInitializationProcessWithUpdateItemsAsync(updateItems, profileServer, locServer, "2.add.identityPublicKey"); bool step4Ok = initOk; log.Trace("Step 4: {0}", step4Ok ? "PASSED" : "FAILED"); // Step 5 log.Trace("Step 5"); updateItems = new List <SharedProfileUpdateItem>(originalUpdateItems); updateItems[2] = new SharedProfileUpdateItem(updateItems[2]); updateItems[2].Add.IdentityPublicKey = updateItems[0].Add.IdentityPublicKey; initOk = await PerformInitializationProcessWithUpdateItemsAsync(updateItems, profileServer, locServer, "2.add.identityPublicKey"); bool step5Ok = initOk; log.Trace("Step 5: {0}", step5Ok ? "PASSED" : "FAILED"); // Step 6 log.Trace("Step 6"); updateItems = new List <SharedProfileUpdateItem>(originalUpdateItems); updateItems[2] = new SharedProfileUpdateItem(updateItems[2]); updateItems[2].Add.Name = new string('a', 70); initOk = await PerformInitializationProcessWithUpdateItemsAsync(updateItems, profileServer, locServer, "2.add.name"); bool step6Ok = initOk; log.Trace("Step 6: {0}", step6Ok ? "PASSED" : "FAILED"); // Step 7 log.Trace("Step 7"); updateItems = new List <SharedProfileUpdateItem>(originalUpdateItems); updateItems[2] = new SharedProfileUpdateItem(updateItems[2]); updateItems[2].Add.Name = new string('ɐ', 50); initOk = await PerformInitializationProcessWithUpdateItemsAsync(updateItems, profileServer, locServer, "2.add.name"); bool step7Ok = initOk; log.Trace("Step 7: {0}", step7Ok ? "PASSED" : "FAILED"); // Step 8 log.Trace("Step 8"); updateItems = new List <SharedProfileUpdateItem>(originalUpdateItems); updateItems[2] = new SharedProfileUpdateItem(updateItems[2]); updateItems[2].Add.Type = new string('a', 70); initOk = await PerformInitializationProcessWithUpdateItemsAsync(updateItems, profileServer, locServer, "2.add.type"); bool step8Ok = initOk; log.Trace("Step 8: {0}", step8Ok ? "PASSED" : "FAILED"); // Step 9 log.Trace("Step 9"); updateItems = new List <SharedProfileUpdateItem>(originalUpdateItems); updateItems[2] = new SharedProfileUpdateItem(updateItems[2]); updateItems[2].Add.Type = new string('ɐ', 50); initOk = await PerformInitializationProcessWithUpdateItemsAsync(updateItems, profileServer, locServer, "2.add.type"); bool step9Ok = initOk; log.Trace("Step 9: {0}", step9Ok ? "PASSED" : "FAILED"); // Step 10 log.Trace("Step 10"); updateItems = new List <SharedProfileUpdateItem>(originalUpdateItems); updateItems[2] = new SharedProfileUpdateItem(updateItems[2]); updateItems[2].Add.Type = ""; initOk = await PerformInitializationProcessWithUpdateItemsAsync(updateItems, profileServer, locServer, "2.add.type"); bool step10Ok = initOk; log.Trace("Step 10: {0}", step10Ok ? "PASSED" : "FAILED"); // Step 11 log.Trace("Step 11"); updateItems = new List <SharedProfileUpdateItem>(originalUpdateItems); updateItems[2] = new SharedProfileUpdateItem(updateItems[2]); updateItems[2].Add.SetThumbnailImage = true; updateItems[2].Add.ThumbnailImage = ProtocolHelper.ByteArrayToByteString(new byte[0]); initOk = await PerformInitializationProcessWithUpdateItemsAsync(updateItems, profileServer, locServer, "2.add.thumbnailImage"); bool step11Ok = initOk; log.Trace("Step 11: {0}", step11Ok ? "PASSED" : "FAILED"); // Step 12 log.Trace("Step 12"); updateItems = new List <SharedProfileUpdateItem>(originalUpdateItems); updateItems[2] = new SharedProfileUpdateItem(updateItems[2]); updateItems[2].Add.SetThumbnailImage = true; updateItems[2].Add.ThumbnailImage = ProtocolHelper.ByteArrayToByteString(new byte[] { 0, 1, 2 }); initOk = await PerformInitializationProcessWithUpdateItemsAsync(updateItems, profileServer, locServer, "2.add.thumbnailImage"); bool step12Ok = initOk; log.Trace("Step 12: {0}", step12Ok ? "PASSED" : "FAILED"); // Step 13 log.Trace("Step 13"); updateItems = new List <SharedProfileUpdateItem>(originalUpdateItems); updateItems[2] = new SharedProfileUpdateItem(updateItems[2]); updateItems[2].Add.Latitude = 987654321; initOk = await PerformInitializationProcessWithUpdateItemsAsync(updateItems, profileServer, locServer, "2.add.latitude"); bool step13Ok = initOk; log.Trace("Step 13: {0}", step13Ok ? "PASSED" : "FAILED"); // Step 14 log.Trace("Step 14"); updateItems = new List <SharedProfileUpdateItem>(originalUpdateItems); updateItems[2] = new SharedProfileUpdateItem(updateItems[2]); updateItems[2].Add.Longitude = 987654321; initOk = await PerformInitializationProcessWithUpdateItemsAsync(updateItems, profileServer, locServer, "2.add.longitude"); bool step14Ok = initOk; log.Trace("Step 14: {0}", step14Ok ? "PASSED" : "FAILED"); // Step 15 log.Trace("Step 15"); updateItems = new List <SharedProfileUpdateItem>(originalUpdateItems); updateItems[2] = new SharedProfileUpdateItem(updateItems[2]); updateItems[2].Add.ExtraData = new string('a', 270); initOk = await PerformInitializationProcessWithUpdateItemsAsync(updateItems, profileServer, locServer, "2.add.extraData"); bool step15Ok = initOk; log.Trace("Step 15: {0}", step15Ok ? "PASSED" : "FAILED"); // Step 16 log.Trace("Step 16"); updateItems = new List <SharedProfileUpdateItem>(originalUpdateItems); updateItems[2] = new SharedProfileUpdateItem(updateItems[2]); updateItems[2].Add.ExtraData = new string('ɐ', 150); initOk = await PerformInitializationProcessWithUpdateItemsAsync(updateItems, profileServer, locServer, "2.add.extraData"); bool step16Ok = initOk; log.Trace("Step 16: {0}", step16Ok ? "PASSED" : "FAILED"); // Step 17 log.Trace("Step 17"); updateItems = new List <SharedProfileUpdateItem>(originalUpdateItems); updateItems[2] = new SharedProfileUpdateItem() { Change = new SharedProfileChangeItem() { IdentityNetworkId = ProtocolHelper.ByteArrayToByteString(Crypto.Sha256(updateItems[0].Add.IdentityPublicKey.ToByteArray())) } }; initOk = await PerformInitializationProcessWithUpdateItemsAsync(updateItems, profileServer, locServer, "2.actionType"); bool step17Ok = initOk; log.Trace("Step 17: {0}", step17Ok ? "PASSED" : "FAILED"); // Step 18 log.Trace("Step 18"); updateItems = new List <SharedProfileUpdateItem>(originalUpdateItems); updateItems[2] = new SharedProfileUpdateItem() { Delete = new SharedProfileDeleteItem() { IdentityNetworkId = ProtocolHelper.ByteArrayToByteString(Crypto.Sha256(updateItems[0].Add.IdentityPublicKey.ToByteArray())) } }; initOk = await PerformInitializationProcessWithUpdateItemsAsync(updateItems, profileServer, locServer, "2.actionType"); bool step18Ok = initOk; log.Trace("Step 18: {0}", step18Ok ? "PASSED" : "FAILED"); // Step 19 log.Trace("Step 19"); updateItems = new List <SharedProfileUpdateItem>(originalUpdateItems); updateItems[2] = new SharedProfileUpdateItem(updateItems[2]); updateItems[2].Add.Name = ""; initOk = await PerformInitializationProcessWithUpdateItemsAsync(updateItems, profileServer, locServer, "2.add.name"); bool step19Ok = initOk; log.Trace("Step 19: {0}", step19Ok ? "PASSED" : "FAILED"); Passed = step1Ok && step2Ok && step3Ok && step4Ok && step5Ok && step6Ok && step7Ok && step8Ok && step9Ok && step10Ok && step11Ok && step12Ok && step13Ok && step14Ok && step15Ok && step16Ok && step17Ok && step18Ok && step19Ok; res = true; } catch (Exception e) { log.Error("Exception occurred: {0}", e.ToString()); } client.Dispose(); if (profileServer != null) { profileServer.Shutdown(); } if (locServer != null) { locServer.Shutdown(); } log.Trace("(-):{0}", res); return(res); }
/// <summary> /// Processes PingRequest message from client. /// <para>Simply copies the payload to a new ping response message.</para> /// </summary> /// <param name="Client">Client that sent the request.</param> /// <param name="RequestMessage">Full request message.</param> /// <returns>Response message to be sent to the client.</returns> public Message ProcessMessagePingRequest(IncomingClient Client, Message RequestMessage) { log.Trace("()"); MessageBuilder messageBuilder = Client.MessageBuilder; PingRequest pingRequest = RequestMessage.Request.SingleRequest.Ping; Message res = messageBuilder.CreatePingResponse(RequestMessage, pingRequest.Payload.ToByteArray(), ProtocolHelper.GetUnixTimestampMs()); log.Trace("(-):*.Response.Status={0}", res.Response.Status); return(res); }
/// <summary> /// Proxies the incoming request to the destination server, and the response back to the client. /// </summary> /// <remarks> /// In what follows, as well as throughout in Reverse Proxy, we consider /// the following picture as illustrative of the Proxy. /// <code> /// +-------------------+ /// | Destination + /// +-------------------+ /// ▲ | /// (b) | | (c) /// | ▼ /// +-------------------+ /// | Proxy + /// +-------------------+ /// ▲ | /// (a) | | (d) /// | ▼ /// +-------------------+ /// | Client + /// +-------------------+ /// </code> /// /// (a) and (b) show the *request* path, going from the client to the target. /// (c) and (d) show the *response* path, going from the destination back to the client. /// /// Normal proxying comprises the following steps: /// (0) Disable ASP .NET Core limits for streaming requests /// (1) Create outgoing HttpRequestMessage /// (2) Setup copy of request body (background) Client --► Proxy --► Destination /// (3) Copy request headers Client --► Proxy --► Destination /// (4) Send the outgoing request using HttpMessageInvoker Client --► Proxy --► Destination /// (5) Copy response status line Client ◄-- Proxy ◄-- Destination /// (6) Copy response headers Client ◄-- Proxy ◄-- Destination /// (7-A) Check for a 101 upgrade response, this takes care of WebSockets as well as any other upgradeable protocol. /// (7-A-1) Upgrade client channel Client ◄--- Proxy ◄--- Destination /// (7-A-2) Copy duplex streams and return Client ◄--► Proxy ◄--► Destination /// (7-B) Copy (normal) response body Client ◄-- Proxy ◄-- Destination /// (8) Copy response trailer headers and finish response Client ◄-- Proxy ◄-- Destination /// (9) Wait for completion of step 2: copying request body Client --► Proxy --► Destination /// /// ASP .NET Core (Kestrel) will finally send response trailers (if any) /// after we complete the steps above and relinquish control. /// </remarks> public async ValueTask <ForwarderError> SendAsync( HttpContext context, string destinationPrefix, HttpMessageInvoker httpClient, ForwarderRequestConfig requestConfig, HttpTransformer transformer) { _ = context ?? throw new ArgumentNullException(nameof(context)); _ = destinationPrefix ?? throw new ArgumentNullException(nameof(destinationPrefix)); _ = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _ = requestConfig ?? throw new ArgumentNullException(nameof(requestConfig)); _ = transformer ?? throw new ArgumentNullException(nameof(transformer)); // HttpClient overload for SendAsync changes response behavior to fully buffered which impacts performance // See discussion in https://github.com/microsoft/reverse-proxy/issues/458 if (httpClient is HttpClient) { throw new ArgumentException($"The http client must be of type HttpMessageInvoker, not HttpClient", nameof(httpClient)); } ForwarderTelemetry.Log.ForwarderStart(destinationPrefix); var activityCancellationSource = ActivityCancellationTokenSource.Rent(requestConfig?.ActivityTimeout ?? DefaultTimeout, context.RequestAborted); try { var isClientHttp2OrGreater = ProtocolHelper.IsHttp2OrGreater(context.Request.Protocol); // NOTE: We heuristically assume gRPC-looking requests may require streaming semantics. // See https://github.com/microsoft/reverse-proxy/issues/118 for design discussion. var isStreamingRequest = isClientHttp2OrGreater && ProtocolHelper.IsGrpcContentType(context.Request.ContentType); // :: Step 1-3: Create outgoing HttpRequestMessage var(destinationRequest, requestContent) = await CreateRequestMessageAsync( context, destinationPrefix, transformer, requestConfig, isStreamingRequest, activityCancellationSource); // :: Step 4: Send the outgoing request using HttpClient HttpResponseMessage destinationResponse; try { ForwarderTelemetry.Log.ForwarderStage(ForwarderStage.SendAsyncStart); destinationResponse = await httpClient.SendAsync(destinationRequest, activityCancellationSource.Token); ForwarderTelemetry.Log.ForwarderStage(ForwarderStage.SendAsyncStop); // Reset the timeout since we received the response headers. activityCancellationSource.ResetTimeout(); } catch (Exception requestException) { return(await HandleRequestFailureAsync(context, requestContent, requestException, transformer, activityCancellationSource)); } Log.ResponseReceived(_logger, destinationResponse); try { // :: Step 5: Copy response status line Client ◄-- Proxy ◄-- Destination // :: Step 6: Copy response headers Client ◄-- Proxy ◄-- Destination var copyBody = await CopyResponseStatusAndHeadersAsync(destinationResponse, context, transformer); if (!copyBody) { // The transforms callback decided that the response body should be discarded. destinationResponse.Dispose(); if (requestContent is not null && requestContent.InProgress) { activityCancellationSource.Cancel(); await requestContent.ConsumptionTask; } return(ForwarderError.None); } } catch (Exception ex) { destinationResponse.Dispose(); if (requestContent is not null && requestContent.InProgress) { activityCancellationSource.Cancel(); await requestContent.ConsumptionTask; } ReportProxyError(context, ForwarderError.ResponseHeaders, ex); // Clear the response since status code, reason and some headers might have already been copied and we want clean 502 response. context.Response.Clear(); context.Response.StatusCode = StatusCodes.Status502BadGateway; return(ForwarderError.ResponseHeaders); } // :: Step 7-A: Check for a 101 upgrade response, this takes care of WebSockets as well as any other upgradeable protocol. if (destinationResponse.StatusCode == HttpStatusCode.SwitchingProtocols) { Debug.Assert(requestContent?.Started != true); return(await HandleUpgradedResponse(context, destinationResponse, activityCancellationSource)); } // NOTE: it may *seem* wise to call `context.Response.StartAsync()` at this point // since it looks like we are ready to send back response headers // (and this might help reduce extra delays while we wait to receive the body from the destination). // HOWEVER, this would produce the wrong result if it turns out that there is no content // from the destination -- instead of sending headers and terminating the stream at once, // we would send headers thinking a body may be coming, and there is none. // This is problematic on gRPC connections when the destination server encounters an error, // in which case it immediately returns the response headers and trailing headers, but no content, // and clients misbehave if the initial headers response does not indicate stream end. // :: Step 7-B: Copy response body Client ◄-- Proxy ◄-- Destination var(responseBodyCopyResult, responseBodyException) = await CopyResponseBodyAsync(destinationResponse.Content, context.Response.Body, activityCancellationSource); if (responseBodyCopyResult != StreamCopyResult.Success) { return(await HandleResponseBodyErrorAsync(context, requestContent, responseBodyCopyResult, responseBodyException !, activityCancellationSource)); } // :: Step 8: Copy response trailer headers and finish response Client ◄-- Proxy ◄-- Destination await CopyResponseTrailingHeadersAsync(destinationResponse, context, transformer); if (isStreamingRequest) { // NOTE: We must call `CompleteAsync` so that Kestrel will flush all bytes to the client. // In the case where there was no response body, // this is also when headers and trailing headers are sent to the client. // Without this, the client might wait forever waiting for response bytes, // while we might wait forever waiting for request bytes, // leading to a stuck connection and no way to make progress. await context.Response.CompleteAsync(); } // :: Step 9: Wait for completion of step 2: copying request body Client --► Proxy --► Destination // NOTE: It is possible for the request body to NOT be copied even when there was an incoming requet body, // e.g. when the request includes header `Expect: 100-continue` and the destination produced a non-1xx response. // We must only wait for the request body to complete if it actually started, // otherwise we run the risk of waiting indefinitely for a task that will never complete. if (requestContent is not null && requestContent.Started) { var(requestBodyCopyResult, requestBodyException) = await requestContent.ConsumptionTask; if (requestBodyCopyResult != StreamCopyResult.Success) { // The response succeeded. If there was a request body error then it was probably because the client or destination decided // to cancel it. Report as low severity. var error = requestBodyCopyResult switch { StreamCopyResult.InputError => ForwarderError.RequestBodyClient, StreamCopyResult.OutputError => ForwarderError.RequestBodyDestination, StreamCopyResult.Canceled => ForwarderError.RequestBodyCanceled, _ => throw new NotImplementedException(requestBodyCopyResult.ToString()) }; ReportProxyError(context, error, requestBodyException !); return(error); } } } finally { activityCancellationSource.Return(); ForwarderTelemetry.Log.ForwarderStop(context.Response.StatusCode); } return(ForwarderError.None); }
private StreamCopyHttpContent?SetupRequestBodyCopy(HttpRequest request, bool isStreamingRequest, ActivityCancellationTokenSource activityToken) { // 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; #if NET var canHaveBodyFeature = request.HttpContext.Features.Get <IHttpRequestBodyDetectionFeature>(); if (canHaveBodyFeature is not null) { // 5.0 servers provide a definitive answer for us. hasBody = canHaveBodyFeature.CanHaveBody; } else #endif // 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 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-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. // https://tools.ietf.org/html/rfc7231#section-4.3.8 // A client MUST NOT send a message body in a TRACE request. else if (HttpMethods.IsGet(method) || HttpMethods.IsHead(method) || HttpMethods.IsDelete(method) || HttpMethods.IsConnect(method) || HttpMethods.IsTrace(method)) { hasBody = false; } // else hasBody defaults to true if (hasBody) { if (isStreamingRequest) { DisableMinRequestBodyDataRateAndMaxRequestBodySize(request.HttpContext); } // 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. return(new StreamCopyHttpContent( request: request, autoFlushHttpClientOutgoingStream: isStreamingRequest, clock: _clock, activityToken)); } return(null); }