/// <summary> /// Handles the HTTP request received over the hybrid connection. /// </summary> /// <param name="context">Relayed request context.</param> /// <returns>Task tracking operation.</returns> private async Task HandleProxyRequestAsync(RelayedHttpListenerContext context) { RelayRequest relayRequest = new RelayRequest { Headers = context.Request.Headers, HttpMethod = new HttpMethod(context.Request.HttpMethod), InputStream = context.Request.InputStream, RequestPathAndQuery = context.Request.Url.AbsoluteUri.Substring(this.relayUrl.Length), RequestStartDateTime = DateTimeOffset.Now, }; try { RelayResponse relayResponse = await this.relayManager.HandleRelayRequestAsync(relayRequest).ConfigureAwait(false); context.Response.StatusCode = relayResponse.HttpStatusCode; context.Response.StatusDescription = relayResponse.StatusDescription; // Copy over outgoing headers. foreach (string headerName in relayResponse.Headers.Keys) { // Not copying over the Transfer-Encoding header as the communication between server and Relay, and Relay and client are // different connections. if (headerName.Equals("Transfer-Encoding", StringComparison.OrdinalIgnoreCase)) { continue; } context.Response.Headers[headerName] = relayResponse.Headers[headerName]; } if (relayResponse.OutputStream != null) { await relayResponse.OutputStream.CopyToAsync(context.Response.OutputStream).ConfigureAwait(false); } await context.Response.CloseAsync().ConfigureAwait(false); } catch (Exception ex) { context.Response.StatusCode = HttpStatusCode.InternalServerError; context.Response.StatusDescription = "Exception occurred at Server"; byte[] errorMessage = Encoding.UTF8.GetBytes(ex.Message); await context.Response.OutputStream.WriteAsync(errorMessage, 0, errorMessage.Length).ConfigureAwait(false); } }
/// <summary> /// Converts <see cref="RelayRequest"/> to <see cref="HttpRequestMessage"/>. /// </summary> /// <param name="relayRequest">Incoming relay request.</param> /// <param name="requestId">Request Id.</param> /// <returns>Http request message.</returns> private async Task <HttpRequestMessage> ToHttpRequestMessageAsync(RelayRequest relayRequest, string requestId) { Uri internalRequestUrl = new Uri(this.internalServiceUrl + "/" + relayRequest.RequestPathAndQuery.TrimStart('/')); HttpRequestMessage httpRequestMessage = new HttpRequestMessage(relayRequest.HttpMethod, internalRequestUrl); // Prepare request content. // Caveat here! .NetFX uses WebRequest behind HttpClient so it does not allow body content in GET requests // but NetCore uses an entirely different stack and thus allows body in GET. // Once ADAL Library starts allowing UI based auth in NetCore we can get rid of NetFX altogether and allow users // to pass request body in GET too. if (relayRequest.InputStream != null && relayRequest.HttpMethod != HttpMethod.Get && relayRequest.HttpMethod != HttpMethod.Head) { MemoryStream memoryStream = new MemoryStream((int)relayRequest.InputStream.Length); await relayRequest.InputStream.CopyToAsync(memoryStream).ConfigureAwait(false); httpRequestMessage.Content = new StreamContent(memoryStream); memoryStream.Seek(0, SeekOrigin.Begin); httpRequestMessage.Content.Headers.ContentLength = memoryStream.Length; RelayRequestManager.CopyContentHeader(httpRequestMessage.Content, relayRequest.Headers); if (httpRequestMessage.Content.Headers.ContentLength != memoryStream.Length) { this.logger.LogWarning( "Content-Length header mismatch for request Id '{0}'. Value from request '{1}'. Value from actual content stream '{2}'", requestId, httpRequestMessage.Content.Headers.ContentLength.GetValueOrDefault(), memoryStream.Length); } } // Try to blindly add the headers. Content headers will get filtered out here. foreach (string headerName in relayRequest.Headers.Keys) { httpRequestMessage.Headers.TryAddWithoutValidation(headerName, relayRequest.Headers[headerName]); } // Set the correct host header. httpRequestMessage.Headers.Host = internalRequestUrl.Host; return(httpRequestMessage); }
/// <summary> /// Handles the incoming request, passes it down to the internal service and returns the response. /// </summary> /// <param name="relayRequest">Incoming relay request.</param> /// <returns>Response from the internal service.</returns> public async Task <RelayResponse> HandleRelayRequestAsync(RelayRequest relayRequest) { if (relayRequest is null) { throw new ArgumentNullException(nameof(relayRequest)); } string requestId = Guid.NewGuid().ToString(); this.logger.LogTrace("Received request with Id '{0}'", requestId); // Inform listener that a request has been received. if (this.relayRequestEventListener != null) { RelayRequest clonedRequest = relayRequest.Clone() as RelayRequest; #pragma warning disable CS4014 // We want eventing pipeline to run in parallel and not block the actual request execution. Task.Run(async() => { try { await this.relayRequestEventListener.RequestReceivedAsync(requestId, clonedRequest).ConfigureAwait(false); } catch (Exception ex) { // Ignoring exceptions from listeners. this.logger.LogWarning("Relay request event listener failed for request Id '{0}' during 'RequestReceivedAsync' phase with exception '{1}'. Ignoring.", requestId, ex); } }); #pragma warning restore CS4014 // We want eventing pipeline to run in parallel and not block the actual request execution. } HttpRequestMessage httpRequestMessage = null; try { httpRequestMessage = await this.ToHttpRequestMessageAsync(relayRequest, requestId).ConfigureAwait(false); } catch (Exception ex) { this.logger.LogError("CRITICAL ERROR!!! Failed to convert Relay request into outgoing request. Error - '{0}'. Request Id - '{1}'", ex, requestId); throw; } // Pass the request through all the plugins. foreach (ITunnelRelayPlugin tunnelRelayPlugin in this.tunnelRelayPlugins) { try { httpRequestMessage = await tunnelRelayPlugin.PreProcessRequestToServiceAsync(httpRequestMessage).ConfigureAwait(false); } catch (Exception ex) { this.logger.LogError("CRITICAL ERROR!!! Plugin '{0}' failed with error - '{1}' for request Id '{2}' during 'PreProcessRequestToServiceAsync' phase", tunnelRelayPlugin.PluginName, ex, requestId); throw; } } HttpResponseMessage httpResponseMessage; try { this.logger.LogTrace("Making external request to server for request Id '{0}'", requestId); httpResponseMessage = await this.httpClient.SendAsync(httpRequestMessage).ConfigureAwait(false); this.logger.LogTrace("Received response from server for request Id '{0}'", requestId); } catch (HttpRequestException httpException) { this.logger.LogError("Hit exception while sending request to server for request Id '{0}'. Error '{1}'", requestId, httpException); httpResponseMessage = new HttpResponseMessage(HttpStatusCode.BadGateway) { Content = new StringContent(httpException.ToString()), }; } catch (Exception ex) { this.logger.LogError("Hit critical exception while processing request '{0}'. Error '{1}'", requestId, ex); throw; } // Pass the response through all the plugins. foreach (ITunnelRelayPlugin tunnelRelayPlugin in this.tunnelRelayPlugins) { try { httpResponseMessage = await tunnelRelayPlugin.PostProcessResponseFromServiceAsync(httpResponseMessage).ConfigureAwait(false); } catch (Exception ex) { this.logger.LogError("CRITICAL ERROR!!! Plugin '{0}' failed with error - '{1}' for request Id '{2}' during 'PostProcessResponseFromServiceAsync' phase", tunnelRelayPlugin.PluginName, ex, requestId); throw; } } RelayResponse relayResponse = null; try { relayResponse = await this.ToRelayResponseAsync(httpResponseMessage).ConfigureAwait(false); } catch (Exception ex) { this.logger.LogError("Failed to convert outgoing response into relay response for request '{0}'. Error '{1}'", requestId, ex); } if (this.relayRequestEventListener != null) { RelayResponse clonedResponse = relayResponse.Clone() as RelayResponse; #pragma warning disable CS4014 // We want eventing pipeline to run in parallel and not slowdown the actual request execution. Task.Run(async() => { try { await this.relayRequestEventListener.ResponseSentAsync(requestId, clonedResponse).ConfigureAwait(false); } catch (Exception ex) { // Ignoring exceptions from listeners. this.logger.LogWarning("Relay request event listener failed for request Id '{0}' during 'ResponseSentAsync' phase with exception '{1}'. Ignoring.", requestId, ex); } }); #pragma warning restore CS4014 // We want eventing pipeline to run in parallel and not slowdown the actual request execution. } return(relayResponse); }