Files
cloakhq--cloakbrowser/dotnet/src/CloakBrowser/Config.cs
T
wehub-resource-sync 0af812490d
CI / python (push) Failing after 0s
CI / javascript (push) Failing after 1s
CI / dotnet (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:12:36 +08:00

533 lines
22 KiB
C#

using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
namespace CloakBrowser;
/// <summary>
/// Stealth configuration and platform detection for CloakBrowser.
/// Direct port of Python <c>cloakbrowser/config.py</c>.
/// </summary>
public static class Config
{
// -----------------------------------------------------------------------
// Chromium version shipped with this release.
// Different platforms may ship different versions during transition periods.
// ChromiumVersion is the latest across all platforms (for display/reference).
// Use GetChromiumVersion() for the current platform's actual version.
// -----------------------------------------------------------------------
public const string ChromiumVersion = "146.0.7680.177.5";
public static readonly IReadOnlyDictionary<string, string> PlatformChromiumVersions =
new Dictionary<string, string>
{
["linux-x64"] = "146.0.7680.177.5",
["linux-arm64"] = "146.0.7680.177.3",
["darwin-arm64"] = "145.0.7632.109.2",
["darwin-x64"] = "145.0.7632.109.2",
["windows-x64"] = "146.0.7680.177.5",
};
// -----------------------------------------------------------------------
// Ed25519 public keys for verifying downloaded binaries.
//
// Each release publishes SHA256SUMS and a detached signature SHA256SUMS.sig.
// The wrapper verifies that signature against the keys below before trusting
// any hash in the manifest, so the download origin alone cannot certify a
// tampered binary. Values are base64 of the 32-byte raw public key. Multiple
// entries are accepted to allow key rotation.
// -----------------------------------------------------------------------
public static readonly IReadOnlyList<string> BinarySigningPubkeys = new[]
{
"MKFKwIhUcKWq5xTuNA0Ovg99njcDEcEJvmWYYhApvaU=",
};
// -----------------------------------------------------------------------
// Playwright default args to suppress - these leak automation signals.
// --enable-automation: exposes navigator.webdriver = true
// --enable-unsafe-swiftshader: forces software WebGL rendering via SwiftShader,
// producing a distinctive renderer string that no real user browser has.
// -----------------------------------------------------------------------
public static readonly string[] IgnoreDefaultArgs =
{ "--enable-automation", "--enable-unsafe-swiftshader" };
// -----------------------------------------------------------------------
// Default viewport - used for HEADLESS only (headed launches use no_viewport
// so the page tracks the real window). Headless has no window chrome, so a
// fixed viewport stays coherent (outer == inner) and gives deterministic
// dimensions. Models a maximized Chrome on 1080p Windows: screen=1920x1080,
// innerHeight=947 (minus ~85px Chrome UI: tabs + address bar + bookmarks).
// -----------------------------------------------------------------------
public const int DefaultViewportWidth = 1920;
public const int DefaultViewportHeight = 947;
private static readonly Random _rng = new();
/// <summary>
/// Build stealth args with a random fingerprint seed per launch.
/// On macOS, skips platform/GPU spoofing - runs as a native Mac browser.
/// Spoofing Windows on Mac creates detectable mismatches (fonts, GPU, etc.).
/// </summary>
public static List<string> GetDefaultStealthArgs()
{
int seed;
lock (_rng) { seed = _rng.Next(10000, 100000); }
var baseArgs = new List<string>
{
"--no-sandbox",
$"--fingerprint={seed}",
};
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
// Tell the fingerprint patches we're on macOS so GPU/UA match natively.
baseArgs.Add("--fingerprint-platform=macos");
return baseArgs;
}
// Linux/Windows: Windows fingerprint profile.
// Screen and window size come from the real display, not this flag (verified:
// identical across seeds), so the wrapper must not emulate a viewport on top in
// headed mode - that would break outerWidth >= innerWidth coherence.
baseArgs.Add("--fingerprint-platform=windows");
return baseArgs;
}
// -----------------------------------------------------------------------
// Platform detection
// -----------------------------------------------------------------------
/// <summary>Platforms with pre-built binaries available for download.</summary>
public static IReadOnlySet<string> AvailablePlatforms =>
new HashSet<string>(PlatformChromiumVersions.Keys);
/// <summary>Return the Chromium version for the current platform.</summary>
public static string GetChromiumVersion()
{
var tag = GetPlatformTag();
return PlatformChromiumVersions.TryGetValue(tag, out var v) ? v : ChromiumVersion;
}
// -----------------------------------------------------------------------
// Version pinning
// -----------------------------------------------------------------------
private static readonly Regex VersionPinRe = new(
@"^[0-9]+(?:\.[0-9]+){3,4}$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
/// <summary>
/// Return an explicit Chromium version pin from arg/env, or null.
///
/// The explicit argument wins over <c>CLOAKBROWSER_VERSION</c>. Only numeric dotted
/// versions are accepted because the value is interpolated into cache paths
/// and download URLs. Port of Python <c>normalize_requested_version()</c>.
/// </summary>
public static string? NormalizeRequestedVersion(string? version = null)
{
var raw = version ?? Environment.GetEnvironmentVariable("CLOAKBROWSER_VERSION");
if (raw == null)
return null;
var normalized = raw.Trim();
if (normalized.Length == 0)
return null;
if (!VersionPinRe.IsMatch(normalized))
throw new ArgumentException(
"Invalid browser version pin. Use a full numeric Chromium version, " +
"e.g. '148.0.7778.215.2'.");
return normalized;
}
/// <summary>
/// Return the platform tag for binary download (e.g. <c>linux-x64</c>, <c>darwin-arm64</c>).
/// </summary>
public static string GetPlatformTag()
{
var arch = RuntimeInformation.OSArchitecture;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return arch switch
{
Architecture.X64 => "linux-x64",
Architecture.Arm64 => "linux-arm64",
_ => throw Unsupported("Linux", arch),
};
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return arch switch
{
Architecture.Arm64 => "darwin-arm64",
Architecture.X64 => "darwin-x64",
_ => throw Unsupported("Darwin", arch),
};
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return arch switch
{
Architecture.X64 => "windows-x64",
_ => throw Unsupported("Windows", arch),
};
}
throw new PlatformNotSupportedException(
$"Unsupported platform: {RuntimeInformation.OSDescription} {arch}");
}
private static PlatformNotSupportedException Unsupported(string system, Architecture arch) =>
new($"Unsupported platform: {system} {arch}. " +
"Supported: linux-x64, linux-arm64, darwin-arm64, darwin-x64, windows-x64");
// -----------------------------------------------------------------------
// Binary cache paths
// -----------------------------------------------------------------------
/// <summary>
/// Return the cache directory for downloaded binaries.
/// Override with the <c>CLOAKBROWSER_CACHE_DIR</c> env var. Default: <c>~/.cloakbrowser/</c>.
/// </summary>
public static string GetCacheDir()
{
var custom = Environment.GetEnvironmentVariable("CLOAKBROWSER_CACHE_DIR");
if (!string.IsNullOrEmpty(custom))
return custom;
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return Path.Combine(home, ".cloakbrowser");
}
/// <summary>Return the directory for a Chromium version binary.</summary>
/// <param name="version">Version string, or null for the platform default.</param>
/// <param name="pro">When true, use the Pro-specific cache dir (<c>chromium-{v}-pro</c>).</param>
public static string GetBinaryDir(string? version = null, bool pro = false)
{
var v = version ?? GetChromiumVersion();
var suffix = pro ? "-pro" : "";
return Path.Combine(GetCacheDir(), $"chromium-{v}{suffix}");
}
/// <summary>Return the expected path to the chrome executable.</summary>
public static string GetBinaryPath(string? version = null, bool pro = false)
{
var binaryDir = GetBinaryDir(version, pro);
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return Path.Combine(binaryDir, "Chromium.app", "Contents", "MacOS", "Chromium");
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return Path.Combine(binaryDir, "chrome.exe");
return Path.Combine(binaryDir, "chrome");
}
/// <summary>
/// Raise a clear error if no pre-built binary exists for this platform.
/// Skipped when <c>CLOAKBROWSER_BINARY_PATH</c> is set (user has their own build).
/// </summary>
public static void CheckPlatformAvailable()
{
if (GetLocalBinaryOverride() != null)
return;
var tag = GetPlatformTag(); // throws if platform unsupported entirely
if (!AvailablePlatforms.Contains(tag))
{
var available = string.Join(", ", AvailablePlatforms.OrderBy(x => x));
throw new PlatformNotSupportedException(
$"\nCloakBrowser - Pre-built binaries are currently only available for: {available}.\n\n" +
"To use CloakBrowser now, set CLOAKBROWSER_BINARY_PATH to a local Chromium binary.");
}
}
/// <summary>
/// Return the best available version: auto-updated if available, else platform default.
/// Reads a platform-scoped marker file from the cache directory.
/// When <paramref name="pro"/> is true, reads from the Pro-specific marker and returns
/// <c>null</c> when no cached Pro binary matches it. A valid Pro license must NEVER fall
/// back to the free binary, so there is deliberately no free-version fallback for Pro —
/// callers treat <c>null</c> as "resolve the latest Pro version from the server".
/// </summary>
public static string? GetEffectiveVersion(bool pro = false)
{
var baseVersion = GetChromiumVersion();
var cache = GetCacheDir();
if (pro)
{
var proMarker = Path.Combine(cache, $"latest_pro_version_{GetPlatformTag()}");
if (File.Exists(proMarker))
{
try
{
var version = File.ReadAllText(proMarker).Trim();
// Match launch's ProBinaryReady (exists AND executable) so `info`
// never reports a build that launch would reject.
if (!string.IsNullOrEmpty(version) && IsExecutableFile(GetBinaryPath(version, pro: true)))
return version;
}
catch (Exception ex) when (ex is FormatException or IOException) { }
}
return null;
}
foreach (var name in new[] { $"latest_version_{GetPlatformTag()}", "latest_version" })
{
var marker = Path.Combine(cache, name);
if (File.Exists(marker))
{
try
{
var version = File.ReadAllText(marker).Trim();
if (!string.IsNullOrEmpty(version) && VersionNewer(version, baseVersion))
{
var binary = GetBinaryPath(version);
if (File.Exists(binary))
return version;
}
}
catch (Exception ex) when (ex is FormatException or IOException) { }
}
}
return baseVersion;
}
/// <summary>True when a binary exists and is executable. Canonical check shared with Download.</summary>
internal static bool IsExecutableFile(string path)
{
if (!File.Exists(path)) return false;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return true;
var mode = File.GetUnixFileMode(path);
return (mode & (UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute)) != 0;
}
/// <summary>Parse "145.0.7718.0" into (145, 0, 7718, 0) for comparison.</summary>
public static int[] VersionTuple(string v) =>
v.Split('.').Select(int.Parse).ToArray();
/// <summary>Return true if version a is strictly newer than version b.</summary>
public static bool VersionNewer(string a, string b)
{
var ta = VersionTuple(a);
var tb = VersionTuple(b);
int len = Math.Max(ta.Length, tb.Length);
for (int i = 0; i < len; i++)
{
int va = i < ta.Length ? ta[i] : 0;
int vb = i < tb.Length ? tb[i] : 0;
if (va != vb) return va > vb;
}
return false;
}
// -----------------------------------------------------------------------
// Download URL
// -----------------------------------------------------------------------
public static string DownloadBaseUrl =>
Environment.GetEnvironmentVariable("CLOAKBROWSER_DOWNLOAD_URL") ?? "https://cloakbrowser.dev";
public const string GitHubApiUrl = "https://api.github.com/repos/CloakHQ/cloakbrowser/releases";
public const string GitHubDownloadBaseUrl =
"https://github.com/CloakHQ/cloakbrowser/releases/download";
/// <summary>Return the archive extension for the current platform (.zip for Windows, .tar.gz otherwise).</summary>
public static string GetArchiveExt() =>
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".zip" : ".tar.gz";
/// <summary>Return the archive filename for a platform tag (e.g. 'cloakbrowser-linux-x64.tar.gz').</summary>
public static string GetArchiveName(string? tag = null)
{
var t = tag ?? GetPlatformTag();
return $"cloakbrowser-{t}{GetArchiveExt()}";
}
/// <summary>Return the full download URL for the current platform's binary archive.</summary>
public static string GetDownloadUrl(string? version = null)
{
var v = version ?? GetChromiumVersion();
return $"{DownloadBaseUrl}/chromium-v{v}/{GetArchiveName()}";
}
/// <summary>Return the GitHub Releases fallback URL for the binary archive.</summary>
public static string GetFallbackDownloadUrl(string? version = null)
{
var v = version ?? GetChromiumVersion();
return $"{GitHubDownloadBaseUrl}/chromium-v{v}/{GetArchiveName()}";
}
// -----------------------------------------------------------------------
// CloakBrowser Pro download URLs (cloakbrowser.dev, license-key authed)
// -----------------------------------------------------------------------
/// <summary>
/// Return the Pro binary download URL for an explicit version. The version is
/// requested explicitly so the served archive matches the signed Pro manifest.
/// </summary>
public static string GetProDownloadUrl(string version) =>
$"{DownloadBaseUrl}/api/download/{version}";
/// <summary>Return the base URL for the Pro signed manifest (SHA256SUMS + .sig) of a version.</summary>
public static string GetProManifestBaseUrl(string version) =>
$"{DownloadBaseUrl}/releases/pro/chromium-v{version}";
/// <summary>Return the "latest Pro" display download URL (shown by binary_info for Pro installs).</summary>
public static string GetProLatestDownloadUrl() =>
$"{DownloadBaseUrl}/api/download/latest";
// -----------------------------------------------------------------------
// Local binary override (skip download, use your own build)
// -----------------------------------------------------------------------
/// <summary>
/// Check if the user has set a local binary path via the <c>CLOAKBROWSER_BINARY_PATH</c> env var.
/// </summary>
public static string? GetLocalBinaryOverride() =>
Environment.GetEnvironmentVariable("CLOAKBROWSER_BINARY_PATH");
// First Chromium build that reports coherent headless dimensions without an
// emulated viewport. On these binaries the wrapper launches headless with no
// viewport; older binaries need a fixed default viewport to stay coherent.
// null => not shipped yet; feature off, behavior byte-identical to today.
// TODO: set to the chromium version string that first ships it.
public static readonly string? HeadlessNoViewportMinVersion = "148.0.7778.215.4";
/// <summary>
/// Whether headless can launch without an emulated viewport on the resolved binary.
/// Only binaries at or above <see cref="HeadlessNoViewportMinVersion"/> qualify; older
/// ones keep the fixed default viewport. A local override binary
/// (<c>CLOAKBROWSER_BINARY_PATH</c>) is unknown-version, so stay on the safe path.
/// </summary>
public static bool BinarySupportsHeadlessNoViewport(string? licenseKey = null, string? browserVersion = null)
{
if (HeadlessNoViewportMinVersion == null)
return false;
// A declared version (browserVersion arg OR CLOAKBROWSER_VERSION env) wins even
// under a local override — the caller asserts the version (also how internal builds
// opt in). Only an override with no declared version stays on the safe path.
string? declared;
try
{
declared = NormalizeRequestedVersion(browserVersion);
}
catch
{
declared = null;
}
string? version;
if (!string.IsNullOrEmpty(declared))
{
version = declared!;
}
else if (GetLocalBinaryOverride() != null)
{
return false;
}
else
{
bool pro = !string.IsNullOrEmpty(License.ResolveLicenseKey(licenseKey));
version = GetEffectiveVersion(pro);
}
// No cached Pro build resolvable (GetEffectiveVersion returned null) → fail safe.
if (version == null) return false;
try
{
return !VersionNewer(HeadlessNoViewportMinVersion, version);
}
catch
{
return false;
}
}
/// <summary>
/// Whether the wrapper may auto-add <c>--start-maximized</c>. Gated on the same
/// threshold as the no_viewport shim: only binaries whose headless surface-fix +
/// headed screen-clamp make a maximized window coherent (<c>outer == screen</c>).
/// Below it, maximizing headless while the CDP viewport stays at 1280x720 yields
/// <c>outerWidth &lt; innerWidth</c> — a bot tell — so the flag must NOT be added.
/// Shares <see cref="HeadlessNoViewportMinVersion"/>; own name so the two can
/// diverge later. Python, JS and .NET mirror this gate.
/// </summary>
public static bool BinarySupportsMaximizedWindow(string? licenseKey = null, string? browserVersion = null)
=> BinarySupportsHeadlessNoViewport(licenseKey, browserVersion);
// First Chromium build, per platform, whose binary can take inline HTTP proxy
// credentials (Chromium-native --proxy-server=http://user:pass@host). Below the
// floor the wrapper MUST route credentialed HTTP/HTTPS proxies through
// Playwright's proxy object — an older binary treats the user:pass@ authority as
// an invalid proxy host and drops proxy auth (#182). A single global threshold
// can't model this: the capability landed in different build lineages at different
// points (linux-x64/windows-x64 at 146.0.7680.177.5, but the free macOS 145.x and
// linux-arm64 146.0.7680.177.3 floors predate it, qualifying only once they resolve
// to a Pro/newer 148+ build). Platforms absent from this map are never-inline.
public static readonly IReadOnlyDictionary<string, string> HttpProxyInlineAuthMinVersion =
new Dictionary<string, string>
{
["linux-x64"] = "146.0.7680.177.5",
["windows-x64"] = "146.0.7680.177.5",
["linux-arm64"] = "148.0.7778.215.3",
["darwin-arm64"] = "148.0.7778.215.3",
["darwin-x64"] = "148.0.7778.215.3",
};
/// <summary>
/// Whether the resolved binary accepts inline HTTP proxy credentials. The
/// capability is a function of the binary version; <paramref name="licenseKey"/>
/// only resolves which version launches (Pro build vs free default), exactly like
/// <see cref="BinarySupportsHeadlessNoViewport"/>. A declared pin wins, else a valid
/// Pro license resolves to the Pro build (which ships the patch), else the free
/// per-platform default — then it compares to this platform's floor. Below the
/// floor, credentialed HTTP proxies fall back to Playwright's proxy object. A local
/// override with no declared version is unknown-version, so stay on the safe
/// fallback. Python, JS and .NET mirror this gate.
/// </summary>
public static bool BinarySupportsHttpProxyInlineAuth(string? licenseKey = null, string? browserVersion = null)
{
string tag;
try
{
tag = GetPlatformTag();
}
catch
{
return false;
}
if (!HttpProxyInlineAuthMinVersion.TryGetValue(tag, out var floor))
return false;
string? declared;
try
{
declared = NormalizeRequestedVersion(browserVersion);
}
catch
{
declared = null;
}
string? version;
if (!string.IsNullOrEmpty(declared))
{
version = declared!;
}
else if (GetLocalBinaryOverride() != null)
{
return false;
}
else
{
bool pro = !string.IsNullOrEmpty(License.ResolveLicenseKey(licenseKey));
version = GetEffectiveVersion(pro);
}
if (version == null) return false;
try
{
return !VersionNewer(floor, version);
}
catch
{
return false;
}
}
}