2020-01-06 00:21:05 +00:00
|
|
|
|
using System;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Net;
|
2020-05-21 22:12:51 +00:00
|
|
|
|
using System.Net.Http;
|
2020-01-06 00:21:05 +00:00
|
|
|
|
using System.Text.RegularExpressions;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using System.Web;
|
|
|
|
|
using Newtonsoft.Json;
|
|
|
|
|
using Wabbajack.Common;
|
|
|
|
|
using Wabbajack.Lib.Validation;
|
|
|
|
|
|
|
|
|
|
namespace Wabbajack.Lib.Downloaders
|
|
|
|
|
{
|
|
|
|
|
// IPS4 is the site used by LoversLab, VectorPlexus, etc. the general mechanics of each site are the
|
|
|
|
|
// same, so we can fairly easily abstract the state.
|
|
|
|
|
// Pass in the state type via TState
|
2020-03-04 12:10:49 +00:00
|
|
|
|
public abstract class AbstractIPS4Downloader<TDownloader, TState> : AbstractNeedsLoginDownloader, IDownloader
|
|
|
|
|
where TState : AbstractIPS4Downloader<TDownloader, TState>.State<TDownloader>, new()
|
2020-01-06 00:21:05 +00:00
|
|
|
|
where TDownloader : IDownloader
|
|
|
|
|
{
|
2020-05-28 23:44:58 +00:00
|
|
|
|
protected AbstractIPS4Downloader(Uri loginUri, string encryptedKeyName, string cookieDomain, string loginCookie = "ips4_member_id")
|
|
|
|
|
: base(loginUri, encryptedKeyName, cookieDomain, loginCookie)
|
2020-04-09 20:20:34 +00:00
|
|
|
|
{
|
|
|
|
|
}
|
2020-01-22 19:57:48 +00:00
|
|
|
|
|
2020-04-09 20:20:34 +00:00
|
|
|
|
public async Task<AbstractDownloadState?> GetDownloaderState(dynamic archiveINI, bool quickMode)
|
2020-01-06 00:21:05 +00:00
|
|
|
|
{
|
|
|
|
|
Uri url = DownloaderUtils.GetDirectURL(archiveINI);
|
|
|
|
|
|
2020-01-27 17:16:44 +00:00
|
|
|
|
var absolute = true;
|
|
|
|
|
if (url == null || url.Host != SiteURL.Host) return null;
|
2020-04-04 17:40:58 +00:00
|
|
|
|
|
|
|
|
|
if (url.PathAndQuery.StartsWith("/applications/core/interface/file/attachment"))
|
|
|
|
|
{
|
|
|
|
|
return new TState
|
|
|
|
|
{
|
|
|
|
|
IsAttachment = true,
|
|
|
|
|
FullURL = url.ToString()
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-11 22:14:59 +00:00
|
|
|
|
if (url.PathAndQuery.StartsWith("/index.php?"))
|
|
|
|
|
{
|
|
|
|
|
var id2 = HttpUtility.ParseQueryString(url.Query)["r"];
|
|
|
|
|
var parsed = HttpUtility.ParseQueryString(url.Query);
|
|
|
|
|
var name = parsed[null].Split("/", StringSplitOptions.RemoveEmptyEntries).Last();
|
|
|
|
|
return new TState
|
|
|
|
|
{
|
2020-04-04 17:40:58 +00:00
|
|
|
|
FullURL = url.AbsolutePath,
|
2020-02-11 22:14:59 +00:00
|
|
|
|
FileID = id2,
|
|
|
|
|
FileName = name
|
|
|
|
|
};
|
|
|
|
|
}
|
2020-05-28 23:44:58 +00:00
|
|
|
|
|
|
|
|
|
if (url.PathAndQuery.StartsWith("/files/getdownload"))
|
|
|
|
|
{
|
|
|
|
|
return new TState
|
|
|
|
|
{
|
|
|
|
|
FullURL = url.ToString(),
|
|
|
|
|
IsAttachment = true
|
|
|
|
|
};
|
|
|
|
|
}
|
2020-02-11 22:14:59 +00:00
|
|
|
|
|
2020-01-27 17:16:44 +00:00
|
|
|
|
if (!url.PathAndQuery.StartsWith("/files/file/"))
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(url.Query)) return null;
|
|
|
|
|
if (!url.Query.Substring(1).StartsWith("/files/file/")) return null;
|
|
|
|
|
absolute = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var id = HttpUtility.ParseQueryString(url.Query)["r"];
|
|
|
|
|
var file = absolute
|
|
|
|
|
? url.AbsolutePath.Split('/').Last(s => s != "")
|
|
|
|
|
: url.Query.Substring(1).Split('/').Last(s => s != "");
|
|
|
|
|
|
2020-01-06 00:21:05 +00:00
|
|
|
|
return new TState
|
|
|
|
|
{
|
2020-04-04 17:40:58 +00:00
|
|
|
|
FullURL = url.AbsolutePath,
|
2020-01-06 00:21:05 +00:00
|
|
|
|
FileID = id,
|
|
|
|
|
FileName = file
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-09 20:20:34 +00:00
|
|
|
|
public class State<TStateDownloader> : AbstractDownloadState, IMetaState
|
|
|
|
|
where TStateDownloader : IDownloader
|
2020-01-06 00:21:05 +00:00
|
|
|
|
{
|
2020-04-09 20:20:34 +00:00
|
|
|
|
public string FullURL { get; set; } = string.Empty;
|
2020-04-04 17:40:58 +00:00
|
|
|
|
public bool IsAttachment { get; set; }
|
2020-04-09 20:20:34 +00:00
|
|
|
|
public string FileID { get; set; } = string.Empty;
|
|
|
|
|
|
|
|
|
|
public string FileName { get; set; } = string.Empty;
|
2020-03-26 23:02:15 +00:00
|
|
|
|
|
|
|
|
|
// from IMetaState
|
2020-04-23 10:38:43 +00:00
|
|
|
|
public Uri URL => IsAttachment ? new Uri("https://www.wabbajack.org/") : new Uri($"{Site}/files/file/{FileName}");
|
2020-04-09 20:20:34 +00:00
|
|
|
|
public string? Name { get; set; }
|
|
|
|
|
public string? Author { get; set; }
|
|
|
|
|
public string? Version { get; set; }
|
2020-04-15 12:05:05 +00:00
|
|
|
|
public Uri? ImageURL { get; set; }
|
2020-03-26 23:02:15 +00:00
|
|
|
|
public virtual bool IsNSFW { get; set; }
|
2020-04-09 20:20:34 +00:00
|
|
|
|
public string? Description { get; set; }
|
2020-01-06 00:21:05 +00:00
|
|
|
|
|
2020-01-22 19:57:48 +00:00
|
|
|
|
private static bool IsHTTPS => Downloader.SiteURL.AbsolutePath.StartsWith("https://");
|
|
|
|
|
private static string URLPrefix => IsHTTPS ? "https://" : "http://";
|
|
|
|
|
|
2020-04-06 20:48:54 +00:00
|
|
|
|
[JsonIgnore]
|
2020-03-04 12:10:49 +00:00
|
|
|
|
public static string Site => string.IsNullOrWhiteSpace(Downloader.SiteURL.Query)
|
2020-01-27 17:16:44 +00:00
|
|
|
|
? $"{URLPrefix}{Downloader.SiteURL.Host}"
|
|
|
|
|
: Downloader.SiteURL.ToString();
|
|
|
|
|
|
2020-03-04 12:10:49 +00:00
|
|
|
|
public static AbstractNeedsLoginDownloader Downloader => (AbstractNeedsLoginDownloader)(object)DownloadDispatcher.GetInstance<TDownloader>();
|
|
|
|
|
|
2020-04-06 20:48:54 +00:00
|
|
|
|
[JsonIgnore]
|
2020-01-11 04:15:53 +00:00
|
|
|
|
public override object[] PrimaryKey
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
2020-01-27 17:16:44 +00:00
|
|
|
|
return FileID == null
|
2020-04-04 17:40:58 +00:00
|
|
|
|
? IsAttachment
|
|
|
|
|
? new object[] {Downloader.SiteURL, IsAttachment, FullURL}
|
|
|
|
|
: new object[] {Downloader.SiteURL, FileName}
|
2020-01-27 17:16:44 +00:00
|
|
|
|
: new object[] {Downloader.SiteURL, FileName, FileID};
|
2020-01-11 04:15:53 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-01-06 00:21:05 +00:00
|
|
|
|
|
|
|
|
|
public override bool IsWhitelisted(ServerWhitelist whitelist)
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-25 22:30:43 +00:00
|
|
|
|
public override async Task<bool> Download(Archive a, AbsolutePath destination)
|
2020-01-06 00:21:05 +00:00
|
|
|
|
{
|
2020-05-21 22:12:51 +00:00
|
|
|
|
using var stream = await ResolveDownloadStream(a);
|
2020-04-09 20:20:34 +00:00
|
|
|
|
if (stream == null) return false;
|
2020-05-21 22:12:51 +00:00
|
|
|
|
await using var fromStream = await stream.Content.ReadAsStreamAsync();
|
2020-05-25 17:34:25 +00:00
|
|
|
|
await using var toStream = await destination.Create();
|
2020-05-21 22:12:51 +00:00
|
|
|
|
await fromStream.CopyToAsync(toStream);
|
2020-01-18 19:57:26 +00:00
|
|
|
|
return true;
|
2020-01-06 00:21:05 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-21 22:12:51 +00:00
|
|
|
|
private async Task<HttpResponseMessage?> ResolveDownloadStream(Archive a)
|
2020-01-06 00:21:05 +00:00
|
|
|
|
{
|
|
|
|
|
TOP:
|
2020-04-04 17:40:58 +00:00
|
|
|
|
string url;
|
|
|
|
|
if (IsAttachment)
|
|
|
|
|
{
|
|
|
|
|
url = FullURL;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
var csrfURL = FileID == null
|
|
|
|
|
? $"{Site}/files/file/{FileName}/?do=download"
|
|
|
|
|
: $"{Site}/files/file/{FileName}/?do=download&r={FileID}";
|
|
|
|
|
var html = await Downloader.AuthedClient.GetStringAsync(csrfURL);
|
2020-01-06 00:21:05 +00:00
|
|
|
|
|
2020-04-04 17:40:58 +00:00
|
|
|
|
var pattern = new Regex("(?<=csrfKey=).*(?=[&\"\'])|(?<=csrfKey: \").*(?=[&\"\'])");
|
|
|
|
|
var matches = pattern.Matches(html).Cast<Match>();
|
2020-01-06 00:21:05 +00:00
|
|
|
|
|
|
|
|
|
var csrfKey = matches.Where(m => m.Length == 32).Select(m => m.ToString()).FirstOrDefault();
|
|
|
|
|
|
2020-04-04 17:40:58 +00:00
|
|
|
|
if (csrfKey == null)
|
|
|
|
|
return null;
|
2020-01-06 00:21:05 +00:00
|
|
|
|
|
2020-04-04 17:40:58 +00:00
|
|
|
|
var sep = Site.EndsWith("?") ? "&" : "?";
|
|
|
|
|
url = FileID == null
|
|
|
|
|
? $"{Site}/files/file/{FileName}/{sep}do=download&confirm=1&t=1&csrfKey={csrfKey}"
|
|
|
|
|
: $"{Site}/files/file/{FileName}/{sep}do=download&r={FileID}&confirm=1&t=1&csrfKey={csrfKey}";
|
|
|
|
|
}
|
2020-01-06 00:21:05 +00:00
|
|
|
|
|
2020-03-04 12:10:49 +00:00
|
|
|
|
var streamResult = await Downloader.AuthedClient.GetAsync(url);
|
2020-01-06 00:21:05 +00:00
|
|
|
|
if (streamResult.StatusCode != HttpStatusCode.OK)
|
|
|
|
|
{
|
2020-03-04 12:10:49 +00:00
|
|
|
|
Utils.ErrorThrow(new InvalidOperationException(), $"{Downloader.SiteName} servers reported an error for file: {FileID}");
|
2020-01-06 00:21:05 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-27 17:16:44 +00:00
|
|
|
|
var contentType = streamResult.Content.Headers.ContentType;
|
2020-01-06 00:21:05 +00:00
|
|
|
|
|
2020-01-27 17:16:44 +00:00
|
|
|
|
if (contentType.MediaType != "application/json")
|
2020-04-24 01:58:18 +00:00
|
|
|
|
{
|
|
|
|
|
var headerVar = a.Size == 0 ? "1" : a.Size.ToString();
|
|
|
|
|
long headerContentSize = 0;
|
|
|
|
|
if (streamResult.Content.Headers.Contains("Content-Length"))
|
|
|
|
|
{
|
|
|
|
|
headerVar = streamResult.Content.Headers.GetValues("Content-Length").FirstOrDefault();
|
|
|
|
|
if (headerVar != null)
|
|
|
|
|
long.TryParse(headerVar, out headerContentSize);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (a.Size != 0 && headerContentSize != 0 && a.Size != headerContentSize)
|
|
|
|
|
return null;
|
|
|
|
|
|
2020-05-21 22:12:51 +00:00
|
|
|
|
return streamResult;
|
2020-04-24 01:58:18 +00:00
|
|
|
|
}
|
2020-01-27 17:16:44 +00:00
|
|
|
|
|
|
|
|
|
// Sometimes LL hands back a json object telling us to wait until a certain time
|
2020-04-06 20:48:54 +00:00
|
|
|
|
var times = (await streamResult.Content.ReadAsStringAsync()).FromJsonString<WaitResponse>();
|
2020-01-27 17:16:44 +00:00
|
|
|
|
var secs = times.Download - times.CurrentTime;
|
|
|
|
|
for (int x = 0; x < secs; x++)
|
2020-01-06 00:21:05 +00:00
|
|
|
|
{
|
2020-03-04 12:10:49 +00:00
|
|
|
|
Utils.Status($"Waiting for {secs} at the request of {Downloader.SiteName}", Percent.FactoryPutInRange(x, secs));
|
2020-01-27 17:16:44 +00:00
|
|
|
|
await Task.Delay(1000);
|
2020-01-06 00:21:05 +00:00
|
|
|
|
}
|
2020-02-26 05:05:33 +00:00
|
|
|
|
streamResult.Dispose();
|
2020-01-27 17:16:44 +00:00
|
|
|
|
Utils.Status("Retrying download");
|
|
|
|
|
goto TOP;
|
2020-01-06 00:21:05 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private class WaitResponse
|
|
|
|
|
{
|
|
|
|
|
[JsonProperty("download")]
|
|
|
|
|
public int Download { get; set; }
|
|
|
|
|
[JsonProperty("currentTime")]
|
|
|
|
|
public int CurrentTime { get; set; }
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-13 22:55:55 +00:00
|
|
|
|
public override async Task<bool> Verify(Archive a)
|
2020-01-06 00:21:05 +00:00
|
|
|
|
{
|
2020-04-24 01:58:18 +00:00
|
|
|
|
var stream = await ResolveDownloadStream(a);
|
2020-01-06 00:21:05 +00:00
|
|
|
|
if (stream == null)
|
|
|
|
|
return false;
|
|
|
|
|
|
2020-05-21 22:12:51 +00:00
|
|
|
|
stream.Dispose();
|
2020-01-06 00:21:05 +00:00
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override IDownloader GetDownloader()
|
|
|
|
|
{
|
|
|
|
|
return DownloadDispatcher.GetInstance<TDownloader>();
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-02 12:23:07 +00:00
|
|
|
|
public override string GetManifestURL(Archive a)
|
2020-02-02 12:15:29 +00:00
|
|
|
|
{
|
2020-04-04 17:53:05 +00:00
|
|
|
|
return IsAttachment ? FullURL : $"{Site}/files/file/{FileName}/?do=download&r={FileID}";
|
2020-02-02 12:15:29 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-11 04:15:53 +00:00
|
|
|
|
public override string[] GetMetaIni()
|
|
|
|
|
{
|
2020-04-04 17:40:58 +00:00
|
|
|
|
if (IsAttachment)
|
|
|
|
|
return new[] {"[General]", $"directURL={FullURL}"};
|
|
|
|
|
|
|
|
|
|
if (FileID == null)
|
|
|
|
|
return new[] {"[General]", $"directURL={Site}/files/file/{FileName}"};
|
2020-02-11 22:14:59 +00:00
|
|
|
|
|
2020-04-04 17:40:58 +00:00
|
|
|
|
if (Site.EndsWith("?"))
|
|
|
|
|
{
|
2020-01-11 04:15:53 +00:00
|
|
|
|
return new[]
|
|
|
|
|
{
|
2020-04-04 17:40:58 +00:00
|
|
|
|
"[General]", $"directURL={Site}/files/file/{FileName}&do=download&r={FileID}&confirm=1&t=1"
|
2020-01-11 04:15:53 +00:00
|
|
|
|
};
|
2020-04-04 17:40:58 +00:00
|
|
|
|
|
2020-02-11 22:14:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-11 04:15:53 +00:00
|
|
|
|
return new[]
|
|
|
|
|
{
|
2020-04-04 17:40:58 +00:00
|
|
|
|
"[General]", $"directURL={Site}/files/file/{FileName}/?do=download&r={FileID}&confirm=1&t=1"
|
2020-01-11 04:15:53 +00:00
|
|
|
|
};
|
2020-04-04 17:40:58 +00:00
|
|
|
|
|
2020-01-11 04:15:53 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-04 12:10:49 +00:00
|
|
|
|
public virtual async Task<bool> LoadMetaData()
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2020-01-06 00:21:05 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|