mirror of
https://github.com/fergalmoran/Readarr.git
synced 2025-12-25 19:09:00 +00:00
Fixed: Gracefully fall back to ipv4 if ipv6 is broken
This commit is contained in:
@@ -3,8 +3,10 @@ using System.IO;
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
|
using System.Net.Sockets;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Cache;
|
using NzbDrone.Common.Cache;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
@@ -16,6 +18,10 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||||||
{
|
{
|
||||||
private const string NO_PROXY_KEY = "no-proxy";
|
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 IHttpProxySettingsProvider _proxySettingsProvider;
|
||||||
private readonly ICreateManagedWebProxy _createManagedWebProxy;
|
private readonly ICreateManagedWebProxy _createManagedWebProxy;
|
||||||
private readonly IUserAgentBuilder _userAgentBuilder;
|
private readonly IUserAgentBuilder _userAgentBuilder;
|
||||||
@@ -142,13 +148,14 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||||||
|
|
||||||
protected virtual System.Net.Http.HttpClient CreateHttpClient(HttpProxySettings proxySettings)
|
protected virtual System.Net.Http.HttpClient CreateHttpClient(HttpProxySettings proxySettings)
|
||||||
{
|
{
|
||||||
var handler = new HttpClientHandler()
|
var handler = new SocketsHttpHandler()
|
||||||
{
|
{
|
||||||
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Brotli,
|
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Brotli,
|
||||||
UseCookies = false, // sic - we don't want to use a shared cookie container
|
UseCookies = false, // sic - we don't want to use a shared cookie container
|
||||||
AllowAutoRedirect = false,
|
AllowAutoRedirect = false,
|
||||||
Credentials = GetCredentialCache(),
|
Credentials = GetCredentialCache(),
|
||||||
PreAuthenticate = true
|
PreAuthenticate = true,
|
||||||
|
ConnectCallback = onConnect,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (proxySettings != null)
|
if (proxySettings != null)
|
||||||
@@ -230,5 +237,67 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||||||
{
|
{
|
||||||
return _credentialCache.Get("credentialCache", () => new CredentialCache());
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user