示例#1
0
 public void SendAsync_Null_ThrowsArgumentNullException()
 {
     using (var invoker = new HttpMessageInvoker(new MockHandler()))
     {
         Assert.Throws<ArgumentNullException>(() => { Task t = invoker.SendAsync(null, CancellationToken.None); });
     }
 }
示例#2
0
 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);
 }
示例#3
0
        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);
        }
示例#4
0
        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;
 }
示例#7
0
 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);
 }
示例#10
0
 /// <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)
 {
 }
示例#11
0
        /// <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);
 }
示例#13
0
 /// <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());
        }
示例#15
0
 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;
 }
示例#16
0
文件: HttpProxy.cs 项目: 4201104140/-
        /// <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;
 }
示例#18
0
        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"));
                }
            }
        }
示例#20
0
 /// <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)
 {
 }
示例#21
0
        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);
        }
示例#22
0
        /// <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);
        }
示例#23
0
    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);
    }
示例#24
0
    /// <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);
    }
示例#25
0
 public OwinHandlerBridge(DelegatingHandler delegatingHandler)
 {
     _delegatingHandler = delegatingHandler;
     _delegatingHandler.InnerHandler = _fixedResponseHandler;
     _invoker = new HttpMessageInvoker(_delegatingHandler);
 }
示例#26
0
 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));
            }
        }
示例#28
0
        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));
                    }
                }
            }
        }
示例#29
0
        /// <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;
            }
        }
示例#30
0
    /// <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);
示例#32
0
 /// <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);
                        }
                    }
                }
            }
        }
示例#35
0
        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());
            });
        }