Repository navigation
Fingerprint and Environment
English · 中文
ruyiJS does not inject page globals to fake a browser. You give each Context a validated environment profile, and the page's own scripts read Navigator, Screen, the viewport, WebGL and the rest from the native browser APIs.
from ruyijs import Context
with Context() as context:
context.configure_fingerprint({
"navigator": {"language": "zh-CN", "hardwareConcurrency": 8},
"screen": {"width": 1920, "height": 1080},
"viewport": {"width": 1280, "height": 720},
"locale": {"timezone": "Asia/Shanghai"},
})
print(context.eval("[navigator.language, screen.width]")) # ['zh-CN', 1920]Or pass the profile to the constructor, as the DataDome example does:
ENVIRONMENT = {"screen": {"width": 1920, "height": 1080, "availWidth": 1920, "availHeight": 1040},
"viewport": {"width": 1904, "height": 987}, "locale": {"timezone": "Asia/Shanghai"}}
with Context(timeout=90, environment=ENVIRONMENT) as context:
...How it behaves (from the docstrings):
-
configure_fingerprint()andconfigure_environment()accept the same groups and schema; the first simply calls the second. - A profile is partial. The fields you pass are merged into the current state atomically.
- Fields and ranges are validated by the runtime. A rejected configuration raises (as a
JSErrorcarrying the runtime's message) and leaves the existing state unchanged, so you can fix it and retry. - Configure before the first page operation, so every script observes one profile.
- The call returns the resulting environment snapshot.
get_defaults()returns the fixed Firefox baseline, independent of your overrides. - The argument must be plain JSON data: a
dictwith string keys andNone/str/bool/int/ finitefloat/list/dictvalues.
The schema belongs to the runtime. The table lists the groups and keys that the README and the shipped examples actually set — it is not the full schema. Call get_defaults() to inspect the baseline.
| Group | Keys seen in the README and examples |
|---|---|
navigator |
userAgent, platform, appCodeName, appName, appVersion, product, productSub, vendor, vendorSub, language, languages, hardwareConcurrency, maxTouchPoints, cookieEnabled, webdriver, oscpu, buildID, doNotTrack, pdfViewerEnabled, onLine, globalPrivacyControl
|
screen |
width, height, availWidth, availHeight, availLeft, availTop, colorDepth, pixelDepth
|
viewport |
width, height, devicePixelRatio
|
locale |
timezone (an IANA name such as "Asia/Shanghai") |
webgl |
vendor, renderer, version, glslVersion, unmaskedVendor, unmaskedRenderer, version2, glslVersion2, attributes, extensions, limits
|
speech |
voices, langs, default
|
The full Firefox-on-Windows profile the Akamai example applies is PROFILE in examples/akamai/akamai.py. A subset of it:
PROFILE_SUBSET = {
"navigator": {
"platform": "Win32",
"language": "zh-CN",
"languages": ["zh-CN", "zh", "en-US", "en"],
"hardwareConcurrency": 12,
"maxTouchPoints": 0,
"webdriver": False,
"oscpu": "Windows NT 10.0; Win64; x64",
},
"screen": {"width": 1920, "height": 1080, "availWidth": 1920, "availHeight": 1040,
"availLeft": 0, "availTop": 0, "colorDepth": 24, "pixelDepth": 24},
"viewport": {"width": 1536, "height": 864, "devicePixelRatio": 1.25},
"locale": {"timezone": "Asia/Shanghai"},
}Context(tls=...) changes only the network fingerprint (TLS ClientHello, HTTP/2 settings, HTTP/3 priorities). When the page should agree, set a user agent of the same Firefox version. The Akamai example derives it from its --tls option; the same idea in a few lines:
version = "120"
with Context(tls=version) as context:
context.configure_fingerprint({"navigator": {"userAgent":
f"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:{version}.0) Gecko/20100101 Firefox/{version}.0"}})More on TLS profiles: Network and TLS.
context.configure_clock("logical", epoch_ms=1700000000000) # deterministic
context.configure_clock("system") # live work-
Logical: requires an explicit integer epoch in milliseconds. Time moves only through
advance(),tick()orsleep(), without wall-clock waiting, so runs are reproducible. -
System:
advance()waits in real time. The live CreepJS and Akamai examples select it before navigating. - Configure the clock before page work or timer advancement.
clock_status()reads the current clock and the pending timer count without running timers.
Timer precision is Firefox 159's (CHANGELOG 0.2.1): performance.now(), timeOrigin, Date.now(), Event.timeStamp and every navigation/resource time are clamped to 1 ms with Firefox's jittered rounding, one seed per document.
Scripts can tell synthetic events from user input through isTrusted; the Akamai example README lists this among the checks. The input methods create trusted events natively, in Firefox's order:
context.click("document.querySelector('#submit')")
context.type("hello", target="document.querySelector('input[name=q]')")
context.press("Enter")
context.dispatch_input("mousemove", target="document.documentElement", clientX=200, clientY=150)target is always a JavaScript expression. dispatch_input sends exactly one Mouse/Pointer event with no layout or hit testing; click, type and press produce the full user-like sequence. See API Reference for timing parameters.
With the logical clock, the replay controls let you re-run a captured flow deterministically:
-
configure_random(state0, state1)— the initial page'sMath.randomstate; -
configure_crypto_replay(documents)—crypto.getRandomValuesandrandomUUIDvalues per document; -
configure_navigation_replay(documents)— navigation timing fields per document; -
configure_timer_replay(),pending_timers(),run_timer(...)— explicit timer selection (message replay has matching methods); -
put_resource(...)— every response served offline (Network and TLS).
Signatures and limits: API Reference.
Create the context with mode="debug" to record accesses to registered browser API members, then read them with peek_api_log(), get_api_log() (drains) or clear_api_log(). Properties a script defines itself are not recorded.
ruyiJS · GitHub · PyPI · LinuxDO · License: AGPL-3.0-only or commercial