public void SendAsync_Null_ThrowsArgumentNullException() { using (var invoker = new HttpMessageInvoker(new MockHandler())) { Assert.Throws<ArgumentNullException>(() => { Task t = invoker.SendAsync(null, CancellationToken.None); }); } }
public async Task SendAsync_Request_HandlerInvoked() { var handler = new MockHandler(); var invoker = new HttpMessageInvoker(handler); HttpResponseMessage response = await invoker.SendAsync(new HttpRequestMessage(), CancellationToken.None); Assert.NotNull(response); Assert.Equal(1, handler.SendAsyncCount); }
public void Dispose_DontDisposeHandler_HandlerNotDisposed() { var handler = new MockHandler(); var invoker = new HttpMessageInvoker(handler, false); invoker.Dispose(); Assert.Equal(0, handler.DisposeCount); Assert.Throws<ObjectDisposedException>(() => { Task t = invoker.SendAsync(new HttpRequestMessage(), CancellationToken.None); }); Assert.Equal(0, handler.SendAsyncCount); }
public static HttpResponseMessage Send(WinHttpHandler handler, Action setup, string fakeServerEndpoint) { TestServer.SetResponse(DecompressionMethods.None, TestServer.ExpectedResponseBody); setup(); var invoker = new HttpMessageInvoker(handler, false); var request = new HttpRequestMessage(HttpMethod.Get, fakeServerEndpoint); Task<HttpResponseMessage> task = invoker.SendAsync(request, CancellationToken.None); return task.GetAwaiter().GetResult(); }
/// <summary> /// Sends a CIBA backchannel authentication request /// </summary> /// <param name="client">The client.</param> /// <param name="request">The request.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns></returns> public static async Task <BackchannelAuthenticationResponse> RequestBackchannelAuthenticationAsync(this HttpMessageInvoker client, BackchannelAuthenticationRequest request, CancellationToken cancellationToken = default) { var clone = request.Clone(); if (request.RequestObject.IsPresent()) { clone.Parameters.AddOptional(OidcConstants.BackchannelAuthenticationRequest.Request, request.RequestObject); } else { clone.Parameters.AddRequired(OidcConstants.AuthorizeRequest.Scope, request.Scope); clone.Parameters.AddOptional(OidcConstants.BackchannelAuthenticationRequest.ClientNotificationToken, request.ClientNotificationToken); clone.Parameters.AddOptional(OidcConstants.BackchannelAuthenticationRequest.AcrValues, request.AcrValues); clone.Parameters.AddOptional(OidcConstants.BackchannelAuthenticationRequest.LoginHintToken, request.LoginHintToken); clone.Parameters.AddOptional(OidcConstants.BackchannelAuthenticationRequest.LoginHint, request.LoginHint); clone.Parameters.AddOptional(OidcConstants.BackchannelAuthenticationRequest.IdTokenHint, request.IdTokenHint); clone.Parameters.AddOptional(OidcConstants.BackchannelAuthenticationRequest.BindingMessage, request.BindingMessage); clone.Parameters.AddOptional(OidcConstants.BackchannelAuthenticationRequest.UserCode, request.UserCode); if (request.RequestedExpiry.HasValue) { clone.Parameters.AddOptional(OidcConstants.BackchannelAuthenticationRequest.RequestedExpiry, request.RequestedExpiry.ToString()); } foreach (var resource in request.Resource) { clone.Parameters.AddRequired(OidcConstants.TokenRequest.Resource, resource, allowDuplicates: true); } } clone.Method = HttpMethod.Post; clone.Prepare(); HttpResponseMessage response; try { response = await client.SendAsync(clone, cancellationToken).ConfigureAwait(); } catch (Exception ex) { return(ProtocolResponse.FromException <BackchannelAuthenticationResponse>(ex)); } return(await ProtocolResponse.FromHttpResponseAsync <BackchannelAuthenticationResponse>(response).ConfigureAwait()); }
public DelegatedHttpMessageHandler(HttpMessageInvoker @delegate) { _delegate = @delegate; }
public DiscogsApiEnvironment(HttpMessageInvoker httpMessageInvoker) { HttpMessageInvoker = httpMessageInvoker ?? throw new ArgumentNullException(nameof(httpMessageInvoker)); }
public void Setup() { _serverCert = Test.Common.Configuration.Certificates.GetServerCertificate(); _listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); _listener.Bind(new IPEndPoint(IPAddress.Loopback, 0)); _listener.Listen(int.MaxValue); string responseText = "HTTP/1.1 200 OK\r\n" + (chunkedResponse ? $"Transfer-Encoding: chunked\r\n\r\n{responseLength.ToString("X")}\r\n{new string('a', responseLength)}\r\n0\r\n\r\n" : $"Content-Length: {responseLength}\r\n\r\n{new string('a', responseLength)}"); ReadOnlyMemory <byte> responseBytes = Encoding.UTF8.GetBytes(responseText); _serverTask = Task.Run(async() => { try { while (true) { using (Socket s = await _listener.AcceptAsync()) { try { Stream stream = new NetworkStream(s); if (ssl) { var sslStream = new SslStream(stream, false, delegate { return(true); }); await sslStream.AuthenticateAsServerAsync(_serverCert, false, SslProtocols.None, false); stream = sslStream; } using (var reader = new StreamReader(stream, Encoding.ASCII, detectEncodingFromByteOrderMarks: false, bufferSize: 100)) { while (true) { while (!string.IsNullOrEmpty(await reader.ReadLineAsync())) { ; } await stream.WriteAsync(responseBytes); } } } catch (SocketException e) when(e.SocketErrorCode == SocketError.ConnectionAborted) { } } } } catch { } }); var ep = (IPEndPoint)_listener.LocalEndPoint; var uri = new Uri($"http{(ssl ? "s" : "")}://{ep.Address}:{ep.Port}/"); _handler = new SocketsHttpHandler(); _invoker = new HttpMessageInvoker(_handler); if (ssl) { _handler.SslOptions.RemoteCertificateValidationCallback = delegate { return(true); }; } _request = new HttpRequestMessage(HttpMethod.Get, uri); }
public void Setup() { this.testHttpMessageHandler = new MockRedirectHandler(); this.retryHandler = new RetryHandler(this.testHttpMessageHandler); this.invoker = new HttpMessageInvoker(this.retryHandler); }
/// <summary> /// Initializes a new instance of the <see cref="HttpResourceDownloader"/> class. /// </summary> /// <param name="messageInvoker">An object used to send server requests.</param> /// <param name="networkStatus">An object that provides the current network status.</param> public HttpResourceDownloader(HttpMessageInvoker messageInvoker, INetworkStatus networkStatus) : base(messageInvoker, networkStatus) { }
/// <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, Uri targetUri, HttpMessageInvoker httpClient, ProxyTelemetryContext proxyTelemetryContext, CancellationToken shortCancellation, CancellationToken longCancellation) { Contracts.CheckValue(context, nameof(context)); Contracts.CheckValue(upgradeFeature, nameof(upgradeFeature)); Contracts.CheckValue(targetUri, nameof(targetUri)); Contracts.CheckValue(httpClient, nameof(httpClient)); // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 1: Create outgoing HttpRequestMessage var upstreamRequest = new HttpRequestMessage(HttpUtilities.GetHttpMethod(context.Request.Method), targetUri) { // Default to HTTP/1.1 for proxying upgradable requests. This is already the default as of .NET Core 3.1 Version = new Version(1, 1), }; // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 2: Copy request headers Downstream --► Proxy --► Upstream CopyHeadersToUpstream(context.Request.Headers, 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.Response.Headers); 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, proxyTelemetryContext, 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( _metrics, new StreamCopyTelemetryContext( direction: "upstream", backendId: proxyTelemetryContext.BackendId, routeId: proxyTelemetryContext.RouteId, endpointId: proxyTelemetryContext.EndpointId)); var upstreamTask = upstreamCopier.CopyAsync(downstreamStream, upstreamStream, longCancellation); var downstreamCopier = new StreamCopier( _metrics, new StreamCopyTelemetryContext( direction: "downstream", backendId: proxyTelemetryContext.BackendId, routeId: proxyTelemetryContext.RouteId, endpointId: proxyTelemetryContext.EndpointId)); var downstreamTask = downstreamCopier.CopyAsync(upstreamStream, downstreamStream, longCancellation); await Task.WhenAll(upstreamTask, downstreamTask); }
public NationalCloudHandlerTests() { this._fakeSuccessHandler = new FakeSuccessHandler(); this._nationalCloudHandler = new NationalCloudHandler(new ODataQueryOptionsHandler(_fakeSuccessHandler)); this._invoker = new HttpMessageInvoker(_nationalCloudHandler); }
/// <summary> /// Initializes a new instance of the <see cref="ServerHttpMessageHandler" /> class. /// </summary> /// <param name="httpMessageInvoker">HTTP message invoker to process the request.</param> /// <param name="disposeInvoker">Indicates whether the invoker should be disposed after request processing.</param> public ServerHttpMessageHandler(HttpMessageInvoker httpMessageInvoker, bool disposeInvoker) { this.httpMessageInvoker = httpMessageInvoker; this.disposeInvoker = disposeInvoker; }
/// <summary> /// Send a dynamic registration request. /// </summary> /// <param name="client">The client.</param> /// <param name="request">The request.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns></returns> public static async Task <DynamicClientRegistrationResponse> RegisterClientAsync(this HttpMessageInvoker client, DynamicClientRegistrationRequest request, CancellationToken cancellationToken = default) { var clone = request.Clone(); clone.Method = HttpMethod.Post; clone.Content = new StringContent(JsonConvert.SerializeObject(request.Document), Encoding.UTF8, "application/json"); clone.Prepare(); if (request.Token.IsPresent()) { clone.SetBearerToken(request.Token); } HttpResponseMessage response; try { response = await client.SendAsync(clone, cancellationToken).ConfigureAwait(); } catch (Exception ex) { return(ProtocolResponse.FromException <DynamicClientRegistrationResponse>(ex)); } return(await ProtocolResponse.FromHttpResponseAsync <DynamicClientRegistrationResponse>(response).ConfigureAwait()); }
public TopicController(IRepository <Topic> topicRepo, ITopicService topicService, ILogger <TopicController> logger, IOptions <ChatyOptions> chatyOptions, IChatHistoryImporter chatHistoryImporter, HttpMessageInvoker httpClient, IRepository <Reply> replyRepo, IRepository <WeChatAccount> wechatAccountRepo) { _topicRepo = topicRepo; _topicService = topicService; _logger = logger; _chatyOptions = chatyOptions?.Value; _chatHistoryImporter = chatHistoryImporter; _httpClient = httpClient; _replyRepo = replyRepo; _wechatAccountRepo = wechatAccountRepo; }
/// <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 Task ProxyAsync( HttpContext context, string destinationPrefix, HttpMessageInvoker httpClient, Transforms transforms, RequestProxyOptions requestOptions) { _ = context ?? throw new ArgumentNullException(nameof(context)); _ = destinationPrefix ?? throw new ArgumentNullException(nameof(destinationPrefix)); _ = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); transforms ??= Transforms.Empty; // 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)); } ProxyTelemetry.Log.ProxyStart(destinationPrefix); try { var requestAborted = context.RequestAborted; // :: Step 1: Create outgoing HttpRequestMessage var destinationRequest = CreateRequestMessage(context, destinationPrefix, transforms.RequestTransforms, requestOptions); var isClientHttp2 = 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 = isClientHttp2 && ProtocolHelper.IsGrpcContentType(context.Request.ContentType); // :: Step 2: Setup copy of request body (background) Client --► Proxy --► Destination // 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 requestContent = SetupRequestBodyCopy(context.Request, destinationRequest, isStreamingRequest, requestAborted); // :: Step 3: Copy request headers Client --► Proxy --► Destination CopyRequestHeaders(context, destinationRequest, transforms); // :: Step 4: Send the outgoing request using HttpClient HttpResponseMessage destinationResponse; var requestTimeoutSource = CancellationTokenSource.CreateLinkedTokenSource(requestAborted); requestTimeoutSource.CancelAfter(requestOptions.Timeout ?? DefaultTimeout); var requestTimeoutToken = requestTimeoutSource.Token; try { ProxyTelemetry.Log.ProxyStage(ProxyStage.SendAsyncStart); destinationResponse = await httpClient.SendAsync(destinationRequest, requestTimeoutToken); ProxyTelemetry.Log.ProxyStage(ProxyStage.SendAsyncStop); } catch (OperationCanceledException canceledException) { if (!requestAborted.IsCancellationRequested && requestTimeoutToken.IsCancellationRequested) { ReportProxyError(context, ProxyError.RequestTimedOut, canceledException); context.Response.StatusCode = StatusCodes.Status504GatewayTimeout; return; } ReportProxyError(context, ProxyError.RequestCanceled, canceledException); context.Response.StatusCode = StatusCodes.Status502BadGateway; return; } catch (Exception requestException) { await HandleRequestFailureAsync(context, requestContent, requestException); return; } finally { requestTimeoutSource.Dispose(); } // Detect connection downgrade, which may be problematic for e.g. gRPC. if (isClientHttp2 && destinationResponse.Version.Major != 2) { // TODO: Do something on connection downgrade... Log.HttpDowngradeDetected(_logger); } // Assert that, if we are proxying content to the destination, 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 (requestContent != null && !requestContent.Started) { // TODO: HttpClient might not need to read the body in some scenarios, such as an early auth failure with Expect: 100-continue. throw new InvalidOperationException("Proxying the Client request body to the Destination server hasn't started. This is a coding defect."); } // :: Step 5: Copy response status line Client ◄-- Proxy ◄-- Destination // :: Step 6: Copy response headers Client ◄-- Proxy ◄-- Destination CopyResponseStatusAndHeaders(destinationResponse, context, transforms.ResponseHeaderTransforms); // :: 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) { await HandleUpgradedResponse(context, destinationResponse, requestAborted); return; } // 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, requestAborted); if (responseBodyCopyResult != StreamCopyResult.Success) { await HandleResponseBodyErrorAsync(context, requestContent, responseBodyCopyResult, responseBodyException); return; } // :: Step 8: Copy response trailer headers and finish response Client ◄-- Proxy ◄-- Destination CopyResponseTrailingHeaders(destinationResponse, 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 Client --► Proxy --► Destination if (requestContent != null) { 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 => ProxyError.RequestBodyClient, StreamCopyResult.OutputError => ProxyError.RequestBodyDestination, StreamCopyResult.Canceled => ProxyError.RequestBodyCanceled, _ => throw new NotImplementedException(requestBodyCopyResult.ToString()) }; ReportProxyError(context, error, requestBodyException); } } } finally { ProxyTelemetry.Log.ProxyStop(context.Response.StatusCode); } }
public MovieDetailsTests(OmdbClassFixture omdbClassFixture) { _client = omdbClassFixture.Client; _omdbSettings = omdbClassFixture.OmdbSettings; }
public void EventSource_SuccessfulRequest_LogsStartStop(string testMethod) { if (UseVersion.Major != 1 && !testMethod.EndsWith("Async")) { // Synchronous requests are only supported for HTTP/1.1 return; } RemoteExecutor.Invoke(async(useVersionString, testMethod) => { const int ResponseContentLength = 42; Version version = Version.Parse(useVersionString); using var listener = new TestEventListener("System.Net.Http", EventLevel.Verbose, eventCounterInterval: 0.1d); bool buffersResponse = false; var events = new ConcurrentQueue <EventWrittenEventArgs>(); await listener.RunWithCallbackAsync(events.Enqueue, async() => { await GetFactoryForVersion(version).CreateClientAndServerAsync( async uri => { using HttpClientHandler handler = CreateHttpClientHandler(useVersionString); using HttpClient client = CreateHttpClient(handler, useVersionString); using var invoker = new HttpMessageInvoker(handler); var request = new HttpRequestMessage(HttpMethod.Get, uri) { Version = version }; switch (testMethod) { case "GetAsync": { buffersResponse = true; await client.GetAsync(uri); } break; case "Send": { buffersResponse = true; await Task.Run(() => client.Send(request)); } break; case "UnbufferedSend": { buffersResponse = false; HttpResponseMessage response = await Task.Run(() => client.Send(request, HttpCompletionOption.ResponseHeadersRead)); response.Content.CopyTo(Stream.Null, null, default); } break; case "SendAsync": { buffersResponse = true; await client.SendAsync(request); } break; case "UnbufferedSendAsync": { buffersResponse = false; HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); await response.Content.CopyToAsync(Stream.Null); } break; case "GetStringAsync": { buffersResponse = true; await client.GetStringAsync(uri); } break; case "GetByteArrayAsync": { buffersResponse = true; await client.GetByteArrayAsync(uri); } break; case "GetStreamAsync": { buffersResponse = false; Stream responseStream = await client.GetStreamAsync(uri); await responseStream.CopyToAsync(Stream.Null); } break; case "InvokerSend": { buffersResponse = false; HttpResponseMessage response = await Task.Run(() => invoker.Send(request, cancellationToken: default)); await response.Content.CopyToAsync(Stream.Null); } break; case "InvokerSendAsync": { buffersResponse = false; HttpResponseMessage response = await invoker.SendAsync(request, cancellationToken: default); await response.Content.CopyToAsync(Stream.Null); } break; } }, async server => { await server.AcceptConnectionAsync(async connection => { await Task.Delay(300); await connection.ReadRequestDataAsync(); await connection.SendResponseAsync(content: new string('a', ResponseContentLength)); }); }); await Task.Delay(300); }); Assert.DoesNotContain(events, ev => ev.EventId == 0); // errors from the EventSource itself EventWrittenEventArgs start = Assert.Single(events, e => e.EventName == "RequestStart"); ValidateStartEventPayload(start); EventWrittenEventArgs stop = Assert.Single(events, e => e.EventName == "RequestStop"); Assert.Empty(stop.Payload); Assert.DoesNotContain(events, e => e.EventName == "RequestFailed"); ValidateConnectionEstablishedClosed(events, version); ValidateRequestResponseStartStopEvents( events, requestContentLength: null, responseContentLength: buffersResponse ? ResponseContentLength : null, count: 1); VerifyEventCounters(events, requestCount: 1, shouldHaveFailures: false); }, UseVersion.ToString(), testMethod).Dispose();
public async Task TestPostAndGet() { HttpConfiguration config = new HttpConfiguration(); config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute("DefaultApi", "api/{controller}/{id}", new { id = RouteParameter.Optional }); HttpServer server = new HttpServer(config); using (HttpMessageInvoker client = new HttpMessageInvoker(server)) { var data = new JobCreationRequest { Type = JobType.SCRAPE, Data = new Dictionary <string, string>() { { "url", @"https://www.eaze.com/" }, { "selector", "footer a" } } }; var postRequest = new HttpRequestMessage { RequestUri = new Uri("http://localhost:10820/api/Job"), Method = HttpMethod.Post, Content = new ObjectContent <JobCreationRequest>(data, new System.Net.Http.Formatting.JsonMediaTypeFormatter()) }; postRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); Guid requestId; using (HttpResponseMessage postResponse = client.SendAsync(postRequest, CancellationToken.None).Result) { requestId = postResponse.Content.ReadAsAsync <Guid>().Result; Assert.AreEqual(HttpStatusCode.Created, postResponse.StatusCode); } var getRequest = new HttpRequestMessage { RequestUri = new Uri(string.Format("http://localhost:10820/api/Job/{0}", requestId)), Method = HttpMethod.Get, }; getRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); //job is still running using (HttpResponseMessage getResponse1 = client.SendAsync(getRequest, CancellationToken.None).Result) { var job = getResponse1.Content.ReadAsAsync <WebScrapeJob>().Result; Assert.AreEqual(JobStatus.RUNNING, job.Status); Assert.AreEqual(HttpStatusCode.OK, getResponse1.StatusCode); } Thread.Sleep(11000); //job has completed using (HttpResponseMessage getResponse2 = await client.SendAsync(getRequest, CancellationToken.None)) { var job = getResponse2.Content.ReadAsAsync <WebScrapeJob>().Result; Assert.AreEqual(JobStatus.COMPLETED, job.Status); Assert.AreEqual(HttpStatusCode.OK, getResponse2.StatusCode); Assert.AreEqual(6, job.Scrapes.Count); Assert.IsTrue(job.Scrapes.Contains("Privacy")); } } }
/// <summary> /// Initializes a new instance of the <see cref="TokenClient"/> class. /// </summary> /// <param name="client">The client.</param> /// <param name="options">The options.</param> /// <exception cref="ArgumentNullException">client</exception> public TokenClient(HttpMessageInvoker client, TokenClientOptions options) : this(() => client, options) { }
private static async Task <HelloReply> MakeRawGrpcCall(HelloRequest request, HttpMessageInvoker client, bool streamRequest, bool streamResponse) { using var httpRequest = new HttpRequestMessage(HttpMethod.Post, RawGrpcUri); httpRequest.Version = HttpVersion.Version20; if (!streamRequest) { var messageSize = request.CalculateSize(); var data = new byte[messageSize + HeaderSize]; request.WriteTo(new CodedOutputStream(data)); Array.Copy(data, 0, data, HeaderSize, messageSize); data[0] = 0; BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(1, 4), (uint)messageSize); httpRequest.Content = new ByteArrayContent(data); httpRequest.Content.Headers.TryAddWithoutValidation("Content-Type", "application/grpc"); } else { httpRequest.Content = new PushUnaryContent <HelloRequest>(request); } httpRequest.Headers.TryAddWithoutValidation("TE", "trailers"); using var response = await client.SendAsync(httpRequest, Cts.Token); response.EnsureSuccessStatusCode(); HelloReply responseMessage; if (!streamResponse) { var data = await response.Content.ReadAsByteArrayAsync(); responseMessage = HelloReply.Parser.ParseFrom(data.AsSpan(5).ToArray()); } else { var responseStream = await response.Content.ReadAsStreamAsync(); var data = new byte[HeaderSize]; int read; var received = 0; while ((read = await responseStream.ReadAsync(data.AsMemory(received, HeaderSize - received), Cts.Token).ConfigureAwait(false)) > 0) { received += read; if (received == HeaderSize) { break; } } if (received < HeaderSize) { throw new InvalidDataException("Unexpected end of content while reading the message header."); } var length = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(1, 4)); if (data.Length < length) { data = new byte[length]; } received = 0; while ((read = await responseStream.ReadAsync(data.AsMemory(received, length - received), Cts.Token).ConfigureAwait(false)) > 0) { received += read; if (received == length) { break; } } read = await responseStream.ReadAsync(data, Cts.Token); if (read > 0) { throw new InvalidDataException("Extra data returned."); } responseMessage = HelloReply.Parser.ParseFrom(data); } var grpcStatus = response.TrailingHeaders.GetValues("grpc-status").SingleOrDefault(); if (grpcStatus != "0") { throw new InvalidOperationException($"Unexpected grpc-status: {grpcStatus}"); } return(responseMessage); }
/// <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: /// (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) Send response headers Downstream ◄-- Proxy ◄-- Upstream /// (8) Copy response body Downstream ◄-- Proxy ◄-- Upstream /// (9) Wait for completion of step 2: copying request body Downstream --► Proxy --► Upstream /// (10) Copy response trailer headers 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, Uri targetUri, HttpMessageInvoker httpClient, ProxyTelemetryContext proxyTelemetryContext, CancellationToken shortCancellation, CancellationToken longCancellation) { Contracts.CheckValue(context, nameof(context)); Contracts.CheckValue(targetUri, nameof(targetUri)); Contracts.CheckValue(httpClient, nameof(httpClient)); // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 1: Create outgoing HttpRequestMessage var upstreamRequest = new HttpRequestMessage(HttpUtilities.GetHttpMethod(context.Request.Method), targetUri) { // We request HTTP/2, but HttpClient will fallback to HTTP/1.1 if it cannot establish HTTP/2 with the target. // This is done without extra round-trips thanks to ALPN. We can detect a downgrade after calling HttpClient.SendAsync // (see Step 3 below). TBD how this will change when HTTP/3 is supported. Version = Http2Version, }; // ::::::::::::::::::::::::::::::::::::::::::::: // :: 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, longCancellation); // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 3: Copy request headers Downstream --► Proxy --► Upstream CopyHeadersToUpstream(context.Request.Headers, upstreamRequest); // ::::::::::::::::::::::::::::::::::::::::::::: // :: 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 (upstreamResponse.Version.Major != 2 && HttpUtilities.IsHttp2(context.Request.Protocol)) { // TODO: Do something on connection downgrade... _logger.LogInformation($"HTTP version downgrade detected! This may break gRPC communications."); } // 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) { throw new ReverseProxyException("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.Response.Headers); // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 7: 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. ////this.logger.LogInformation($" Starting downstream <-- Proxy response"); // TODO: Some of the tasks in steps (7) - (9) may go unobserved depending on what fails first. Needs more consideration. await context.Response.StartAsync(shortCancellation); // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 8: Copy response body Downstream ◄-- Proxy ◄-- Upstream await CopyBodyDownstreamAsync(upstreamResponse.Content, context.Response.Body, proxyTelemetryContext, longCancellation); // ::::::::::::::::::::::::::::::::::::::::::::: // :: 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; } // ::::::::::::::::::::::::::::::::::::::::::::: // :: Step 10: Copy response trailer headers Downstream ◄-- Proxy ◄-- Upstream CopyTrailingHeadersToDownstream(upstreamResponse, context); }
private static async Task <(byte[], HttpResponseHeaders)> StartLongRunningRequestAsync(ILogger logger, IHost host, HttpMessageInvoker client) { var request = new HttpRequestMessage(HttpMethod.Get, $"http://127.0.0.1:{host.GetPort()}/"); request.Version = HttpVersion.Version20; request.VersionPolicy = HttpVersionPolicy.RequestVersionExact; logger.LogInformation($"Sending request to '{request.RequestUri}'."); var responseMessage = await client.SendAsync(request, CancellationToken.None).DefaultTimeout(); responseMessage.EnsureSuccessStatusCode(); var responseStream = await responseMessage.Content.ReadAsStreamAsync(); logger.LogInformation($"Started reading response content"); var data = new List <byte>(); var buffer = new byte[1024 * 128]; int readCount; try { while ((readCount = await responseStream.ReadAsync(buffer)) != 0) { data.AddRange(buffer.AsMemory(0, readCount).ToArray()); logger.LogInformation($"Received {readCount} bytes. Total {data.Count} bytes."); } } catch { logger.LogInformation($"Error reading response. Total {data.Count} bytes."); throw; } logger.LogInformation($"Finished reading response content"); return(data.ToArray(), responseMessage.TrailingHeaders); }
/// <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 isClientHttp2 = 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 = isClientHttp2 && 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)); } // Detect connection downgrade, which may be problematic for e.g. gRPC. if (isClientHttp2 && destinationResponse.Version.Major != 2) { // TODO: Do something on connection downgrade... Log.HttpDowngradeDetected(_logger); } 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); }
public OwinHandlerBridge(DelegatingHandler delegatingHandler) { _delegatingHandler = delegatingHandler; _delegatingHandler.InnerHandler = _fixedResponseHandler; _invoker = new HttpMessageInvoker(_delegatingHandler); }
private static async Task IssueRequestAsync(HttpMessageHandler handler) { using (var c = new HttpMessageInvoker(handler, disposeHandler: false)) await Assert.ThrowsAnyAsync <Exception>(() => c.SendAsync(new HttpRequestMessage(HttpMethod.Get, new Uri("/shouldquicklyfail", UriKind.Relative)), default)); }
/// <summary> /// Sends a userinfo request. /// </summary> /// <param name="client">The client.</param> /// <param name="request">The request.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns></returns> public static async Task <DeviceAuthorizationResponse> RequestDeviceAuthorizationAsync(this HttpMessageInvoker client, DeviceAuthorizationRequest request, CancellationToken cancellationToken = default) { var httpRequest = new HttpRequestMessage(HttpMethod.Post, request.Address); httpRequest.Headers.Accept.Clear(); httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); var clone = request.Clone(); ClientCredentialsHelper.PopulateClientCredentials(clone, httpRequest); clone.Parameters.AddOptional(OidcConstants.AuthorizeRequest.Scope, request.Scope); httpRequest.Content = new FormUrlEncodedContent(clone.Parameters); HttpResponseMessage response; try { response = await client.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { return(new DeviceAuthorizationResponse(ex)); } string content = null; if (response.Content != null) { content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); } if (response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.BadRequest) { return(new DeviceAuthorizationResponse(content)); } else { return(new DeviceAuthorizationResponse(response.StatusCode, response.ReasonPhrase, content)); } }
public async Task StartAsync(Application application) { var invoker = new HttpMessageInvoker(new ConnectionRetryHandler(new SocketsHttpHandler { AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.None, UseProxy = false })); foreach (var service in application.Services.Values) { var serviceDescription = service.Description; if (service.Description.RunInfo is IngressRunInfo runInfo) { var host = Host.CreateDefaultBuilder() .ConfigureWebHostDefaults(builder => { var urls = new List <string>(); // Bind to the addresses on this resource for (int i = 0; i < serviceDescription.Replicas; i++) { // Fake replicas since it's all running processes var replica = service.Description.Name + "_" + Guid.NewGuid().ToString().Substring(0, 10).ToLower(); var status = new IngressStatus(service, replica); service.Replicas[replica] = status; var ports = new List <int>(); foreach (var binding in serviceDescription.Bindings) { if (binding.Port == null) { continue; } var port = binding.ReplicaPorts[i]; ports.Add(port); var url = $"{binding.Protocol}://localhost:{port}"; urls.Add(url); } status.Ports = ports; service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Added, status)); } builder.ConfigureServices(services => { services.AddSingleton <MatcherPolicy, IngressHostMatcherPolicy>(); services.AddLogging(loggingBuilder => { loggingBuilder.AddProvider(new ServiceLoggerProvider(service.Logs)); }); services.Configure <IServerAddressesFeature>(serverAddresses => { var addresses = serverAddresses.Addresses; if (addresses.IsReadOnly) { throw new NotSupportedException("Changing the URL isn't supported."); } addresses.Clear(); foreach (var u in urls) { addresses.Add(u); } }); }); builder.UseUrls(urls.ToArray()); builder.Configure(app => { app.UseRouting(); app.UseEndpoints(endpointBuilder => { foreach (var rule in runInfo.Rules) { if (!application.Services.TryGetValue(rule.Service, out var target)) { continue; } _logger.LogInformation("Processing ingress rule: Path:{Path}, Host:{Host}, Service:{Service}", rule.Path, rule.Host, rule.Service); var targetServiceDescription = target.Description; RegisterListener(target); var uris = new List <(int Port, Uri Uri)>(); // HTTP before HTTPS (this might change once we figure out certs...) var targetBinding = targetServiceDescription.Bindings.FirstOrDefault(b => b.Protocol == "http") ?? targetServiceDescription.Bindings.FirstOrDefault(b => b.Protocol == "https"); if (targetBinding == null) { _logger.LogInformation("Service {ServiceName} does not have any HTTP or HTTPs bindings", targetServiceDescription.Name); continue; } // For each of the target service replicas, get the base URL // based on the replica port for (int i = 0; i < targetServiceDescription.Replicas; i++) { var port = targetBinding.ReplicaPorts[i]; var url = $"{targetBinding.Protocol}://localhost:{port}"; uris.Add((port, new Uri(url))); } _logger.LogInformation("Service {ServiceName} is using {Urls}", targetServiceDescription.Name, string.Join(",", uris.Select(u => u.ToString()))); // The only load balancing strategy here is round robin long count = 0; RequestDelegate del = async context => { var next = (int)(Interlocked.Increment(ref count) % uris.Count); // we find the first `Ready` port for (int i = 0; i < uris.Count; i++) { if (_readyPorts.ContainsKey(uris[next].Port)) { break; } next = (int)(Interlocked.Increment(ref count) % uris.Count); } // if we've looped through all the port and didn't find a single one that is `Ready`, we return HTTP BadGateway if (!_readyPorts.ContainsKey(uris[next].Port)) { context.Response.StatusCode = (int)HttpStatusCode.BadGateway; await context.Response.WriteAsync("Bad gateway"); return; } var uri = new UriBuilder(uris[next].Uri) { Path = rule.PreservePath ? $"{context.Request.Path}" : (string)context.Request.RouteValues["path"] ?? "/", Query = context.Request.QueryString.Value }; await context.ProxyRequest(invoker, uri.Uri); }; IEndpointConventionBuilder conventions = endpointBuilder.Map((rule.Path?.TrimEnd('/') ?? "") + "/{**path}", del); if (rule.Host != null) { conventions.WithMetadata(new IngressHostMetadata(rule.Host)); } conventions.WithDisplayName(rule.Service); } }); }); }); var webApp = host.Build(); _webApplications.Add(webApp); // For each ingress rule, bind to the path and host await webApp.StartAsync(); foreach (var replica in service.Replicas) { service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Started, replica.Value)); } } } }
/// <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, Uri targetUri, HttpMessageInvoker httpClient, ProxyTelemetryContext proxyTelemetryContext, CancellationToken shortCancellation, CancellationToken longCancellation) { Contracts.CheckValue(context, nameof(context)); Contracts.CheckValue(targetUri, nameof(targetUri)); Contracts.CheckValue(httpClient, 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 1: Create outgoing HttpRequestMessage var upstreamRequest = new HttpRequestMessage(HttpUtilities.GetHttpMethod(context.Request.Method), targetUri) { // We request HTTP/2, but HttpClient will fallback to HTTP/1.1 if it cannot establish HTTP/2 with the target. // This is done without extra round-trips thanks to ALPN. We can detect a downgrade after calling HttpClient.SendAsync // (see Step 3 below). TBD how this will change when HTTP/3 is supported. Version = Http2Version, }; // ::::::::::::::::::::::::::::::::::::::::::::: // :: 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); // ::::::::::::::::::::::::::::::::::::::::::::: // :: 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.Response.Headers); // 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); 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> /// Sends a token request using the urn:openid:params:grant-type:ciba grant type. /// </summary> /// <param name="client">The client.</param> /// <param name="request">The request.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns></returns> public static async Task <TokenResponse> RequestBackchannelAuthenticationTokenAsync(this HttpMessageInvoker client, BackchannelAuthenticationTokenRequest request, CancellationToken cancellationToken = default) { var clone = request.Clone(); clone.Parameters.AddRequired(OidcConstants.TokenRequest.GrantType, OidcConstants.GrantTypes.Ciba); clone.Parameters.AddRequired(OidcConstants.TokenRequest.AuthenticationRequestId, request.AuthenticationRequestId); foreach (var resource in request.Resource) { clone.Parameters.AddRequired(OidcConstants.TokenRequest.Resource, resource, allowDuplicates: true); } return(await client.RequestTokenAsync(clone, cancellationToken).ConfigureAwait()); }
public abstract Task <HttpResponseMessage> SendAsync(HttpMessageInvoker inner, HttpRequestMessage request, CancellationToken cancellationToken);
/// <summary> /// Sends the request. /// </summary> /// <param name="invoker">The invoker.</param> /// <param name="cancellationToken">The token to monitor for cancellation requests.</param> /// <returns>A <see cref="ODataBatchResponseItem"/>.</returns> public abstract Task <ODataBatchResponseItem> SendRequestAsync(HttpMessageInvoker invoker, CancellationToken cancellationToken);
public static IHttpRequest UseClient(this IHttpRequest request, HttpMessageInvoker httpMessageInvoker) { request.ExecutionOptions.MessageInvoker = httpMessageInvoker; return(request); }
public async Task SendAsync_SendSameRequestMultipleTimesDirectlyOnHandler_Success(string stringContent, int startingPosition) { using (var handler = new HttpMessageInvoker(new HttpClientHandler())) { byte[] byteContent = Encoding.ASCII.GetBytes(stringContent); var content = new MemoryStream(); content.Write(byteContent, 0, byteContent.Length); content.Position = startingPosition; var request = new HttpRequestMessage(HttpMethod.Post, Configuration.Http.RemoteEchoServer) { Content = new StreamContent(content) }; for (int iter = 0; iter < 2; iter++) { using (HttpResponseMessage response = await handler.SendAsync(request, CancellationToken.None)) { Assert.Equal(HttpStatusCode.OK, response.StatusCode); string responseContent = await response.Content.ReadAsStringAsync(); Assert.Contains($"\"Content-Length\": \"{request.Content.Headers.ContentLength.Value}\"", responseContent); Assert.Contains(stringContent.Substring(startingPosition), responseContent); if (startingPosition != 0) { Assert.DoesNotContain(stringContent.Substring(0, startingPosition), responseContent); } } } } }
public async Task ServerTrailersSetOnResponseAfterContentRead() { var tcs = new TaskCompletionSource <object>(TaskCreationOptions.RunContinuationsAsynchronously); var handler = new ClientHandler(PathString.Empty, new DummyApplication(async context => { context.Response.AppendTrailer("StartTrailer", "Value!"); await context.Response.WriteAsync("Hello World"); await context.Response.Body.FlushAsync(); // Pause writing response to ensure trailers are written at the end await tcs.Task; await context.Response.WriteAsync("Bye World"); await context.Response.Body.FlushAsync(); context.Response.AppendTrailer("EndTrailer", "Value!"); })); var invoker = new HttpMessageInvoker(handler); var message = new HttpRequestMessage(HttpMethod.Post, "https://example.com/"); var response = await invoker.SendAsync(message, CancellationToken.None); Assert.Empty(response.TrailingHeaders); var responseBody = await response.Content.ReadAsStreamAsync(); int read = await responseBody.ReadAsync(new byte[100], 0, 100); Assert.Equal(11, read); Assert.Empty(response.TrailingHeaders); var readTask = responseBody.ReadAsync(new byte[100], 0, 100); Assert.False(readTask.IsCompleted); tcs.TrySetResult(null); read = await readTask; Assert.Equal(9, read); Assert.Empty(response.TrailingHeaders); // Read nothing because we're at the end of the response read = await responseBody.ReadAsync(new byte[100], 0, 100); Assert.Equal(0, read); // Ensure additional reads after end don't effect trailers read = await responseBody.ReadAsync(new byte[100], 0, 100); Assert.Equal(0, read); Assert.Collection(response.TrailingHeaders, kvp => { Assert.Equal("StartTrailer", kvp.Key); Assert.Equal("Value!", kvp.Value.Single()); }, kvp => { Assert.Equal("EndTrailer", kvp.Key); Assert.Equal("Value!", kvp.Value.Single()); }); }