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); }
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. } }
private void Finish(HttpConnection connection) { // We cannot reuse this connection, so close it. _connection = null; connection.Dispose(); }
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(); } }
/// <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(); }
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))); }
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 !); }