Пример #1
0
            public override int Read(Span <byte> buffer)
            {
                HttpConnection connection = _connection;

                if (connection == null || buffer.Length == 0)
                {
                    // Response body fully consumed or the caller didn't ask for any data
                    return(0);
                }

                int bytesRead = connection.ReadBuffered(buffer);

                if (bytesRead == 0)
                {
                    // We cannot reuse this connection, so close it.
                    _connection = null;
                    connection.Dispose();
                }

                return(bytesRead);
            }
Пример #2
0
        public ValueTask <HttpConnection> GetConnectionAsync <TState>(
            Func <TState, CancellationToken, ValueTask <HttpConnection> > createConnection, TState state, CancellationToken cancellationToken)
        {
            if (cancellationToken.IsCancellationRequested)
            {
                return(new ValueTask <HttpConnection>(Task.FromCanceled <HttpConnection>(cancellationToken)));
            }

            List <CachedConnection> list = _idleConnections;

            lock (SyncObj)
            {
                // Try to return a cached connection.  We need to loop in case the connection
                // we get from the list is unusable.
                while (list.Count > 0)
                {
                    CachedConnection cachedConnection = list[list.Count - 1];
                    HttpConnection   conn             = cachedConnection._connection;

                    list.RemoveAt(list.Count - 1);
                    if (cachedConnection.IsUsable())
                    {
                        // We found a valid collection.  Return it.
                        if (NetEventSource.IsEnabled)
                        {
                            conn.Trace("Found usable connection in pool.");
                        }
                        return(new ValueTask <HttpConnection>(conn));
                    }

                    // We got a connection, but it was already closed by the server or the
                    // server sent unexpected data or the connection is too old.  In any case,
                    // we can't use the connection, so get rid of it and try again.
                    if (NetEventSource.IsEnabled)
                    {
                        conn.Trace("Found invalid connection in pool.");
                    }
                    conn.Dispose();
                }

                // No valid cached connections, so we need to create a new one.  If
                // there's no limit on the number of connections associated with this
                // pool, or if we haven't reached such a limit, simply create a new
                // connection.
                if (_associatedConnectionCount < _maxConnections)
                {
                    if (NetEventSource.IsEnabled)
                    {
                        Trace("Creating new connection for pool.");
                    }
                    IncrementConnectionCountNoLock();
                    return(WaitForCreatedConnectionAsync(createConnection(state, cancellationToken)));
                }
                else
                {
                    // There is a limit, and we've reached it, which means we need to
                    // wait for a connection to be returned to the pool or for a connection
                    // associated with the pool to be dropped before we can create a
                    // new one.  Create a waiter object and register it with the pool; it'll
                    // be signaled with the created connection when one is returned or
                    // space is available and the provided creation func has successfully
                    // created the connection to be used.
                    if (NetEventSource.IsEnabled)
                    {
                        Trace("Limit reached.  Waiting to create new connection.");
                    }
                    var waiter = new ConnectionWaiter <TState>(this, createConnection, state, cancellationToken);
                    EnqueueWaiter(waiter);
                    if (cancellationToken.CanBeCanceled)
                    {
                        // If cancellation could be requested, register a callback for it that'll cancel
                        // the waiter and remove the waiter from the queue.  Note that this registration needs
                        // to happen under the reentrant lock and after enqueueing the waiter.
                        waiter._cancellationTokenRegistration = cancellationToken.Register(s =>
                        {
                            var innerWaiter = (ConnectionWaiter)s;
                            lock (innerWaiter._pool.SyncObj)
                            {
                                // If it's in the list, remove it and cancel it.
                                if (innerWaiter._pool.RemoveWaiterForCancellation(innerWaiter))
                                {
                                    bool canceled = innerWaiter.TrySetCanceled(innerWaiter._cancellationToken);
                                    Debug.Assert(canceled);
                                }
                            }
                        }, waiter);
                    }
                    return(new ValueTask <HttpConnection>(waiter.Task));
                }

                // Note that we don't check for _disposed.  We may end up disposing the
                // created connection when it's returned, but we don't want to block use
                // of the pool if it's already been disposed, as there's a race condition
                // between getting a pool and someone disposing of it, and we don't want
                // to complicate the logic about trying to get a different pool when the
                // retrieved one has been disposed of.  In the future we could alternatively
                // try returning such connections to whatever pool is currently considered
                // current for that endpoint, if there is one.
            }
        }
Пример #3
0
 private void Finish(HttpConnection connection)
 {
     // We cannot reuse this connection, so close it.
     _connection = null;
     connection.Dispose();
 }
