Files
KfChatDotNet/KfChatDotNetBot/Services/KfTokenService.cs
barelyprofessional 4c8cbc1748 Actually save cookies
2026-02-08 20:40:39 -06:00

194 lines
7.7 KiB
C#

using System.Net;
using System.Text.Json;
using HtmlAgilityPack;
using KfChatDotNetBot.Settings;
using NLog;
namespace KfChatDotNetBot.Services;
public class KfTokenService
{
private readonly Logger _logger = LogManager.GetCurrentClassLogger();
private readonly CancellationToken _ctx;
private CookieContainer _cookies = new();
private KiwiFlare _kiwiFlare;
private readonly string _kfDomain;
private readonly string? _proxy;
public KfTokenService(string kfDomain, string? proxy = null, CancellationToken? cancellationToken = null)
{
_ctx = cancellationToken ?? CancellationToken.None;
_kiwiFlare = new KiwiFlare(kfDomain, proxy, cancellationToken);
_proxy = proxy;
_kfDomain = kfDomain;
var cachedCookies = SettingsProvider.GetValueAsync(BuiltIn.Keys.KiwiFarmsCookies).Result
.JsonDeserialize<Dictionary<string, string>>();
// This shouldn't happen as the setting's default value is {}, but I'm just doing it to shut the IDE up
if (cachedCookies == null) return;
foreach (var key in cachedCookies.Keys)
{
_cookies.Add(new Cookie(key, cachedCookies[key], "/", _kfDomain));
}
}
private HttpClientHandler GetHttpClientHandler()
{
var handler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All };
if (_proxy != null)
{
handler.Proxy = new WebProxy(_proxy);
handler.UseProxy = true;
}
handler.CookieContainer = _cookies;
return handler;
}
private async Task GetNewClearanceToken()
{
_logger.Debug("Getting a new clearance token");
var i = 0;
// Shitty retry logic as the forum is still annoyingly unstable
while (i < 10)
{
i++;
try
{
var challenge = await _kiwiFlare.GetChallenge();
if (challenge == null)
{
_logger.Error("Challenge data was null. Might be KiwiFlare is only partially enabled? Not going to do anything.");
return;
}
var solution = await _kiwiFlare.SolveChallenge(challenge);
string token;
if (challenge.IsTtrs)
{
token = await _kiwiFlare.SubmitAnswerTtrs(solution);
_cookies.Add(new Cookie("ttrs_clearance", token, "/", _kfDomain));
}
else
{
token = await _kiwiFlare.SubmitAnswer(solution);
_cookies.Add(new Cookie("sssg_clearance", token, "/", _kfDomain));
}
_logger.Debug("Successfully retrieved a new token and added to the cookie container");
return;
}
catch (Exception e)
{
_logger.Error($"Failed to solve the KiwiFlare challenge, attempt {i} of 10");
_logger.Error(e);
}
}
_logger.Error("Ran out of attempts");
throw new Exception("Failed to solve the challenge");
}
private async Task<string> GetLoginPage()
{
using var client = new HttpClient(GetHttpClientHandler());
var response = await client.GetAsync($"https://{_kfDomain}/login", _ctx);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync(_ctx);
if (response.StatusCode == HttpStatusCode.NonAuthoritativeInformation)
{
_logger.Info("Caught a 203 response when trying to load the logon page which means we have to solve a KiwiFlare challenge");
await GetNewClearanceToken();
_logger.Info("Solved the challenge, now going to grab the login page again");
response = await client.GetAsync($"https://{_kfDomain}/login", _ctx);
content = await response.Content.ReadAsStringAsync(_ctx);
}
return content;
}
public async Task<bool> IsLoggedIn()
{
var document = new HtmlDocument();
document.LoadHtml(await GetLoginPage());
var html = document.DocumentNode.SelectSingleNode("//html");
if (html == null) throw new Exception("Caught a null when retrieving html element");
if (!html.Attributes.Contains("data-logged-in"))
{
throw new Exception("data-logged-in attribute missing");
}
var success = html.Attributes["data-logged-in"].Value == "true";
if (success)
{
await SaveCookies();
}
return success;
}
public async Task PerformLogin(string username, string password)
{
var document = new HtmlDocument();
document.LoadHtml(await GetLoginPage());
var html = document.DocumentNode.SelectSingleNode("//html");
if (html == null) throw new Exception("Caught a null when retrieving html element");
// Already logged in
if (html.GetAttributeValue("data-logged-in", "false") == "true") return;
if (!html.Attributes.Contains("data-csrf")) throw new Exception("data-csrf missing from html element");
var csrf = html.GetAttributeValue("data-csrf", string.Empty);
var formData = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>
{
new("_xfToken", csrf),
new("login", username),
new("password", password),
new("_xfRedirect", $"https://{_kfDomain}/"),
new("remember", "1")
});
using var client = new HttpClient(GetHttpClientHandler());
var postResponse = await client.PostAsync($"https://{_kfDomain}/login/login", formData, _ctx);
if (postResponse.StatusCode == HttpStatusCode.SeeOther)
{
_logger.Debug("Got HTTP response 303. Success!");
return;
}
postResponse.EnsureSuccessStatusCode();
_logger.Info($"Received HTTP response {postResponse.StatusCode}, checking to see if we're logged in");
var postDocument = new HtmlDocument();
postDocument.Load(await postResponse.Content.ReadAsStreamAsync(_ctx));
html = postDocument.DocumentNode.SelectSingleNode("//html");
if (html == null) throw new Exception("Caught a null when retrieving html element");
// Logged in!
if (html.GetAttributeValue("data-logged-in", "false") == "true") return;
_logger.Error("Not logged in :(");
var message =
postDocument.DocumentNode.SelectSingleNode(
"//div[@class=\"blockMessage blockMessage--error blockMessage--iconic\"]");
_logger.Error($"Error from the page was {message?.InnerText}");
throw new KiwiFarmsLogonFailedException();
}
public string? GetXfSessionCookie()
{
_logger.Debug("JSON serialization of all the cookies");
_logger.Debug(JsonSerializer.Serialize(_cookies.GetAllCookies()));
var cookie = _cookies.GetAllCookies()["xf_session"];
_logger.Debug($"xf_session => {cookie?.Value}");
return cookie?.Value;
}
public Dictionary<string, string> GetCookies()
{
return _cookies.GetAllCookies().ToDictionary(cookie => cookie.Name, cookie => cookie.Value);
}
public async Task SaveCookies()
{
_logger.Debug("Saving cookies");
var cookiesToSave = _cookies.GetAllCookies().ToDictionary(cookie => cookie.Name, cookie => cookie.Value);
await SettingsProvider.SetValueAsJsonObjectAsync(BuiltIn.Keys.KiwiFarmsCookies, cookiesToSave);
}
public void WipeCookies()
{
_logger.Info("Wiping out cookies");
_cookies = new CookieContainer();
}
public class KiwiFarmsLogonFailedException : Exception;
}