2022-02-11 23:40:38 +00:00
|
|
|
using System.IO.Compression;
|
|
|
|
using System.Security.Cryptography;
|
|
|
|
using CS_AES_CTR;
|
|
|
|
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
|
2022-02-11 05:05:51 +00:00
|
|
|
using Microsoft.Extensions.Logging;
|
2022-02-11 14:03:16 +00:00
|
|
|
using Wabbajack.Common;
|
2022-02-11 05:05:51 +00:00
|
|
|
using Wabbajack.Downloaders.Interfaces;
|
|
|
|
using Wabbajack.DTOs;
|
|
|
|
using Wabbajack.DTOs.DownloadStates;
|
|
|
|
using Wabbajack.DTOs.Validation;
|
|
|
|
using Wabbajack.Hashing.xxHash64;
|
|
|
|
using Wabbajack.Networking.BethesdaNet;
|
2022-02-11 14:03:16 +00:00
|
|
|
using Wabbajack.Networking.BethesdaNet.DTOs;
|
|
|
|
using Wabbajack.Networking.Http;
|
2022-02-11 05:05:51 +00:00
|
|
|
using Wabbajack.Paths;
|
|
|
|
using Wabbajack.Paths.IO;
|
|
|
|
using Wabbajack.RateLimiter;
|
|
|
|
|
|
|
|
namespace Wabbajack.Downloaders.Bethesda;
|
|
|
|
|
|
|
|
public class BethesdaDownloader : ADownloader<DTOs.DownloadStates.Bethesda>, IUrlDownloader, IChunkedSeekableStreamDownloader
|
|
|
|
{
|
|
|
|
private readonly Client _client;
|
|
|
|
private readonly IResource<HttpClient> _limiter;
|
|
|
|
private readonly HttpClient _httpClient;
|
|
|
|
private readonly ILogger<BethesdaDownloader> _logger;
|
|
|
|
|
|
|
|
public BethesdaDownloader(ILogger<BethesdaDownloader> logger, Client client, HttpClient httpClient, IResource<HttpClient> limiter)
|
|
|
|
{
|
|
|
|
_logger = logger;
|
|
|
|
_client = client;
|
|
|
|
_limiter = limiter;
|
|
|
|
_httpClient = httpClient;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public override async Task<Hash> Download(Archive archive, DTOs.DownloadStates.Bethesda state, AbsolutePath destination, IJob job, CancellationToken token)
|
|
|
|
{
|
|
|
|
|
|
|
|
var depot = await _client.GetDepots(state, token);
|
2022-02-11 14:03:16 +00:00
|
|
|
var tree = await _client.GetTree(state, token);
|
2022-02-11 05:05:51 +00:00
|
|
|
|
2022-02-11 14:03:16 +00:00
|
|
|
var chunks = tree!.DepotList.First().FileList.First().ChunkList;
|
|
|
|
|
2022-02-11 23:40:38 +00:00
|
|
|
using var os = destination.Open(FileMode.Create, FileAccess.ReadWrite, FileShare.Read);
|
|
|
|
|
|
|
|
var hasher = new xxHashAlgorithm(0);
|
|
|
|
Hash finalHash = default;
|
|
|
|
|
2022-10-07 22:14:01 +00:00
|
|
|
var aesKey = depot!.ExInfoA.ToArray();
|
2022-02-11 23:40:38 +00:00
|
|
|
var aesIV = depot.ExInfoB.Take(16).ToArray();
|
|
|
|
|
2022-02-11 14:03:16 +00:00
|
|
|
await chunks.PMapAll(async chunk =>
|
|
|
|
{
|
2022-02-11 23:40:38 +00:00
|
|
|
var data = await GetChunk(state, chunk, depot.PropertiesId, token);
|
2022-02-11 14:03:16 +00:00
|
|
|
var reported = job.Report(data.Length, token);
|
|
|
|
|
2022-02-11 23:40:38 +00:00
|
|
|
var aesCtr = new AES_CTR(aesKey, aesIV, false);
|
|
|
|
data = aesCtr.DecryptBytes(data);
|
|
|
|
|
|
|
|
if (chunk.UncompressedSize != chunk.ChunkSize)
|
|
|
|
{
|
|
|
|
var inflater = new InflaterInputStream(new MemoryStream(data));
|
|
|
|
data = await inflater.ReadAllAsync();
|
|
|
|
}
|
|
|
|
|
2022-02-11 14:03:16 +00:00
|
|
|
await reported;
|
|
|
|
return data;
|
2022-02-11 23:40:38 +00:00
|
|
|
})
|
|
|
|
.Do(async data =>
|
|
|
|
{
|
|
|
|
if (data.Length < tree.DepotList.First().BytesPerChunk)
|
|
|
|
{
|
|
|
|
hasher.HashBytes(data);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
finalHash = Hash.FromULong(hasher.FinalizeHashValueInternal(data));
|
|
|
|
}
|
|
|
|
|
|
|
|
await os.WriteAsync(data, token);
|
2022-02-11 14:03:16 +00:00
|
|
|
});
|
|
|
|
|
2022-02-11 23:40:38 +00:00
|
|
|
return finalHash;
|
2022-02-11 05:05:51 +00:00
|
|
|
}
|
|
|
|
|
2022-02-11 23:40:38 +00:00
|
|
|
private async Task<byte[]> GetChunk(DTOs.DownloadStates.Bethesda state, Chunk chunk, long propertiesId,
|
2022-02-11 14:03:16 +00:00
|
|
|
CancellationToken token)
|
|
|
|
{
|
2022-02-11 23:40:38 +00:00
|
|
|
var uri = new Uri($"https://content.cdp.bethesda.net/{state.ProductId}/{propertiesId}/{chunk.Sha}");
|
2022-02-11 14:03:16 +00:00
|
|
|
var msg = new HttpRequestMessage(HttpMethod.Get, uri);
|
|
|
|
msg.Headers.Add("User-Agent", "bnet");
|
|
|
|
using var job = await _limiter.Begin("Getting chunk", chunk.ChunkSize, token);
|
2022-02-11 23:40:38 +00:00
|
|
|
using var response = await _httpClient.SendAsync(msg, token);
|
2022-02-11 14:03:16 +00:00
|
|
|
if (!response.IsSuccessStatusCode)
|
|
|
|
throw new HttpException(response);
|
|
|
|
await job.Report(chunk.ChunkSize, token);
|
|
|
|
return await response.Content.ReadAsByteArrayAsync(token);
|
|
|
|
}
|
|
|
|
|
2022-02-11 05:05:51 +00:00
|
|
|
public override async Task<bool> Prepare()
|
|
|
|
{
|
2022-10-07 22:14:01 +00:00
|
|
|
await _client.CdpAuth(CancellationToken.None);
|
2022-02-11 05:05:51 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
public override bool IsAllowed(ServerAllowList allowList, IDownloadState state)
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
public override IDownloadState? Resolve(IReadOnlyDictionary<string, string> iniData)
|
|
|
|
{
|
2022-08-21 20:23:11 +00:00
|
|
|
if (iniData.ContainsKey("directURL") && Uri.TryCreate(iniData["directURL"].CleanIniString(), UriKind.Absolute, out var uri))
|
2022-02-11 05:05:51 +00:00
|
|
|
{
|
|
|
|
return Parse(uri);
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
public override Priority Priority => Priority.Normal;
|
|
|
|
|
|
|
|
public override async Task<bool> Verify(Archive archive, DTOs.DownloadStates.Bethesda state, IJob job, CancellationToken token)
|
|
|
|
{
|
2022-02-11 14:03:16 +00:00
|
|
|
var depot = await _client.GetDepots(state, token);
|
|
|
|
return depot != null;
|
2022-02-11 05:05:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public override IEnumerable<string> MetaIni(Archive a, DTOs.DownloadStates.Bethesda state)
|
|
|
|
{
|
|
|
|
return new[] {$"directURL={UnParse(state)}"};
|
|
|
|
}
|
|
|
|
|
|
|
|
public IDownloadState? Parse(Uri uri)
|
|
|
|
{
|
|
|
|
if (uri.Scheme != "bethesda") return null;
|
|
|
|
var path = uri.PathAndQuery.Split("/", StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
if (path.Length != 4) return null;
|
|
|
|
var game = GameRegistry.TryGetByFuzzyName(uri.Host);
|
|
|
|
if (game == null) return null;
|
|
|
|
|
|
|
|
if (!long.TryParse(path[1], out var productId)) return null;
|
|
|
|
if (!long.TryParse(path[2], out var branchId)) return null;
|
|
|
|
|
|
|
|
bool isCCMod = false;
|
|
|
|
switch (path[0])
|
|
|
|
{
|
|
|
|
case "cc":
|
|
|
|
isCCMod = true;
|
|
|
|
break;
|
|
|
|
case "mod":
|
|
|
|
isCCMod = false;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return new DTOs.DownloadStates.Bethesda
|
|
|
|
{
|
|
|
|
Game = game.Game,
|
|
|
|
IsCCMod = isCCMod,
|
|
|
|
ProductId = productId,
|
2022-02-11 23:40:38 +00:00
|
|
|
BranchId = branchId,
|
2022-02-11 05:05:51 +00:00
|
|
|
ContentId = path[3]
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
public Uri UnParse(IDownloadState state)
|
|
|
|
{
|
|
|
|
var cstate = (DTOs.DownloadStates.Bethesda) state;
|
2022-02-11 23:40:38 +00:00
|
|
|
return new Uri($"bethesda://{cstate.Game}/{(cstate.IsCCMod ? "cc" : "mod")}/{cstate.ProductId}/{cstate.BranchId}/{cstate.ContentId}");
|
2022-02-11 05:05:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public ValueTask<Stream> GetChunkedSeekableStream(Archive archive, CancellationToken token)
|
|
|
|
{
|
|
|
|
throw new NotImplementedException();
|
|
|
|
}
|
|
|
|
}
|