Пример #4
0
        private ValueTask <(HttpConnection, HttpResponseMessage)> GetConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            if (cancellationToken.IsCancellationRequested)
            {
                return(new ValueTask <(HttpConnection, HttpResponseMessage)>(Task.FromCanceled <(HttpConnection, HttpResponseMessage)>(cancellationToken)));
            }

            TimeSpan                pooledConnectionLifetime    = _poolManager.Settings._pooledConnectionLifetime;
            TimeSpan                pooledConnectionIdleTimeout = _poolManager.Settings._pooledConnectionIdleTimeout;
            DateTimeOffset          now  = DateTimeOffset.UtcNow;
            List <CachedConnection> list = _idleConnections;

            // Try to get a cached connection.  If we can and if it's usable, return it.  If we can but it's not usable,
            // try again.  And if we can't because there aren't any valid ones, create a new one and return it.
            while (true)
            {
                CachedConnection cachedConnection;
                lock (SyncObj)
                {
                    if (list.Count > 0)
                    {
                        // Pop off the next connection to try.  We'll test it outside of the lock
                        // to avoid doing expensive validation while holding the lock.
                        cachedConnection = list[list.Count - 1];
                        list.RemoveAt(list.Count - 1);
                    }
                    else
                    {
                        // No valid cached connections, so we need to create a new one.  If
                        // there's no limit on the number of connections associated with this
                        // pool, or if we haven't reached such a limit, simply create a new
                        // connection.
                        if (_associatedConnectionCount < _maxConnections)
                        {
                            if (NetEventSource.IsEnabled)
                            {
                                Trace("Creating new connection for pool.");
                            }
                            IncrementConnectionCountNoLock();
                            return(WaitForCreatedConnectionAsync(CreateConnectionAsync(request, cancellationToken)));
                        }
                        else
                        {
                            // There is a limit, and we've reached it, which means we need to
                            // wait for a connection to be returned to the pool or for a connection
                            // associated with the pool to be dropped before we can create a
                            // new one.  Create a waiter object and register it with the pool; it'll
                            // be signaled with the created connection when one is returned or
                            // space is available and the provided creation func has successfully
                            // created the connection to be used.
                            if (NetEventSource.IsEnabled)
                            {
                                Trace("Limit reached.  Waiting to create new connection.");
                            }
                            var waiter = new ConnectionWaiter(this, request, cancellationToken);
                            EnqueueWaiter(waiter);
                            if (cancellationToken.CanBeCanceled)
                            {
                                // If cancellation could be requested, register a callback for it that'll cancel
                                // the waiter and remove the waiter from the queue.  Note that this registration needs
                                // to happen under the reentrant lock and after enqueueing the waiter.
                                waiter._cancellationTokenRegistration = cancellationToken.Register(s =>
                                {
                                    var innerWaiter = (ConnectionWaiter)s;
                                    lock (innerWaiter._pool.SyncObj)
                                    {
                                        // If it's in the list, remove it and cancel it.
                                        if (innerWaiter._pool.RemoveWaiterForCancellation(innerWaiter))
                                        {
                                            bool canceled = innerWaiter.TrySetCanceled(innerWaiter._cancellationToken);
                                            Debug.Assert(canceled);
                                        }
                                    }
                                }, waiter);
                            }
                            return(new ValueTask <(HttpConnection, HttpResponseMessage)>(waiter.Task));
                        }

                        // Note that we don't check for _disposed.  We may end up disposing the
                        // created connection when it's returned, but we don't want to block use
                        // of the pool if it's already been disposed, as there's a race condition
                        // between getting a pool and someone disposing of it, and we don't want
                        // to complicate the logic about trying to get a different pool when the
                        // retrieved one has been disposed of.  In the future we could alternatively
                        // try returning such connections to whatever pool is currently considered
                        // current for that endpoint, if there is one.
                    }
                }

                HttpConnection conn = cachedConnection._connection;
                if (cachedConnection.IsUsable(now, pooledConnectionLifetime, pooledConnectionIdleTimeout) &&
                    !conn.EnsureReadAheadAndPollRead())
                {
                    // We found a valid connection.  Return it.
                    if (NetEventSource.IsEnabled)
                    {
                        conn.Trace("Found usable connection in pool.");
                    }
                    return(new ValueTask <(HttpConnection, HttpResponseMessage)>((conn, null)));
                }

                // We got a connection, but it was already closed by the server or the
                // server sent unexpected data or the connection is too old.  In any case,
                // we can't use the connection, so get rid of it and loop around to try again.
                if (NetEventSource.IsEnabled)
                {
                    conn.Trace("Found invalid connection in pool.");
                }
                conn.Dispose();
            }
        }
