@ -3,8 +3,10 @@ using System.IO;
using System.Net ;
using System.Net.Http ;
using System.Net.Http.Headers ;
using System.Net.Sockets ;
using System.Text ;
using System.Threading ;
using System.Threading.Tasks ;
using NLog ;
using NzbDrone.Common.Cache ;
using NzbDrone.Common.Extensions ;
@ -16,6 +18,10 @@ namespace NzbDrone.Common.Http.Dispatchers
{
private const string NO_PROXY_KEY = "no-proxy" ;
private const int connection_establish_timeout = 2000 ;
private static bool useIPv6 = Socket . OSSupportsIPv6 ;
private static bool hasResolvedIPv6Availability ;
private readonly IHttpProxySettingsProvider _proxySettingsProvider ;
private readonly ICreateManagedWebProxy _createManagedWebProxy ;
private readonly IUserAgentBuilder _userAgentBuilder ;
@ -142,13 +148,14 @@ namespace NzbDrone.Common.Http.Dispatchers
protected virtual System . Net . Http . HttpClient CreateHttpClient ( HttpProxySettings proxySettings )
{
var handler = new HttpClient Handler( )
var handler = new Sockets HttpHandler( )
{
AutomaticDecompression = DecompressionMethods . GZip | DecompressionMethods . Brotli ,
UseCookies = false , // sic - we don't want to use a shared cookie container
AllowAutoRedirect = false ,
Credentials = GetCredentialCache ( ) ,
PreAuthenticate = true
PreAuthenticate = true ,
ConnectCallback = onConnect ,
} ;
if ( proxySettings ! = null )
@ -230,5 +237,67 @@ namespace NzbDrone.Common.Http.Dispatchers
{
return _credentialCache . Get ( "credentialCache" , ( ) = > new CredentialCache ( ) ) ;
}
private static async ValueTask < Stream > onConnect ( SocketsHttpConnectionContext context , CancellationToken cancellationToken )
{
// Until .NET supports an implementation of Happy Eyeballs (https://tools.ietf.org/html/rfc8305#section-2), let's make IPv4 fallback work in a simple way.
// This issue is being tracked at https://github.com/dotnet/runtime/issues/26177 and expected to be fixed in .NET 6.
if ( useIPv6 )
{
try
{
var localToken = cancellationToken ;
if ( ! hasResolvedIPv6Availability )
{
// to make things move fast, use a very low timeout for the initial ipv6 attempt.
var quickFailCts = new CancellationTokenSource ( connection_establish_timeout ) ;
var linkedTokenSource = CancellationTokenSource . CreateLinkedTokenSource ( cancellationToken , quickFailCts . Token ) ;
localToken = linkedTokenSource . Token ;
}
return await attemptConnection ( AddressFamily . InterNetworkV6 , context , localToken ) ;
}
catch
{
// very naively fallback to ipv4 permanently for this execution based on the response of the first connection attempt.
// note that this may cause users to eventually get switched to ipv4 (on a random failure when they are switching networks, for instance)
// but in the interest of keeping this implementation simple, this is acceptable.
useIPv6 = false ;
}
finally
{
hasResolvedIPv6Availability = true ;
}
}
// fallback to IPv4.
return await attemptConnection ( AddressFamily . InterNetwork , context , cancellationToken ) ;
}
private static async ValueTask < Stream > attemptConnection ( AddressFamily addressFamily , SocketsHttpConnectionContext context , CancellationToken cancellationToken )
{
// The following socket constructor will create a dual-mode socket on systems where IPV6 is available.
var socket = new Socket ( addressFamily , SocketType . Stream , ProtocolType . Tcp )
{
// Turn off Nagle's algorithm since it degrades performance in most HttpClient scenarios.
NoDelay = true
} ;
try
{
await socket . ConnectAsync ( context . DnsEndPoint , cancellationToken ) . ConfigureAwait ( false ) ;
// The stream should take the ownership of the underlying socket,
// closing it when it's disposed.
return new NetworkStream ( socket , ownsSocket : true ) ;
}
catch
{
socket . Dispose ( ) ;
throw ;
}
}
}
}