Skip to content

Commit

Permalink
Happy Eyeballs for HTTP requests.
Browse files Browse the repository at this point in the history
Fixes #38
Thanks to #38
  • Loading branch information
PJB3005 committed Dec 26, 2021
1 parent 64ef598 commit c28ea15
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 2 deletions.
104 changes: 104 additions & 0 deletions SS14.Launcher/HappyEyeballsHttp.cs
Original file line number Diff line number Diff line change
@@ -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);
}

/// <summary>
/// Whether IPv6 should be preferred. Value may change based on runtime failures.
/// </summary>
private static bool _useIPv6 = Socket.OSSupportsIPv6;

/// <summary>
/// Whether the initial IPv6 check has been performed (to determine whether v6 is available or not).
/// </summary>
private static bool _hasResolvedIPv6Availability;

private const int FirstTryTimeout = 2000;

private static async ValueTask<Stream> 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<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;
}
}
}
3 changes: 1 addition & 2 deletions SS14.Launcher/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
Expand Down Expand Up @@ -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());
Expand Down

0 comments on commit c28ea15

Please sign in to comment.