using Newtonsoft.Json; using OF_DL.Entities; using PuppeteerSharp; using PuppeteerSharp.BrowserData; using Serilog; namespace OF_DL.Services; public class AuthService : IAuthService { private const int LoginTimeout = 600000; // 10 minutes private const int FeedLoadTimeout = 60000; // 1 minute private readonly string[] _desiredCookies = [ "auth_id", "sess" ]; private readonly LaunchOptions _options = new() { Headless = false, Channel = ChromeReleaseChannel.Stable, DefaultViewport = null, Args = ["--no-sandbox", "--disable-setuid-sandbox"], UserDataDir = Path.GetFullPath("chrome-data") }; public Auth? CurrentAuth { get; set; } public async Task LoadFromFileAsync(string filePath = "auth.json") { try { if (!File.Exists(filePath)) { Log.Debug("Auth file not found: {FilePath}", filePath); return false; } string json = await File.ReadAllTextAsync(filePath); CurrentAuth = JsonConvert.DeserializeObject(json); Log.Debug("Auth file loaded and deserialized successfully"); return CurrentAuth != null; } catch (Exception ex) { Log.Error(ex, "Failed to load auth from file"); return false; } } public async Task LoadFromBrowserAsync() { try { bool runningInDocker = Environment.GetEnvironmentVariable("OFDL_DOCKER") != null; await SetupBrowser(runningInDocker); CurrentAuth = await GetAuthFromBrowser(); return CurrentAuth != null; } catch (Exception ex) { Log.Error(ex, "Failed to load auth from browser"); return false; } } public async Task SaveToFileAsync(string filePath = "auth.json") { if (CurrentAuth == null) { Log.Warning("Attempted to save null auth to file"); return; } try { string json = JsonConvert.SerializeObject(CurrentAuth, Formatting.Indented); await File.WriteAllTextAsync(filePath, json); Log.Debug($"Auth saved to file: {filePath}"); } catch (Exception ex) { Log.Error(ex, "Failed to save auth to file"); } } private async Task SetupBrowser(bool runningInDocker) { string? executablePath = Environment.GetEnvironmentVariable("OFDL_PUPPETEER_EXECUTABLE_PATH"); if (executablePath != null) { Log.Information( "OFDL_PUPPETEER_EXECUTABLE_PATH environment variable found. Using browser executable path: {executablePath}", executablePath); _options.ExecutablePath = executablePath; } else { BrowserFetcher browserFetcher = new(); List installedBrowsers = browserFetcher.GetInstalledBrowsers().ToList(); if (installedBrowsers.Count == 0) { Log.Information("Downloading browser."); InstalledBrowser? downloadedBrowser = await browserFetcher.DownloadAsync(); Log.Information("Browser downloaded. Path: {executablePath}", downloadedBrowser.GetExecutablePath()); _options.ExecutablePath = downloadedBrowser.GetExecutablePath(); } else { _options.ExecutablePath = installedBrowsers.First().GetExecutablePath(); } } if (runningInDocker) { Log.Information("Running in Docker. Disabling sandbox and GPU."); _options.Args = ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"]; } } private async Task GetBcToken(IPage page) => await page.EvaluateExpressionAsync("window.localStorage.getItem('bcTokenSha') || ''"); private async Task GetAuthFromBrowser(bool isDocker = false) { try { IBrowser? browser; try { browser = await Puppeteer.LaunchAsync(_options); } catch (ProcessException e) { if (e.Message.Contains("Failed to launch browser") && Directory.Exists(_options.UserDataDir)) { Log.Error("Failed to launch browser. Deleting chrome-data directory and trying again."); Directory.Delete(_options.UserDataDir, true); browser = await Puppeteer.LaunchAsync(_options); } else { throw; } } if (browser == null) { throw new Exception("Could not get browser"); } IPage[]? pages = await browser.PagesAsync(); IPage? page = pages.First(); if (page == null) { throw new Exception("Could not get page"); } Log.Debug("Navigating to OnlyFans."); await page.GoToAsync("https://onlyfans.com"); Log.Debug("Waiting for user to login"); await page.WaitForSelectorAsync(".b-feed", new WaitForSelectorOptions { Timeout = LoginTimeout }); Log.Debug("Feed element detected (user logged in)"); await page.ReloadAsync(); await page.WaitForNavigationAsync(new NavigationOptions { WaitUntil = [WaitUntilNavigation.Networkidle2], Timeout = FeedLoadTimeout }); Log.Debug("DOM loaded. Getting BC token and cookies ..."); string xBc; try { xBc = await GetBcToken(page); } catch (Exception e) { throw new Exception("Error getting bcToken"); } Dictionary mappedCookies = (await page.GetCookiesAsync()) .Where(cookie => cookie.Domain.Contains("onlyfans.com")) .ToDictionary(cookie => cookie.Name, cookie => cookie.Value); mappedCookies.TryGetValue("auth_id", out string? userId); if (userId == null) { throw new Exception("Could not find 'auth_id' cookie"); } mappedCookies.TryGetValue("sess", out string? sess); if (sess == null) { throw new Exception("Could not find 'sess' cookie"); } string? userAgent = await browser.GetUserAgentAsync(); if (userAgent == null) { throw new Exception("Could not get user agent"); } string cookies = string.Join(" ", mappedCookies.Keys.Where(key => _desiredCookies.Contains(key)) .Select(key => $"${key}={mappedCookies[key]};")); return new Auth { COOKIE = cookies, USER_AGENT = userAgent, USER_ID = userId, X_BC = xBc }; } catch (Exception e) { Log.Error(e, "Error getting auth from browser"); return null; } } }