Пример #5
0
        /// <summary>Returns the connection to the pool for subsequent reuse.</summary>
        /// <param name="connection">The connection to return.</param>
        public void ReturnConnection(HttpConnection connection)
        {
            TimeSpan lifetime        = _poolManager.Settings._pooledConnectionLifetime;
            bool     lifetimeExpired =
                lifetime != Timeout.InfiniteTimeSpan &&
                (lifetime == TimeSpan.Zero || connection.CreationTime + lifetime <= DateTime.UtcNow);

            if (!lifetimeExpired)
            {
                List <CachedConnection> list = _idleConnections;
                lock (SyncObj)
                {
                    Debug.Assert(list.Count <= _maxConnections, $"Expected {list.Count} <= {_maxConnections}");

                    // Mark the pool as still being active.
                    _usedSinceLastCleanup = true;

                    // If there's someone waiting for a connection and this one's still valid, simply transfer this one to them rather than pooling it.
                    // Note that while we checked connection lifetime above, we don't check idle timeout, as even if idle timeout
                    // is zero, we consider a connection that's just handed from one use to another to never actually be idle.
                    bool receivedUnexpectedData = false;
                    if (HasWaiter())
                    {
                        receivedUnexpectedData = connection.EnsureReadAheadAndPollRead();
                        if (!receivedUnexpectedData && TransferConnection(connection))
                        {
                            if (NetEventSource.IsEnabled)
                            {
                                connection.Trace("Transferred connection to waiter.");
                            }
                            return;
                        }
                    }

                    // If the connection is still valid, add it to the list.
                    // If the pool has been disposed of, dispose the connection being returned,
                    // as the pool is being deactivated. We do this after the above in order to
                    // use pooled connections to satisfy any requests that pended before the
                    // the pool was disposed of.  We also dispose of connections if connection
                    // timeouts are such that the connection would immediately expire, anyway, as
                    // well as for connections that have unexpectedly received extraneous data / EOF.
                    if (!receivedUnexpectedData &&
                        !_disposed &&
                        _poolManager.Settings._pooledConnectionIdleTimeout != TimeSpan.Zero)
                    {
                        // Pool the connection by adding it to the list.
                        list.Add(new CachedConnection(connection));
                        if (NetEventSource.IsEnabled)
                        {
                            connection.Trace("Stored connection in pool.");
                        }
                        return;
                    }
                }
            }

            // The connection could be not be reused.  Dispose of it.
            // Disposing it will alert any waiters that a connection slot has become available.
            if (NetEventSource.IsEnabled)
            {
                connection.Trace(
                    lifetimeExpired ? "Disposing connection return to pool. Connection lifetime expired." :
                    _poolManager.Settings._pooledConnectionIdleTimeout == TimeSpan.Zero ? "Disposing connection returned to pool. Zero idle timeout." :
                    _disposed ? "Disposing connection returned to pool. Pool was disposed." :
                    "Disposing connection returned to pool. Read-ahead unexpectedly completed.");
            }
            connection.Dispose();
        }
