mirror of
https://github.com/barelyprofessional/KfChatDotNet.git
synced 2026-05-02 04:22:04 -04:00
Removed Puppeteer and logging in by POSTing the logon form now that we can get clearance tokens.
This commit is contained in:
@@ -1,83 +1,167 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using HtmlAgilityPack;
|
||||
using KfChatDotNetBot.Settings;
|
||||
using NLog;
|
||||
using PuppeteerSharp;
|
||||
|
||||
namespace KfChatDotNetBot.Services;
|
||||
|
||||
public class KfTokenService
|
||||
{
|
||||
// Shout out Gamba Sesh for open sourcing his token retriever which heavily inspired this implementation
|
||||
public static async Task<string> FetchSessionTokenAsync(string domain, string username, string password, string browserPath, string? proxy = null)
|
||||
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)
|
||||
{
|
||||
var logger = LogManager.GetCurrentClassLogger();
|
||||
var browserFetcher = new BrowserFetcher(new BrowserFetcherOptions
|
||||
{ Browser = SupportedBrowser.Chromium, Path = browserPath });
|
||||
if (proxy != null)
|
||||
_ctx = cancellationToken ?? CancellationToken.None;
|
||||
_kiwiFlare = new KiwiFlare(kfDomain, proxy, cancellationToken);
|
||||
_proxy = proxy;
|
||||
_kfDomain = kfDomain;
|
||||
var cachedCookies = Helpers.GetValue(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)
|
||||
{
|
||||
browserFetcher.WebProxy = new WebProxy(proxy);
|
||||
logger.Debug($"Detected proxy settings for browser download: {proxy}");
|
||||
}
|
||||
|
||||
var installedBrowser = await browserFetcher.DownloadAsync();
|
||||
logger.Debug("Downloaded browser");
|
||||
List<string> launchArgs = ["--no-sandbox"];
|
||||
if (proxy != null)
|
||||
{
|
||||
logger.Debug($"Configuring Chromium to use proxy {proxy}");
|
||||
launchArgs.Add($"--proxy-server=\"{proxy}\"");
|
||||
}
|
||||
|
||||
var launchOptions = new LaunchOptions
|
||||
{
|
||||
Headless = false, ExecutablePath = installedBrowser.GetExecutablePath(), UserDataDir = "kf_profile",
|
||||
Args = launchArgs.ToArray()
|
||||
};
|
||||
|
||||
await using var browser = await Puppeteer.LaunchAsync(launchOptions);
|
||||
await using var page = await browser.NewPageAsync();
|
||||
await page.GoToAsync($"https://{domain}/login");
|
||||
await page.WaitForSelectorAsync("img[alt=\"Kiwi Farms\"]");
|
||||
if (await page.QuerySelectorAsync("html[data-template=\"login\"]") == null)
|
||||
{
|
||||
logger.Debug("Page template is not login. This is expected if we're already logged in. Reloading page to get the freshest cookies then retrieving");
|
||||
await page.ReloadAsync();
|
||||
return await GetXfSessionCookie();
|
||||
}
|
||||
|
||||
var usernameFieldSelector = await page.QuerySelectorAsync("input[autocomplete=\"username\"]");
|
||||
var passwordFieldSelector = await page.QuerySelectorAsync("input[autocomplete=\"current-password\"]");
|
||||
var loginButtonSelector = await page.QuerySelectorAsync("div[class=\"formSubmitRow-controls\"] > button[type=\"submit\"]");
|
||||
if (usernameFieldSelector == null || passwordFieldSelector == null || loginButtonSelector == null)
|
||||
{
|
||||
// Realistically this shouldn't happen unless Null changes the login template in a big way
|
||||
logger.Error("Username/password fields could not be found");
|
||||
throw new MissingLoginElementsException();
|
||||
}
|
||||
|
||||
await usernameFieldSelector.TypeAsync(username);
|
||||
await passwordFieldSelector.TypeAsync(password);
|
||||
await loginButtonSelector.ClickAsync();
|
||||
logger.Debug("Login fields have been filled out and button clicked. Awaiting page navigation.");
|
||||
await page.WaitForNavigationAsync();
|
||||
logger.Debug("Navigation completed. Doing the cookie needful");
|
||||
return await GetXfSessionCookie();
|
||||
|
||||
async Task<string> GetXfSessionCookie()
|
||||
{
|
||||
var cookies = await page.GetCookiesAsync();
|
||||
var xfSession = cookies.FirstOrDefault(x => x.Name == "xf_session");
|
||||
if (xfSession == null)
|
||||
{
|
||||
logger.Error("xf_session cookie not set. Cookie data follows");
|
||||
logger.Error(JsonSerializer.Serialize(cookies));
|
||||
throw new MissingSessionCookieException();
|
||||
}
|
||||
logger.Debug($"Returning xf_session value: {xfSession.Value}");
|
||||
return xfSession.Value;
|
||||
_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;
|
||||
}
|
||||
|
||||
public class MissingSessionCookieException : Exception;
|
||||
public class MissingLoginElementsException : Exception;
|
||||
private async Task CheckClearanceToken()
|
||||
{
|
||||
var clearanceCookie = _cookies.GetAllCookies()["sssg_clearance"];
|
||||
_logger.Debug($"Got clearance cookie with value: {clearanceCookie}");
|
||||
if (clearanceCookie != null)
|
||||
{
|
||||
if (await _kiwiFlare.CheckAuth(clearanceCookie.Value)) return;
|
||||
_logger.Debug("Cookie is no longer valid, removing");
|
||||
_cookies.GetAllCookies().Remove(clearanceCookie);
|
||||
}
|
||||
_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();
|
||||
var solution = await _kiwiFlare.SolveChallenge(challenge);
|
||||
var 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");
|
||||
}
|
||||
|
||||
public async Task<bool> IsLoggedIn()
|
||||
{
|
||||
_logger.Debug("Checking clearance token is actually valid first");
|
||||
await CheckClearanceToken();
|
||||
using var client = new HttpClient(GetHttpClientHandler());
|
||||
var response = await client.GetAsync($"https://{_kfDomain}/login", _ctx);
|
||||
if (response.StatusCode == HttpStatusCode.NonAuthoritativeInformation)
|
||||
{
|
||||
_logger.Error("Caught a 203 response when trying to load logon page which means we were KiwiFlare challenged");
|
||||
throw new KiwiFlareChallengedException();
|
||||
}
|
||||
response.EnsureSuccessStatusCode();
|
||||
var document = new HtmlDocument();
|
||||
document.Load(await response.Content.ReadAsStreamAsync(_ctx));
|
||||
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");
|
||||
}
|
||||
|
||||
return html.Attributes["data-logged-in"].Value == "true";
|
||||
}
|
||||
|
||||
public async Task PerformLogin(string username, string password)
|
||||
{
|
||||
_logger.Debug("Checking clearance token is actually valid first");
|
||||
await CheckClearanceToken();
|
||||
using var client = new HttpClient(GetHttpClientHandler());
|
||||
var response = await client.GetAsync($"https://{_kfDomain}/login", _ctx);
|
||||
if (response.StatusCode == HttpStatusCode.NonAuthoritativeInformation)
|
||||
{
|
||||
_logger.Error("Caught a 203 response when trying to load logon page which means we were KiwiFlare challenged");
|
||||
throw new KiwiFlareChallengedException();
|
||||
}
|
||||
response.EnsureSuccessStatusCode();
|
||||
var document = new HtmlDocument();
|
||||
document.Load(await response.Content.ReadAsStreamAsync(_ctx));
|
||||
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}/")
|
||||
});
|
||||
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.Error($"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()
|
||||
{
|
||||
var cookie = _cookies.GetAllCookies()["xf_session"];
|
||||
return cookie?.Value;
|
||||
}
|
||||
|
||||
public async Task SaveCookies()
|
||||
{
|
||||
var cookiesToSave = _cookies.GetAllCookies().ToDictionary(cookie => cookie.Name, cookie => cookie.Value);
|
||||
await Helpers.SetValueAsJsonObject(BuiltIn.Keys.KiwiFarmsCookies, cookiesToSave);
|
||||
}
|
||||
|
||||
public class KiwiFlareChallengedException : Exception;
|
||||
public class KiwiFarmsLogonFailedException : Exception;
|
||||
}
|
||||
Reference in New Issue
Block a user