From c28ea153ac72968aa9964cc4d7ad09c289c504b7 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Mon, 27 Dec 2021 00:15:19 +0100 Subject: [PATCH] Happy Eyeballs for HTTP requests. Fixes #38 Thanks to https://github.com/space-wizards/SS14.Launcher/issues/38 --- SS14.Launcher/HappyEyeballsHttp.cs | 104 +++++++++++++++++++++++++++++ SS14.Launcher/Program.cs | 3 +- 2 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 SS14.Launcher/HappyEyeballsHttp.cs diff --git a/SS14.Launcher/HappyEyeballsHttp.cs b/SS14.Launcher/HappyEyeballsHttp.cs new file mode 100644 index 00000000..46f9a2b6 --- /dev/null +++ b/SS14.Launcher/HappyEyeballsHttp.cs @@ -0,0 +1,104 @@ +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace SS14.Launcher; + +public static class HappyEyeballsHttp +{ + // .NET does not implement Happy Eyeballs at the time of writing. + // https://github.com/space-wizards/SS14.Launcher/issues/38 + // This is the workaround. + // + // Implementation taken from https://github.com/ppy/osu-framework/pull/4191/files + public static HttpClient CreateHttpClient() + { + var handler = new SocketsHttpHandler + { + ConnectCallback = OnConnect, + AutomaticDecompression = DecompressionMethods.All + }; + + return new HttpClient(handler); + } + + /// + /// Whether IPv6 should be preferred. Value may change based on runtime failures. + /// + private static bool _useIPv6 = Socket.OSSupportsIPv6; + + /// + /// Whether the initial IPv6 check has been performed (to determine whether v6 is available or not). + /// + private static bool _hasResolvedIPv6Availability; + + private const int FirstTryTimeout = 2000; + + private static async ValueTask OnConnect( + SocketsHttpConnectionContext context, + CancellationToken cancellationToken) + { + 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(FirstTryTimeout); + 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 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; + } + } +} diff --git a/SS14.Launcher/Program.cs b/SS14.Launcher/Program.cs index 0ab0a828..4025d672 100644 --- a/SS14.Launcher/Program.cs +++ b/SS14.Launcher/Program.cs @@ -1,5 +1,4 @@ using System; -using System.Net.Http; using System.Net.Http.Headers; using System.Runtime.InteropServices; using System.Threading.Tasks; @@ -78,7 +77,7 @@ public static void Main(string[] args) cfg.Load(); Locator.CurrentMutable.RegisterConstant(cfg); - var http = new HttpClient(); + var http = HappyEyeballsHttp.CreateHttpClient(); http.DefaultRequestHeaders.UserAgent.Add( new ProductInfoHeaderValue(LauncherVersion.Name, LauncherVersion.Version?.ToString())); http.DefaultRequestHeaders.Add("SS14-Launcher-Fingerprint", cfg.Fingerprint.ToString());