Пример #6
0
        private ValueTask <HttpConnection> GetOrReserveHttp11ConnectionAsync(CancellationToken cancellationToken)
        {
            if (cancellationToken.IsCancellationRequested)
            {
                return(new ValueTask <HttpConnection>(Task.FromCanceled <HttpConnection>(cancellationToken)));
            }

            TimeSpan                pooledConnectionLifetime    = _poolManager.Settings._pooledConnectionLifetime;
            TimeSpan                pooledConnectionIdleTimeout = _poolManager.Settings._pooledConnectionIdleTimeout;
            DateTimeOffset          now  = DateTimeOffset.UtcNow;
            List <CachedConnection> list = _idleConnections;

            // Try to find a usable cached connection.
            // If we can't find one, we will either wait for one to become available (if at the connection limit)
            // or just increment the connection count and return null so the caller can create a new connection.
            TaskCompletionSourceWithCancellation <HttpConnection> waiter;

            while (true)
            {
                CachedConnection cachedConnection;
                lock (SyncObj)
                {
                    if (list.Count > 0)
                    {
                        // We have a cached connection that we can attempt to use.
                        // Test it below outside the lock, to avoid doing expensive validation while holding the lock.
                        cachedConnection = list[list.Count - 1];
                        list.RemoveAt(list.Count - 1);
                    }
                    else
                    {
                        // No valid cached connections.
                        if (_associatedConnectionCount < _maxConnections)
                        {
                            // We are under the connection limit, so just increment the count and return null
                            // to indicate to the caller that they should create a new connection.
                            IncrementConnectionCountNoLock();
                            return(new ValueTask <HttpConnection>((HttpConnection)null));
                        }
                        else
                        {
                            // We've reached the connection limit and need to wait for an existing connection
                            // to become available, or to be closed so that we can create a new connection.
                            // Enqueue a waiter that will be signalled when this happens.
                            // Break out of the loop and then do the actual wait below.
                            waiter = EnqueueWaiter();
                            break;
                        }

                        // Note that we don't check for _disposed.  We may end up disposing the
                        // created connection when it's returned, but we don't want to block use
                        // of the pool if it's already been disposed, as there's a race condition
                        // between getting a pool and someone disposing of it, and we don't want
                        // to complicate the logic about trying to get a different pool when the
                        // retrieved one has been disposed of.  In the future we could alternatively
                        // try returning such connections to whatever pool is currently considered
                        // current for that endpoint, if there is one.
                    }
                }

                HttpConnection conn = cachedConnection._connection;
                if (cachedConnection.IsUsable(now, pooledConnectionLifetime, pooledConnectionIdleTimeout) &&
                    !conn.EnsureReadAheadAndPollRead())
                {
                    // We found a valid connection.  Return it.
                    if (NetEventSource.IsEnabled)
                    {
                        conn.Trace("Found usable connection in pool.");
                    }
                    return(new ValueTask <HttpConnection>(conn));
                }

                // We got a connection, but it was already closed by the server or the
                // server sent unexpected data or the connection is too old.  In any case,
                // we can't use the connection, so get rid of it and loop around to try again.
                if (NetEventSource.IsEnabled)
                {
                    conn.Trace("Found invalid connection in pool.");
                }
                conn.Dispose();
            }

            // We are at the connection limit. Wait for an available connection or connection count (indicated by null).
            if (NetEventSource.IsEnabled)
            {
                Trace("Connection limit reached, waiting for available connection.");
            }
            return(new ValueTask <HttpConnection>(waiter.WaitWithCancellationAsync(cancellationToken)));
        }
