|
|
|
|
@ -5,91 +5,46 @@ using System.Linq;
|
|
|
|
|
using System.Net;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using SteamKit2;
|
|
|
|
|
using SteamKit2.CDN;
|
|
|
|
|
using SteamPrefill.Handlers;
|
|
|
|
|
|
|
|
|
|
namespace DepotDownloader
|
|
|
|
|
namespace LancachePrefill.Common
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// CDNClientPool provides a pool of connections to CDN endpoints, requesting CDN tokens as needed
|
|
|
|
|
/// </summary>
|
|
|
|
|
class CDNClientPool
|
|
|
|
|
public class CDNClientPool
|
|
|
|
|
{
|
|
|
|
|
private const int ServerEndpointMinimumSize = 8;
|
|
|
|
|
|
|
|
|
|
private readonly Steam3Session steamSession;
|
|
|
|
|
private readonly uint appId;
|
|
|
|
|
public Client CDNClient { get; }
|
|
|
|
|
public Server ProxyServer { get; private set; }
|
|
|
|
|
|
|
|
|
|
private readonly ConcurrentStack<Server> activeConnectionPool = new ConcurrentStack<Server>();
|
|
|
|
|
private readonly BlockingCollection<Server> availableServerEndpoints = new BlockingCollection<Server>();
|
|
|
|
|
|
|
|
|
|
private readonly AutoResetEvent populatePoolEvent = new AutoResetEvent(true);
|
|
|
|
|
private readonly Task monitorTask;
|
|
|
|
|
private readonly CancellationTokenSource shutdownToken = new CancellationTokenSource();
|
|
|
|
|
private readonly IAnsiConsole _ansiConsole;
|
|
|
|
|
private readonly string _cdnUrl;
|
|
|
|
|
private readonly ConcurrentStack<Server> _activeConnectionPool = new ConcurrentStack<Server>();
|
|
|
|
|
private readonly BlockingCollection<Server> _availableServerEndpoints = new BlockingCollection<Server>();
|
|
|
|
|
private readonly AutoResetEvent _populatePoolEvent = new AutoResetEvent(true);
|
|
|
|
|
private readonly Task _monitorTask;
|
|
|
|
|
private readonly CancellationTokenSource _shutdownToken = new CancellationTokenSource();
|
|
|
|
|
public CancellationTokenSource ExhaustedToken { get; set; }
|
|
|
|
|
|
|
|
|
|
public CDNClientPool(Steam3Session steamSession, uint appId)
|
|
|
|
|
public CDNClientPool(IAnsiConsole ansiConsole, string cdnUrl)
|
|
|
|
|
{
|
|
|
|
|
this.steamSession = steamSession;
|
|
|
|
|
this.appId = appId;
|
|
|
|
|
CDNClient = new Client(steamSession.steamClient);
|
|
|
|
|
_ansiConsole = ansiConsole;
|
|
|
|
|
_cdnUrl = cdnUrl;
|
|
|
|
|
|
|
|
|
|
monitorTask = Task.Factory.StartNew(ConnectionPoolMonitorAsync, TaskCreationOptions.LongRunning);
|
|
|
|
|
_monitorTask = Task.Factory.StartNew(ConnectionPoolMonitorAsync, TaskCreationOptions.LongRunning);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Shutdown()
|
|
|
|
|
{
|
|
|
|
|
shutdownToken.Cancel();
|
|
|
|
|
monitorTask.Wait();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task<IReadOnlyCollection<Server>> FetchBootstrapServerListAsync()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var cdnServers = await steamSession.steamContent.GetServersForSteamPipe().ConfigureAwait(false);
|
|
|
|
|
return cdnServers;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine("Failed to retrieve content server list: {0}", ex.Message);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task<IPAddress> ResolveLancacheIpAsync(string hostname)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var hostEntry = await Dns.GetHostEntryAsync(hostname).ConfigureAwait(false);
|
|
|
|
|
return hostEntry.AddressList.FirstOrDefault(ip => IsPrivateIp(ip));
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine("Failed to resolve Lancache IP: {0}", ex.Message);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool IsPrivateIp(IPAddress ip)
|
|
|
|
|
{
|
|
|
|
|
byte[] bytes = ip.GetAddressBytes();
|
|
|
|
|
return bytes[0] == 10 ||
|
|
|
|
|
(bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31) ||
|
|
|
|
|
(bytes[0] == 192 && bytes[1] == 168);
|
|
|
|
|
_shutdownToken.Cancel();
|
|
|
|
|
_monitorTask.Wait();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task ConnectionPoolMonitorAsync()
|
|
|
|
|
{
|
|
|
|
|
var didPopulate = false;
|
|
|
|
|
|
|
|
|
|
while (!shutdownToken.IsCancellationRequested)
|
|
|
|
|
while (!_shutdownToken.IsCancellationRequested)
|
|
|
|
|
{
|
|
|
|
|
populatePoolEvent.WaitOne(TimeSpan.FromSeconds(1));
|
|
|
|
|
_populatePoolEvent.WaitOne(TimeSpan.FromSeconds(1));
|
|
|
|
|
|
|
|
|
|
if (availableServerEndpoints.Count < ServerEndpointMinimumSize && steamSession.steamClient.IsConnected)
|
|
|
|
|
if (_availableServerEndpoints.Count < ServerEndpointMinimumSize)
|
|
|
|
|
{
|
|
|
|
|
var servers = await FetchBootstrapServerListAsync().ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
@ -99,45 +54,29 @@ namespace DepotDownloader
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ProxyServer = servers.FirstOrDefault(x => x.UseAsProxy);
|
|
|
|
|
|
|
|
|
|
foreach (var server in servers)
|
|
|
|
|
{
|
|
|
|
|
if (server.Type == "SteamCache")
|
|
|
|
|
var resolvedIp = await ResolveLancacheIpAsync(server.Host).ConfigureAwait(false);
|
|
|
|
|
if (resolvedIp != null && IsPrivateIp(resolvedIp))
|
|
|
|
|
{
|
|
|
|
|
var lancacheIp = await ResolveLancacheIpAsync(server.Host).ConfigureAwait(false);
|
|
|
|
|
if (lancacheIp != null && IsPrivateIp(lancacheIp))
|
|
|
|
|
var lancacheServer = new Server
|
|
|
|
|
{
|
|
|
|
|
var lancacheServer = new Server
|
|
|
|
|
{
|
|
|
|
|
Host = lancacheIp.ToString(),
|
|
|
|
|
Type = server.Type,
|
|
|
|
|
NumEntries = server.NumEntries,
|
|
|
|
|
WeightedLoad = server.WeightedLoad,
|
|
|
|
|
AllowedAppIds = server.AllowedAppIds.ToArray(),
|
|
|
|
|
Protocol = Server.ConnectionProtocol.HTTP // Downgrade to HTTP
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Console.WriteLine($"Found Lancache Server: {lancacheServer.Host}. Downgrading connection to HTTP.");
|
|
|
|
|
availableServerEndpoints.Add(lancacheServer);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
var isEligibleForApp = server.AllowedAppIds.Length == 0 || server.AllowedAppIds.Contains(appId);
|
|
|
|
|
if (isEligibleForApp && (server.Type == "SteamCache" || server.Type == "CDN"))
|
|
|
|
|
{
|
|
|
|
|
for (var i = 0; i < server.NumEntries; i++)
|
|
|
|
|
{
|
|
|
|
|
availableServerEndpoints.Add(server);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Host = resolvedIp.ToString(),
|
|
|
|
|
Type = server.Type,
|
|
|
|
|
NumEntries = server.NumEntries,
|
|
|
|
|
WeightedLoad = server.WeightedLoad,
|
|
|
|
|
AllowedAppIds = server.AllowedAppIds.ToArray(),
|
|
|
|
|
Protocol = Server.ConnectionProtocol.HTTP // Downgrade to HTTP
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_ansiConsole.MarkupLine($"Found Lancache Server: {_cdnUrl}. Downgrading connection to HTTP.");
|
|
|
|
|
_availableServerEndpoints.Add(lancacheServer);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
didPopulate = true;
|
|
|
|
|
}
|
|
|
|
|
else if (availableServerEndpoints.Count == 0 && !steamSession.steamClient.IsConnected && didPopulate)
|
|
|
|
|
else if (_availableServerEndpoints.Count == 0 && didPopulate)
|
|
|
|
|
{
|
|
|
|
|
ExhaustedToken?.Cancel();
|
|
|
|
|
return;
|
|
|
|
|
@ -145,39 +84,39 @@ namespace DepotDownloader
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Server BuildConnection(CancellationToken token)
|
|
|
|
|
private async Task<IReadOnlyCollection<Server>> FetchBootstrapServerListAsync()
|
|
|
|
|
{
|
|
|
|
|
if (availableServerEndpoints.Count < ServerEndpointMinimumSize)
|
|
|
|
|
{
|
|
|
|
|
populatePoolEvent.Set();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return availableServerEndpoints.Take(token);
|
|
|
|
|
// Implement logic to fetch the CDN server list
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<DepotChunk> DownloadDepotChunkAsync(uint depotId, DepotManifest.ChunkData chunkData, Server connection, byte[] depotKey)
|
|
|
|
|
private async Task<IPAddress> ResolveLancacheIpAsync(string hostname)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
// Call the DownloadDepotChunkAsync method of CDNClient
|
|
|
|
|
var chunk = await CDNClient.DownloadDepotChunkAsync(depotId, chunkData, connection, depotKey);
|
|
|
|
|
|
|
|
|
|
// Return the downloaded chunk
|
|
|
|
|
return chunk;
|
|
|
|
|
var hostEntry = await Dns.GetHostEntryAsync(hostname).ConfigureAwait(false);
|
|
|
|
|
return hostEntry.AddressList.FirstOrDefault(ip => IsPrivateIp(ip));
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
// Handle any exceptions that occur during chunk download
|
|
|
|
|
Console.WriteLine($"Error downloading chunk: {ex.Message}");
|
|
|
|
|
_ansiConsole.MarkupLine($"Failed to resolve Lancache IP: {ex.Message}");
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool IsPrivateIp(IPAddress ip)
|
|
|
|
|
{
|
|
|
|
|
byte[] bytes = ip.GetAddressBytes();
|
|
|
|
|
return bytes[0] == 10 ||
|
|
|
|
|
(bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31) ||
|
|
|
|
|
(bytes[0] == 192 && bytes[1] == 168);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Server GetConnection(CancellationToken token)
|
|
|
|
|
{
|
|
|
|
|
if (!activeConnectionPool.TryPop(out var connection))
|
|
|
|
|
if (!_activeConnectionPool.TryPop(out var connection))
|
|
|
|
|
{
|
|
|
|
|
connection = BuildConnection(token);
|
|
|
|
|
connection = _availableServerEndpoints.Take(token);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return connection;
|
|
|
|
|
@ -187,14 +126,12 @@ namespace DepotDownloader
|
|
|
|
|
{
|
|
|
|
|
if (server == null) return;
|
|
|
|
|
|
|
|
|
|
activeConnectionPool.Push(server);
|
|
|
|
|
_activeConnectionPool.Push(server);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void ReturnBrokenConnection(Server server)
|
|
|
|
|
{
|
|
|
|
|
if (server == null) return;
|
|
|
|
|
|
|
|
|
|
// Broken connections are not returned to the pool
|
|
|
|
|
// Implement logic to handle broken connections
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|