Skip to content

Commit

Permalink
Third review changes
Browse files Browse the repository at this point in the history
  • Loading branch information
tpill90 committed Nov 14, 2024
1 parent 04df3b5 commit 080c383
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 96 deletions.
45 changes: 13 additions & 32 deletions SteamKit2/SteamKit2/Steam/CDN/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,13 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using SteamKit2.Steam.CDN;

namespace SteamKit2.CDN
{
/// <summary>
/// The <see cref="Client"/> class is used for downloading game content from the Steam servers.
/// </summary>
public sealed class Client : IDisposable
public partial class Client : IDisposable
{
HttpClient httpClient;

Expand All @@ -30,9 +29,6 @@ public sealed class Client : IDisposable
/// </summary>
public static TimeSpan ResponseBodyTimeout { get; set; } = TimeSpan.FromSeconds( 60 );

private static bool CheckedForLancacheServer { get; set; }
private static bool LancacheDetected { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="Client"/> class.
/// </summary>
Expand Down Expand Up @@ -230,23 +226,18 @@ public async Task<int> DownloadDepotChunkAsync( uint depotId, DepotManifest.Chun
}
}

if ( !CheckedForLancacheServer )
{
LancacheDetected = LancacheDetector.DetectLancacheServer();
CheckedForLancacheServer = true;
}


var chunkID = Utils.EncodeHexString( chunk.ChunkID );
var url = $"depot/{depotId}/chunk/{chunkID}";

var builtUrl = BuildCommand( server, url, cdnAuthToken, proxyServer );
using var request = new HttpRequestMessage( HttpMethod.Get, builtUrl );
if ( LancacheDetected )
HttpRequestMessage request;
if ( UseLancacheServer )
{
request = BuildLancacheRequest( server, url, cdnAuthToken );
}
else
{
request.Headers.Host = server.Host;
// User agent must match the Steam client in order for Lancache to correctly identify and cache Valve's CDN content
request.Headers.Add( "User-Agent", "Valve/Steam HTTP Client 1.0" );
var builtUrl = BuildCommand( server, url, cdnAuthToken, proxyServer );
request = new HttpRequestMessage( HttpMethod.Get, builtUrl );
}

using var cts = new CancellationTokenSource();
Expand Down Expand Up @@ -330,24 +321,14 @@ public async Task<int> DownloadDepotChunkAsync( uint depotId, DepotManifest.Chun
DebugLog.WriteLine( nameof( CDN ), $"Failed to download a depot chunk {request.RequestUri}: {ex.Message}" );
throw;
}
finally
{
request.Dispose();
}
}

static Uri BuildCommand( Server server, string command, string? query, Server? proxyServer )
{
// If a Lancache instance is detected in Steam it will take priority over all other available servers
if ( LancacheDetected )
{
var builder = new UriBuilder()
{
Scheme = "http",
Host = "lancache.steamcontent.com",
Port = 80,
Path = command,
Query = query ?? string.Empty
};
return builder.Uri;
}

var uriBuilder = new UriBuilder
{
Scheme = server.Protocol == Server.ConnectionProtocol.HTTP ? "http" : "https",
Expand Down
109 changes: 109 additions & 0 deletions SteamKit2/SteamKit2/Steam/CDN/ClientLancache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* This file is subject to the terms and conditions defined in
* file 'license.txt', which is part of this source code package.
*/

using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using System.Threading.Tasks;

namespace SteamKit2.CDN
{
public partial class Client
{
/// <summary>
/// When set to true, will attempt to download from a Lancache instance on the LAN rather than going out to Steam's CDNs.
/// </summary>
public static bool UseLancacheServer { get; private set; }

private static string TriggerDomain = "lancache.steamcontent.com";

/// <summary>
/// Attempts to automatically resolve a Lancache on the local network. If detected, SteamKit will route all downloads through the cache
/// rather than through Steam's CDN.
///
/// Will try to detect the Lancache through the poisoned DNS entries, however if that is not possible it will then check
/// 'localhost' to see if the Lancache is available locally. If the server is not available on 'localhost', then 172.17.0.1 will be checked to see if
/// the prefill is running from a docker container.
///
/// This is a modified version from the original source : https://github.com/tpill90/lancache-prefill-common/blob/main/dotnet/LancacheIpResolver.cs
/// </summary>
public static async Task DetectLancacheServerAsync()
{
var ipAddresses = ( await Dns.GetHostAddressesAsync( TriggerDomain ) )
.Where( e => e.AddressFamily == AddressFamily.InterNetwork || e.AddressFamily == AddressFamily.InterNetworkV6 )
.ToArray();

if ( ipAddresses.Any( e => IsPrivateAddress(e) ) )
{
UseLancacheServer = true;
return;
}

//If there are no private IPs, then there can't be a Lancache instance. Lancache's IP must resolve to a private RFC 1918 address.
UseLancacheServer = false;
}

/// <summary>
/// Determines if an IP address is a private address, as specified in RFC1918
/// </summary>
/// <param name="toTest">The IP address that will be tested</param>
/// <returns>Returns true if the IP is a private address, false if it isn't private</returns>
internal static bool IsPrivateAddress( IPAddress toTest )
{
if ( IPAddress.IsLoopback( toTest ) )
{
return true;
}

byte[] bytes = toTest.GetAddressBytes();

// IPv4
if ( toTest.AddressFamily == AddressFamily.InterNetwork )
{
switch ( bytes[ 0 ] )
{
case 10:
return true;
case 172:
return bytes[ 1 ] >= 16 && bytes[ 1 ] < 32;
case 192:
return bytes[ 1 ] == 168;
default:
return false;
}
}

// IPv6
if ( toTest.AddressFamily == AddressFamily.InterNetworkV6 )
{
// Check for Unique Local Address (fc00::/7) and loopback (::1)
return ( bytes[ 0 ] & 0xFE ) == 0xFC || toTest.IsIPv6LinkLocal;
}

return false;
}

static HttpRequestMessage BuildLancacheRequest( Server server, string command, string? query)
{
var builder = new UriBuilder
{
Scheme = "http",
Host = "lancache.steamcontent.com",
Port = 80,
Path = command,
Query = query ?? string.Empty
};

var request = new HttpRequestMessage( HttpMethod.Get, builder.Uri );
request.Headers.Host = server.Host;
// User agent must match the Steam client in order for Lancache to correctly identify and cache Valve's CDN content
request.Headers.Add( "User-Agent", "Valve/Steam HTTP Client 1.0" );

return request;
}
}
}
64 changes: 0 additions & 64 deletions SteamKit2/SteamKit2/Steam/CDN/LancacheDetector.cs

This file was deleted.

27 changes: 27 additions & 0 deletions SteamKit2/Tests/CDNClientFacts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,33 @@ public async Task ThrowsWhenDestinationBufferSmallerWithDepotKey()
Assert.Equal( "destination", ex.ParamName );
}

[Theory]
[InlineData( "10.0.0.1", true )] // Private IPv4 (10.0.0.0/8)
[InlineData( "172.16.0.1", true )] // Private IPv4 (172.16.0.0/12)
[InlineData( "192.168.0.1", true )] // Private IPv4 (192.168.0.0/16)
[InlineData( "8.8.8.8", false )] // Public IPv4
[InlineData( "127.0.0.1", true )] // Loopback IPv4
public void IsPrivateAddress_IPv4Tests( string ipAddress, bool expected )
{
IPAddress address = IPAddress.Parse( ipAddress );
bool result = Client.IsPrivateAddress( address );

Assert.Equal( expected, result );
}

[Theory]
[InlineData( "fc00::1", true )] // Private IPv6 (Unique Local Address)
[InlineData( "fe80::1", true )] // Link-local IPv6
[InlineData( "2001:db8::1", false )] // Public IPv6
[InlineData( "::1", true )] // Loopback IPv6
public void IsPrivateAddress_IPv6Tests( string ipAddress, bool expected )
{
IPAddress address = IPAddress.Parse( ipAddress );
bool result = Client.IsPrivateAddress( address );

Assert.Equal( expected, result );
}

sealed class TeapotHttpMessageHandler : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken )
Expand Down

0 comments on commit 080c383

Please sign in to comment.