feat: open the browser window maximized on 148.0.7778.215.4+ builds

Default headed and headless launches to a maximized window (fills the
screen) on binaries at or above the same threshold as the headless
no-viewport default. Suppressed when the caller sets --window-size /
--window-position / --start-maximized or an explicit viewport; older
builds are unchanged. Version-gated via a dedicated helper sharing the
no-viewport threshold. Mirrored across Python, JS and .NET with parity tests.

The Docker image runs openbox so headed --start-maximized is honored
(bare Xvfb has no window manager; headless is unaffected).
This commit is contained in:
CloakHQ
2026-07-04 20:20:01 +02:00
parent 88d62e04ca
commit d029f2170f
13 changed files with 270 additions and 10 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libgdk-pixbuf-2.0-0 libxss1 libxtst6 fonts-liberation \
fonts-noto-color-emoji fonts-unifont fonts-freefont-ttf \
fonts-ipafont-gothic fonts-wqy-zenhei fonts-tlwg-loma-otf \
xvfb xdotool \
xvfb xdotool openbox \
curl ca-certificates \
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
+5
View File
@@ -11,6 +11,11 @@ rm -f /tmp/.X99-lock /tmp/.X11-unix/X99
Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp &
sleep 1
# Window manager so headed --start-maximized is honored (bare Xvfb has no WM;
# without one the flag is a silent no-op and the window stays un-maximized).
DISPLAY=:99 openbox &
sleep 1
# Opt-in: fetch the Widevine CDM so persistent contexts present as a real
# Chrome (a DRM/EME probe is used by some bot detectors). Off by default — only
# runs when CLOAKBROWSER_FETCH_WIDEVINE is set, and never if the user already
+17 -5
View File
@@ -24,6 +24,7 @@ from .config import (
DEFAULT_VIEWPORT,
IGNORE_DEFAULT_ARGS,
binary_supports_headless_no_viewport,
binary_supports_maximized_window,
get_default_stealth_args,
normalize_requested_version,
)
@@ -210,7 +211,7 @@ def launch(
args = list(args or [])
args.append(f"--fingerprint-webrtc-ip={exit_ip}")
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless, extension_paths=extension_paths)
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless, extension_paths=extension_paths, start_maximized=binary_supports_maximized_window(license_key, browser_version))
_maybe_warn_windows_fonts(chrome_args)
logger.debug("Launching stealth Chromium (headless=%s, args=%d)", headless, len(chrome_args))
@@ -316,7 +317,7 @@ async def launch_async( # noqa: C901
if exit_ip and not (args and any(a.startswith("--fingerprint-webrtc-ip") for a in args)):
args = list(args or [])
args.append(f"--fingerprint-webrtc-ip={exit_ip}")
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless, extension_paths=extension_paths)
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless, extension_paths=extension_paths, start_maximized=binary_supports_maximized_window(license_key, browser_version))
_maybe_warn_windows_fonts(chrome_args)
logger.debug("Launching stealth Chromium async (headless=%s, args=%d)", headless, len(chrome_args))
@@ -434,7 +435,7 @@ def launch_persistent_context(
if exit_ip and not (args and any(a.startswith("--fingerprint-webrtc-ip") for a in args)):
args = list(args or [])
args.append(f"--fingerprint-webrtc-ip={exit_ip}")
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless, extension_paths=extension_paths)
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless, extension_paths=extension_paths, start_maximized=binary_supports_maximized_window(license_key, browser_version) and viewport is _VIEWPORT_UNSET and "viewport" not in kwargs and "no_viewport" not in kwargs)
_maybe_warn_windows_fonts(chrome_args)
logger.debug(
@@ -573,7 +574,7 @@ async def launch_persistent_context_async(
if exit_ip and not (args and any(a.startswith("--fingerprint-webrtc-ip") for a in args)):
args = list(args or [])
args.append(f"--fingerprint-webrtc-ip={exit_ip}")
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless, extension_paths=extension_paths)
chrome_args = build_args(stealth_args, (args or []) + proxy_extra_args, timezone=timezone, locale=locale, headless=headless, extension_paths=extension_paths, start_maximized=binary_supports_maximized_window(license_key, browser_version) and viewport is _VIEWPORT_UNSET and "viewport" not in kwargs and "no_viewport" not in kwargs)
_maybe_warn_windows_fonts(chrome_args)
logger.debug(
@@ -1079,6 +1080,7 @@ def build_args(
locale: str | None = None,
headless: bool = True,
extension_paths: list[str] | None = None,
start_maximized: bool = False,
) -> list[str]:
"""Combine stealth args with user-provided args and locale flags.
@@ -1125,12 +1127,22 @@ def build_args(
if extension_paths:
abs_paths = [os.path.abspath(p) for p in extension_paths]
ext_val = ",".join(abs_paths)
seen["--load-extension"] = f"--load-extension={ext_val}"
seen["--disable-extensions-except"] = (
f"--disable-extensions-except={ext_val}"
)
# Open maximized (real Windows Chrome overwhelmingly runs maximized) so the
# window fills the spoofed screen. Skipped if the caller already chose a
# window geometry. Gated to binaries where this stays coherent (see
# binary_supports_maximized_window) — below the gate it would create
# outerWidth < innerWidth.
if start_maximized and not any(
k in seen for k in ("--start-maximized", "--window-size", "--window-position")
):
seen["--start-maximized"] = "--start-maximized"
return list(seen.values())
+16
View File
@@ -336,3 +336,19 @@ def binary_supports_headless_no_viewport(
return not _version_newer(HEADLESS_NO_VIEWPORT_MIN_VERSION, version)
except (ValueError, AttributeError):
return False
def binary_supports_maximized_window(
license_key: str | None = None, browser_version: str | None = None
) -> bool:
"""Whether the wrapper may auto-add ``--start-maximized``.
Gated on the same threshold as the no_viewport shim: only binaries whose
headless surface-fix + headed screen-clamp make a maximized window coherent
(``outer == screen``). Below it, maximizing headless while the CDP viewport
stays at 1280x720 yields ``outerWidth < innerWidth`` — an impossible-window
bot tell — so the flag must NOT be added. Shares
``HEADLESS_NO_VIEWPORT_MIN_VERSION``; kept as its own name so the two can
diverge later. Python, JS and .NET mirror this gate.
"""
return binary_supports_headless_no_viewport(license_key, browser_version)
+19 -3
View File
@@ -32,7 +32,8 @@ public static class CloakLauncher
var combined = new List<string>(args ?? new List<string>());
combined.AddRange(proxyResolution.ExtraArgs);
var chromeArgs = BuildArgs(options.StealthArgs, combined, timezone, locale, options.Headless, options.ExtensionPaths);
var chromeArgs = BuildArgs(options.StealthArgs, combined, timezone, locale, options.Headless, options.ExtensionPaths,
startMaximized: Config.BinarySupportsMaximizedWindow(options.LicenseKey, options.BrowserVersion));
MaybeWarnWindowsFonts(chromeArgs);
CloakLog.Debug($"Launching stealth Chromium (headless={options.Headless}, args={chromeArgs.Count})");
@@ -137,7 +138,9 @@ public static class CloakLauncher
var combined = new List<string>(args ?? new List<string>());
combined.AddRange(proxyResolution.ExtraArgs);
var chromeArgs = BuildArgs(options.StealthArgs, combined, timezone, locale, options.Headless, options.ExtensionPaths);
var chromeArgs = BuildArgs(options.StealthArgs, combined, timezone, locale, options.Headless, options.ExtensionPaths,
startMaximized: Config.BinarySupportsMaximizedWindow(options.LicenseKey, options.BrowserVersion)
&& !options.NoViewport && options.Viewport == null);
MaybeWarnWindowsFonts(chromeArgs);
CloakLog.Debug($"Launching persistent stealth Chromium (headless={options.Headless}, user_data_dir={userDataDir})");
@@ -274,7 +277,8 @@ public static class CloakLauncher
string? timezone = null,
string? locale = null,
bool headless = true,
List<string>? extensionPaths = null)
List<string>? extensionPaths = null,
bool startMaximized = false)
{
// Preserve insertion order while deduping by key.
var seen = new Dictionary<string, string>();
@@ -322,6 +326,18 @@ public static class CloakLauncher
Set("--disable-extensions-except", $"--disable-extensions-except={extVal}");
}
// Open maximized (real Chrome overwhelmingly runs maximized) so the window
// fills the spoofed screen. Skipped if the caller chose a window geometry.
// Gated to binaries where this stays coherent (see BinarySupportsMaximizedWindow)
// — below the gate it would make outerWidth < innerWidth.
if (startMaximized
&& !seen.ContainsKey("--start-maximized")
&& !seen.ContainsKey("--window-size")
&& !seen.ContainsKey("--window-position"))
{
Set("--start-maximized", "--start-maximized");
}
return order.Select(k => seen[k]).ToList();
}
+12
View File
@@ -426,4 +426,16 @@ public static class Config
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);
}
@@ -73,4 +73,39 @@ public class BuildArgsTests
Assert.DoesNotContain(args, a => a.StartsWith("--lang="));
Assert.DoesNotContain(args, a => a.StartsWith("--fingerprint-timezone="));
}
[Fact]
public void StartMaximized_True_AddsFlag()
{
var args = CloakLauncher.BuildArgs(stealthArgs: true, extraArgs: null, startMaximized: true);
Assert.Contains("--start-maximized", args);
}
[Fact]
public void StartMaximized_DefaultOff_NoFlag()
{
var args = CloakLauncher.BuildArgs(stealthArgs: true, extraArgs: null);
Assert.DoesNotContain("--start-maximized", args);
}
[Fact]
public void StartMaximized_SuppressedByUserWindowSize()
{
var args = CloakLauncher.BuildArgs(
stealthArgs: true,
extraArgs: new List<string> { "--window-size=1000,800" },
startMaximized: true);
Assert.DoesNotContain("--start-maximized", args);
Assert.Contains("--window-size=1000,800", args);
}
[Fact]
public void StartMaximized_NotDoubled()
{
var args = CloakLauncher.BuildArgs(
stealthArgs: true,
extraArgs: new List<string> { "--start-maximized" },
startMaximized: true);
Assert.Single(args, a => a == "--start-maximized");
}
}
@@ -238,3 +238,28 @@ public class HeadlessNoViewportGateTests
}
}
}
/// <summary>
/// BinarySupportsMaximizedWindow() — parity-critical: Python and JS mirror this gate.
/// Shares the no_viewport threshold today.
/// </summary>
public class MaximizedWindowGateTests
{
[Fact]
public void DeclaredBelowThreshold_Off()
{
Assert.False(Config.BinarySupportsMaximizedWindow(browserVersion: "148.0.7778.215.3"));
}
[Fact]
public void DeclaredAtThreshold_On()
{
Assert.True(Config.BinarySupportsMaximizedWindow(browserVersion: "148.0.7778.215.4"));
}
[Fact]
public void DeclaredAboveThreshold_On()
{
Assert.True(Config.BinarySupportsMaximizedWindow(browserVersion: "149.0.0.0"));
}
}
+22 -1
View File
@@ -3,7 +3,7 @@
*/
import path from "path";
import type { LaunchOptions } from "./types.js";
import { getDefaultStealthArgs } from "./config.js";
import { getDefaultStealthArgs, binarySupportsMaximizedWindow } from "./config.js";
const DEBUG = /\bcloakbrowser\b/.test(process.env.DEBUG ?? "");
@@ -66,5 +66,26 @@ export function buildArgs(options: LaunchOptions): string[] {
`--disable-extensions-except=${joined}`
);
}
// Open maximized (real Chrome overwhelmingly runs maximized) so the window
// fills the spoofed screen. Skipped if the caller chose a window geometry or an
// explicit viewport (Playwright `viewport` / Puppeteer `defaultViewport`).
// Gated to binaries where this stays coherent (see binarySupportsMaximizedWindow)
// — below the gate it would make outerWidth < innerWidth.
// viewport lives on LaunchContextOptions; present at runtime for the
// persistent-context path, absent for plain launch. Read defensively.
const explicitViewport =
(options as { viewport?: unknown }).viewport !== undefined ||
options.launchOptions?.defaultViewport !== undefined;
const hasWindowFlag = ["--start-maximized", "--window-size", "--window-position"].some(
k => seen.has(k)
);
if (
!explicitViewport &&
!hasWindowFlag &&
binarySupportsMaximizedWindow(options.licenseKey, options.browserVersion)
) {
seen.set("--start-maximized", "--start-maximized");
}
return [...seen.values()];
}
+16
View File
@@ -290,6 +290,22 @@ export function binarySupportsHeadlessNoViewport(
}
}
/**
* Whether the wrapper may auto-add `--start-maximized`. Gated on the same
* threshold as the no_viewport shim: only binaries whose headless surface-fix +
* headed screen-clamp make a maximized window coherent (`outer == screen`).
* Below it, maximizing headless while the CDP viewport stays at 1280x720 yields
* `outerWidth < innerWidth` — a bot tell — so the flag must NOT be added. Shares
* HEADLESS_NO_VIEWPORT_MIN_VERSION; own name so the two can diverge later.
* Python, JS and .NET mirror this gate.
*/
export function binarySupportsMaximizedWindow(
licenseKey?: string,
browserVersion?: string,
): boolean {
return binarySupportsHeadlessNoViewport(licenseKey, browserVersion);
}
// ---------------------------------------------------------------------------
// Playwright default args to suppress — these leak automation signals.
// --enable-automation: exposes navigator.webdriver = true
+48
View File
@@ -10,6 +10,7 @@ import {
getFallbackDownloadUrl,
normalizeRequestedVersion,
binarySupportsHeadlessNoViewport,
binarySupportsMaximizedWindow,
} from "../src/config.js";
import { _buildArgsForTest, resolveTimezone } from "../src/playwright.js";
@@ -299,3 +300,50 @@ describe("binarySupportsHeadlessNoViewport", () => {
expect(binarySupportsHeadlessNoViewport(undefined, "not.a.version")).toBe(false);
});
});
describe("binarySupportsMaximizedWindow", () => {
// Parity-critical: Python and .NET mirror this gate. Shares the no_viewport
// threshold today.
it("is OFF one build below the threshold", () => {
expect(binarySupportsMaximizedWindow(undefined, "148.0.7778.215.3")).toBe(false);
});
it("is ON at the threshold", () => {
expect(binarySupportsMaximizedWindow(undefined, "148.0.7778.215.4")).toBe(true);
});
it("is ON above the threshold", () => {
expect(binarySupportsMaximizedWindow(undefined, "149.0.0.0")).toBe(true);
});
});
describe("buildArgs --start-maximized", () => {
it("adds --start-maximized when the binary is gated ON", () => {
const args = _buildArgsForTest({ browserVersion: "148.0.7778.215.4" });
expect(args).toContain("--start-maximized");
});
it("does not add it below the gate", () => {
const args = _buildArgsForTest({ browserVersion: "148.0.7778.215.3" });
expect(args).not.toContain("--start-maximized");
});
it("is suppressed by a user --window-size", () => {
const args = _buildArgsForTest({
browserVersion: "148.0.7778.215.4",
args: ["--window-size=1000,800"],
});
expect(args).not.toContain("--start-maximized");
expect(args).toContain("--window-size=1000,800");
});
it("is suppressed by an explicit viewport", () => {
const args = _buildArgsForTest({
browserVersion: "148.0.7778.215.4",
viewport: { width: 800, height: 600 },
} as Parameters<typeof _buildArgsForTest>[0]);
expect(args).not.toContain("--start-maximized");
});
it("does not double a user-supplied --start-maximized", () => {
const args = _buildArgsForTest({
browserVersion: "148.0.7778.215.4",
args: ["--start-maximized"],
});
expect(args.filter(a => a === "--start-maximized")).toHaveLength(1);
});
});
+31
View File
@@ -199,3 +199,34 @@ def test_resolve_webrtc_args_no_flag():
result = _resolve_webrtc_args(["--no-sandbox"], "http://proxy:8080")
assert result == ["--no-sandbox"]
def test_start_maximized_injected_when_gated():
"""start_maximized=True adds the flag."""
args = build_args(stealth_args=True, extra_args=None, start_maximized=True)
assert "--start-maximized" in args
def test_start_maximized_absent_by_default():
"""Default (gate off) does not add the flag."""
args = build_args(stealth_args=True, extra_args=None)
assert "--start-maximized" not in args
args = build_args(stealth_args=True, extra_args=None, start_maximized=False)
assert "--start-maximized" not in args
def test_start_maximized_suppressed_by_user_window_size():
"""A user --window-size means the user chose a geometry; don't also maximize."""
args = build_args(
stealth_args=True, extra_args=["--window-size=1000,800"], start_maximized=True
)
assert "--start-maximized" not in args
assert "--window-size=1000,800" in args
def test_start_maximized_not_doubled():
"""A user-supplied --start-maximized is not duplicated."""
args = build_args(
stealth_args=True, extra_args=["--start-maximized"], start_maximized=True
)
assert args.count("--start-maximized") == 1
+23
View File
@@ -7,6 +7,7 @@ import pytest
from cloakbrowser.config import (
binary_supports_headless_no_viewport,
binary_supports_maximized_window,
get_archive_ext,
get_archive_name,
get_binary_path,
@@ -215,3 +216,25 @@ class TestHeadlessNoViewportGate:
os.environ.pop("CLOAKBROWSER_LICENSE_KEY", None)
os.environ.pop("CLOAKBROWSER_VERSION", None)
assert binary_supports_headless_no_viewport() is False
class TestMaximizedWindowGate:
"""binary_supports_maximized_window() — parity-critical: JS and .NET mirror this.
Shares HEADLESS_NO_VIEWPORT_MIN_VERSION today, so it tracks the same threshold
as the no_viewport gate: below it, auto --start-maximized would make headless
report outerWidth < innerWidth (a bot tell), so the flag must stay off.
"""
def test_declared_below_threshold_off(self):
assert binary_supports_maximized_window(browser_version="148.0.7778.215.3") is False
def test_declared_at_threshold_on(self):
assert binary_supports_maximized_window(browser_version="148.0.7778.215.4") is True
def test_declared_above_threshold_on(self):
assert binary_supports_maximized_window(browser_version="149.0.0.0") is True
def test_local_override_without_declared_off(self):
with patch.dict(os.environ, {"CLOAKBROWSER_BINARY_PATH": "/fake/chrome"}):
assert binary_supports_maximized_window() is False