using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; using CRD.Utils; using CRD.Utils.Files; using CRD.Utils.Structs; using CRD.Utils.Structs.Crunchyroll; using CRD.Views; using Newtonsoft.Json; using ReactiveUI; namespace CRD.Downloader.Crunchyroll; public class CrAuth(CrunchyrollManager crunInstance, CrAuthSettings authSettings){ public CrToken? Token; public CrProfile Profile = new(); public Subscription? Subscription{ get; set; } public CrMultiProfile MultiProfile = new(); public CrunchyrollEndpoints EndpointEnum = CrunchyrollEndpoints.Unknown; public CrAuthSettings AuthSettings = authSettings; public Dictionary cookieStore = new(); public void Init(){ Profile = new CrProfile{ Username = "???", Avatar = "crbrand_avatars_logo_marks_mangagirl_taupe.png", PreferredContentAudioLanguage = "ja-JP", PreferredContentSubtitleLanguage = crunInstance.DefaultLocale, HasPremium = false, }; } private string GetTokenFilePath(){ switch (AuthSettings.Endpoint){ case "tv/samsung": case "tv/vidaa": case "tv/android_tv": return CfgManager.PathCrToken.Replace(".json", "_tv.json"); case "android/phone": case "android/tablet": return CfgManager.PathCrToken.Replace(".json", "_android.json"); case "console/switch": case "console/ps4": case "console/ps5": case "console/xbox_one": return CfgManager.PathCrToken.Replace(".json", "_console.json"); case "---": return CfgManager.PathCrToken.Replace(".json", "_guest.json"); default: return CfgManager.PathCrToken; } } public async Task Auth(){ if (CfgManager.CheckIfFileExists(GetTokenFilePath())){ Token = CfgManager.ReadJsonFromFile(GetTokenFilePath()); await LoginWithToken(); } else{ await AuthAnonymous(); } } public void SetETPCookie(string refreshToken){ HttpClientReq.Instance.AddCookie(".crunchyroll.com", new Cookie("etp_rt", refreshToken), cookieStore); HttpClientReq.Instance.AddCookie(".crunchyroll.com", new Cookie("c_locale", "en-US"), cookieStore); } public async Task AuthAnonymous(){ string uuid = string.IsNullOrEmpty(Token?.device_id) ? Guid.NewGuid().ToString() : Token.device_id; Subscription = new Subscription(); var formData = new Dictionary{ { "grant_type", "client_id" }, { "scope", "offline_access" }, { "device_id", uuid }, { "device_type", AuthSettings.Device_type }, }; if (!string.IsNullOrEmpty(AuthSettings.Device_name)){ formData.Add("device_name", AuthSettings.Device_name); } var requestContent = new FormUrlEncodedContent(formData); var crunchyAuthHeaders = new Dictionary{ { "Authorization", AuthSettings.Authorization }, { "User-Agent", AuthSettings.UserAgent } }; var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){ Content = requestContent }; foreach (var header in crunchyAuthHeaders){ request.Headers.Add(header.Key, header.Value); } var response = await HttpClientReq.Instance.SendHttpRequest(request); if (response.IsOk){ JsonTokenToFileAndVariable(response.ResponseContent, uuid); } else{ Console.Error.WriteLine("Anonymous login failed"); } Profile = new CrProfile{ Username = "???", Avatar = "crbrand_avatars_logo_marks_mangagirl_taupe.png", PreferredContentAudioLanguage = "ja-JP", PreferredContentSubtitleLanguage = "de-DE" }; } private void JsonTokenToFileAndVariable(string content, string deviceId){ Token = Helpers.Deserialize(content, crunInstance.SettingsJsonSerializerSettings); if (Token is{ expires_in: not null }){ Token.device_id = deviceId; Token.expires = DateTime.Now.AddSeconds((double)Token.expires_in); CfgManager.WriteJsonToFile(GetTokenFilePath(), Token); } } public async Task Auth(AuthData data){ string uuid = Guid.NewGuid().ToString(); var formData = new Dictionary{ { "username", data.Username }, { "password", data.Password }, { "grant_type", "password" }, { "scope", "offline_access" }, { "device_id", uuid }, { "device_type", AuthSettings.Device_type }, }; if (!string.IsNullOrEmpty(AuthSettings.Device_name)){ formData.Add("device_name", AuthSettings.Device_name); } var requestContent = new FormUrlEncodedContent(formData); var crunchyAuthHeaders = new Dictionary{ { "Authorization", AuthSettings.Authorization }, { "User-Agent", AuthSettings.UserAgent } }; var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){ Content = requestContent }; foreach (var header in crunchyAuthHeaders){ request.Headers.Add(header.Key, header.Value); } var response = await HttpClientReq.Instance.SendHttpRequest(request); if (response.IsOk){ JsonTokenToFileAndVariable(response.ResponseContent, uuid); } else{ if (response.ResponseContent.Contains("invalid_credentials")){ MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - because of invalid login credentials", ToastType.Error, 5)); } else if (response.ResponseContent.Contains("Just a moment...") || response.ResponseContent.Contains("Access denied") || response.ResponseContent.Contains("Attention Required! | Cloudflare") || response.ResponseContent.Trim().Equals("error code: 1020") || response.ResponseContent.IndexOf("DDOS-GUARD", StringComparison.OrdinalIgnoreCase) > -1){ MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - Cloudflare error try to change to BetaAPI in settings", ToastType.Error, 5)); } else{ MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - {response.ResponseContent.Substring(0, response.ResponseContent.Length < 200 ? response.ResponseContent.Length : 200)}", ToastType.Error, 5)); await Console.Error.WriteLineAsync("Full Response: " + response.ResponseContent); } } if (Token?.refresh_token != null){ SetETPCookie(Token.refresh_token); await GetMultiProfile(); } } public async Task ChangeProfile(string profileId){ if (Token?.access_token == null && Token?.refresh_token == null || Token.access_token != null && Token.refresh_token == null){ await AuthAnonymous(); } if (Profile.Username == "???"){ return; } if (string.IsNullOrEmpty(profileId) || Token?.refresh_token == null){ return; } string uuid = string.IsNullOrEmpty(Token.device_id) ? Guid.NewGuid().ToString() : Token.device_id; SetETPCookie(Token.refresh_token); var formData = new Dictionary{ { "grant_type", "refresh_token_profile_id" }, { "profile_id", profileId }, { "device_id", uuid }, { "device_type", AuthSettings.Device_type }, }; var requestContent = new FormUrlEncodedContent(formData); var crunchyAuthHeaders = new Dictionary{ { "Authorization", AuthSettings.Authorization }, { "User-Agent", AuthSettings.UserAgent } }; var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){ Content = requestContent }; foreach (var header in crunchyAuthHeaders){ request.Headers.Add(header.Key, header.Value); } if (Token?.refresh_token != null) SetETPCookie(Token.refresh_token); var response = await HttpClientReq.Instance.SendHttpRequest(request, false, cookieStore); if (response.IsOk){ JsonTokenToFileAndVariable(response.ResponseContent, uuid); if (Token?.refresh_token != null){ SetETPCookie(Token.refresh_token); } await GetMultiProfile(); } else{ Console.Error.WriteLine("Refresh Token Auth Failed"); } } public async Task GetProfile(){ if (Token?.access_token == null){ Console.Error.WriteLine("Missing Access Token"); return; } var request = HttpClientReq.CreateRequestMessage(ApiUrls.Profile, HttpMethod.Get, true, Token.access_token, null); var response = await HttpClientReq.Instance.SendHttpRequest(request); if (response.IsOk){ var profileTemp = Helpers.Deserialize(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings); if (profileTemp != null){ Profile = profileTemp; await GetSubscription(); } } } private async Task GetSubscription(){ var requestSubs = HttpClientReq.CreateRequestMessage(ApiUrls.Subscription + Token.account_id, HttpMethod.Get, true, Token.access_token, null); var responseSubs = await HttpClientReq.Instance.SendHttpRequest(requestSubs); if (responseSubs.IsOk){ var subsc = Helpers.Deserialize(responseSubs.ResponseContent, crunInstance.SettingsJsonSerializerSettings); Subscription = subsc; if (subsc is{ SubscriptionProducts:{ Count: 0 }, ThirdPartySubscriptionProducts.Count: > 0 }){ var thirdPartySub = subsc.ThirdPartySubscriptionProducts.First(); var expiration = thirdPartySub.InGrace ? thirdPartySub.InGraceExpirationDate : thirdPartySub.ExpirationDate; var remaining = expiration - DateTime.Now; Profile.HasPremium = true; if (Subscription != null){ Subscription.IsActive = remaining > TimeSpan.Zero; Subscription.NextRenewalDate = expiration; } } else if (subsc is{ SubscriptionProducts:{ Count: 0 }, NonrecurringSubscriptionProducts.Count: > 0 }){ var nonRecurringSub = subsc.NonrecurringSubscriptionProducts.First(); var remaining = nonRecurringSub.EndDate - DateTime.Now; Profile.HasPremium = true; if (Subscription != null){ Subscription.IsActive = remaining > TimeSpan.Zero; Subscription.NextRenewalDate = nonRecurringSub.EndDate; } } else if (subsc is{ SubscriptionProducts:{ Count: 0 }, FunimationSubscriptions.Count: > 0 }){ Profile.HasPremium = true; } else if (subsc is{ SubscriptionProducts.Count: > 0 }){ Profile.HasPremium = true; } else{ Profile.HasPremium = false; Console.Error.WriteLine($"No subscription available:\n {JsonConvert.SerializeObject(subsc, Formatting.Indented)} "); } } else{ Profile.HasPremium = false; Console.Error.WriteLine("Failed to check premium subscription status"); } } private async Task GetMultiProfile(){ if (Token?.access_token == null){ Console.Error.WriteLine("Missing Access Token"); return; } var request = HttpClientReq.CreateRequestMessage(ApiUrls.MultiProfile, HttpMethod.Get, true, Token?.access_token); var response = await HttpClientReq.Instance.SendHttpRequest(request, false, cookieStore); if (response.IsOk){ MultiProfile = Helpers.Deserialize(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings) ?? new CrMultiProfile(); var selectedProfile = MultiProfile.Profiles.FirstOrDefault( e => e.IsSelected); if (selectedProfile != null) Profile = selectedProfile; await GetSubscription(); } } } } } public async Task LoginWithToken(){ if (Token?.refresh_token == null){ Console.Error.WriteLine("Missing Refresh Token"); await AuthAnonymous(); return; } string uuid = string.IsNullOrEmpty(Token.device_id) ? Guid.NewGuid().ToString() : Token.device_id; var formData = new Dictionary{ { "refresh_token", Token.refresh_token }, { "scope", "offline_access" }, { "device_id", uuid }, { "grant_type", "refresh_token" }, { "device_type", AuthSettings.Device_type }, }; if (!string.IsNullOrEmpty(AuthSettings.Device_name)){ formData.Add("device_name", AuthSettings.Device_name); } var requestContent = new FormUrlEncodedContent(formData); var crunchyAuthHeaders = new Dictionary{ { "Authorization", AuthSettings.Authorization }, { "User-Agent", AuthSettings.UserAgent } }; var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){ Content = requestContent }; foreach (var header in crunchyAuthHeaders){ request.Headers.Add(header.Key, header.Value); } SetETPCookie(Token.refresh_token); var response = await HttpClientReq.Instance.SendHttpRequest(request); if (response.ResponseContent.Contains("Just a moment...") || response.ResponseContent.Contains("Access denied") || response.ResponseContent.Contains("Attention Required! | Cloudflare") || response.ResponseContent.Trim().Equals("error code: 1020") || response.ResponseContent.IndexOf("DDOS-GUARD", StringComparison.OrdinalIgnoreCase) > -1){ MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - Cloudflare error try to change to BetaAPI in settings", ToastType.Error, 5)); Console.Error.WriteLine($"Failed to login - Cloudflare error try to change to BetaAPI in settings"); } if (response.IsOk){ JsonTokenToFileAndVariable(response.ResponseContent, uuid); if (Token?.refresh_token != null){ SetETPCookie(Token.refresh_token); await GetMultiProfile(); } } else{ Console.Error.WriteLine("Token Auth Failed"); await AuthAnonymous(); MainWindow.Instance.ShowError("Login failed. Please check the log for more details."); } } public async Task RefreshToken(bool needsToken){ if (EndpointEnum == CrunchyrollEndpoints.Guest){ if (Token != null && !(DateTime.Now > Token.expires)){ return; } await AuthAnonymousFoxy(); return; } if (Token?.access_token == null && Token?.refresh_token == null || Token.access_token != null && Token.refresh_token == null){ await AuthAnonymous(); } else{ if (!(DateTime.Now > Token.expires) && needsToken){ return; } } if (Profile.Username == "???"){ return; } string uuid = string.IsNullOrEmpty(Token?.device_id) ? Guid.NewGuid().ToString() : Token.device_id; var formData = new Dictionary{ { "refresh_token", Token?.refresh_token ?? "" }, { "grant_type", "refresh_token" }, { "scope", "offline_access" }, { "device_id", uuid }, { "device_type", AuthSettings.Device_type }, }; if (!string.IsNullOrEmpty(AuthSettings.Device_name)){ formData.Add("device_name", AuthSettings.Device_name); } var requestContent = new FormUrlEncodedContent(formData); var crunchyAuthHeaders = new Dictionary{ { "Authorization", AuthSettings.Authorization }, { "User-Agent", AuthSettings.UserAgent } }; var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){ Content = requestContent }; foreach (var header in crunchyAuthHeaders){ request.Headers.Add(header.Key, header.Value); } SetETPCookie(Token?.refresh_token ?? string.Empty); var response = await HttpClientReq.Instance.SendHttpRequest(request); if (response.IsOk){ JsonTokenToFileAndVariable(response.ResponseContent, uuid); } else{ Console.Error.WriteLine("Refresh Token Auth Failed"); } } }