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>(); // 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 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 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> { 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 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; }