Пример #7
0
        private static async Task <HttpResponseMessage> SendWithNtAuthAsync(HttpRequestMessage request, Uri authUri, bool async, ICredentials credentials, bool isProxyAuth, HttpConnection connection, HttpConnectionPool connectionPool, CancellationToken cancellationToken)
        {
            HttpResponseMessage response = await InnerSendAsync(request, async, isProxyAuth, connectionPool, connection, cancellationToken).ConfigureAwait(false);

            if (!isProxyAuth && connection.Kind == HttpConnectionKind.Proxy && !ProxySupportsConnectionAuth(response))
            {
                // Proxy didn't indicate that it supports connection-based auth, so we can't proceed.
                if (NetEventSource.Log.IsEnabled())
                {
                    NetEventSource.Error(connection, $"Proxy doesn't support connection-based auth, uri={authUri}");
                }
                return(response);
            }

            if (TryGetAuthenticationChallenge(response, isProxyAuth, authUri, credentials, out AuthenticationChallenge challenge))
            {
                if (challenge.AuthenticationType == AuthenticationType.Negotiate ||
                    challenge.AuthenticationType == AuthenticationType.Ntlm)
                {
                    bool isNewConnection = false;
                    bool needDrain       = true;
                    try
                    {
                        if (response.Headers.ConnectionClose.GetValueOrDefault())
                        {
                            // Server is closing the connection and asking us to authenticate on a new connection.

                            // First, detach the current connection from the pool. This means it will no longer count against the connection limit.
                            // Instead, it will be replaced by the new connection below.
                            connection.DetachFromPool();

                            connection = await connectionPool.CreateHttp11ConnectionAsync(request, async, cancellationToken).ConfigureAwait(false);

                            connection !.Acquire();
                            isNewConnection = true;
                            needDrain       = false;
                        }

                        if (NetEventSource.Log.IsEnabled())
                        {
                            NetEventSource.Info(connection, $"Authentication: {challenge.AuthenticationType}, Uri: {authUri.AbsoluteUri}");
                        }

                        // Calculate SPN (Service Principal Name) using the host name of the request.
                        // Use the request's 'Host' header if available. Otherwise, use the request uri.
                        // Ignore the 'Host' header if this is proxy authentication since we need to use
                        // the host name of the proxy itself for SPN calculation.
                        string hostName;
                        if (!isProxyAuth && request.HasHeaders && request.Headers.Host != null)
                        {
                            // Use the host name without any normalization.
                            hostName = request.Headers.Host;
                            if (NetEventSource.Log.IsEnabled())
                            {
                                NetEventSource.Info(connection, $"Authentication: {challenge.AuthenticationType}, Host: {hostName}");
                            }
                        }
                        else
                        {
                            // Need to use FQDN normalized host so that CNAME's are traversed.
                            // Use DNS to do the forward lookup to an A (host) record.
                            // But skip DNS lookup on IP literals. Otherwise, we would end up
                            // doing an unintended reverse DNS lookup.
                            UriHostNameType hnt = authUri.HostNameType;
                            if (hnt == UriHostNameType.IPv6 || hnt == UriHostNameType.IPv4)
                            {
                                hostName = authUri.IdnHost;
                            }
                            else
                            {
                                IPHostEntry result = await Dns.GetHostEntryAsync(authUri.IdnHost, cancellationToken).ConfigureAwait(false);

                                hostName = result.HostName;
                            }

                            if (!isProxyAuth && !authUri.IsDefaultPort && UsePortInSpn)
                            {
                                hostName = string.Create(null, stackalloc char[128], $"{hostName}:{authUri.Port}");
                            }
                        }

                        string spn = "HTTP/" + hostName;
                        if (NetEventSource.Log.IsEnabled())
                        {
                            NetEventSource.Info(connection, $"Authentication: {challenge.AuthenticationType}, SPN: {spn}");
                        }

                        ProtectionLevel requiredProtectionLevel = ProtectionLevel.None;
                        // When connecting to proxy server don't enforce the integrity to avoid
                        // compatibility issues. The assumption is that the proxy server comes
                        // from a trusted source. On macOS we always need to enforce the integrity
                        // to avoid the GSSAPI implementation generating corrupted authentication
                        // tokens.
                        if (!isProxyAuth || OperatingSystem.IsMacOS())
                        {
                            requiredProtectionLevel = ProtectionLevel.Sign;
                        }

                        NegotiateAuthenticationClientOptions authClientOptions = new NegotiateAuthenticationClientOptions
                        {
                            Package    = challenge.SchemeName,
                            Credential = challenge.Credential,
                            TargetName = spn,
                            RequiredProtectionLevel = requiredProtectionLevel,
                            Binding = connection.TransportContext?.GetChannelBinding(ChannelBindingKind.Endpoint)
                        };

                        using NegotiateAuthentication authContext = new NegotiateAuthentication(authClientOptions);
                        string?challengeData = challenge.ChallengeData;
                        NegotiateAuthenticationStatusCode statusCode;
                        while (true)
                        {
                            string?challengeResponse = authContext.GetOutgoingBlob(challengeData, out statusCode);
                            if (statusCode > NegotiateAuthenticationStatusCode.ContinueNeeded || challengeResponse == null)
                            {
                                // Response indicated denial even after login, so stop processing and return current response.
                                break;
                            }

                            if (needDrain)
                            {
                                await connection.DrainResponseAsync(response !, cancellationToken).ConfigureAwait(false);
                            }

                            SetRequestAuthenticationHeaderValue(request, new AuthenticationHeaderValue(challenge.SchemeName, challengeResponse), isProxyAuth);

                            response = await InnerSendAsync(request, async, isProxyAuth, connectionPool, connection, cancellationToken).ConfigureAwait(false);

                            if (authContext.IsAuthenticated || !TryGetChallengeDataForScheme(challenge.SchemeName, GetResponseAuthenticationHeaderValues(response, isProxyAuth), out challengeData))
                            {
                                break;
                            }

                            if (!IsAuthenticationChallenge(response, isProxyAuth))
                            {
                                // Tail response for Negoatiate on successful authentication. Validate it before we proceed.
                                authContext.GetOutgoingBlob(challengeData, out statusCode);
                                if (statusCode > NegotiateAuthenticationStatusCode.ContinueNeeded)
                                {
                                    isNewConnection = false;
                                    connection.Dispose();
                                    throw new HttpRequestException(SR.Format(SR.net_http_authvalidationfailure, statusCode), null, HttpStatusCode.Unauthorized);
                                }
                                break;
                            }

                            needDrain = true;
                        }
                    }
                    finally
                    {
                        if (isNewConnection)
                        {
                            connection !.Release();
                        }
                    }
                }
            }

            return(response !);
        }