mirror of
https://github.com/lukaszraczylo/mcp-captcharesolver.git
synced 2026-10-20 01:27:53 +00:00
feat(stealth): make evasion script coherent with generated fingerprint
- Convert assets/evasions.js to evasions.js.tmpl (Go text/template) - Placeholders: WebGLVendor, WebGLRenderer, Platform, Languages - Add navigator.platform spoof from fingerprint - ScriptParams gains OS, Browser, Locale, Seed fields (fieldalignment ordered) - ScriptResult gains Fingerprint field — returns the identity the script was rendered for - StealthScript calls GenerateFingerprint and renders template; single call gives both script and coherent fingerprint - languagesJS: locale to JS list (en-GB -> 'en-GB', 'en') - Template parsed once at init via template.Must - StealthScriptIn in tools.go gains OS, Browser, Locale, Seed pass-through - Tests: added TestStealthScriptCoherence asserting rendered values match fingerprint
This commit is contained in:
+12
-2
@@ -132,11 +132,21 @@ func (h *Handlers) solveRotation(ctx context.Context, _ *mcp.CallToolRequest, in
|
|||||||
// ---- stealth tools ----
|
// ---- stealth tools ----
|
||||||
|
|
||||||
type StealthScriptIn struct {
|
type StealthScriptIn struct {
|
||||||
Engine string `json:"engine,omitempty" jsonschema:"playwright | cdp | generic"`
|
Seed uint64 `json:"seed,omitempty" jsonschema:"use the same seed as generate_fingerprint to keep the injected script and applied profile consistent"`
|
||||||
|
OS string `json:"os,omitempty" jsonschema:"windows | macos | linux — use the same os/browser/locale/seed as generate_fingerprint, or read the returned fingerprint, to keep the injected script and applied profile consistent"`
|
||||||
|
Browser string `json:"browser,omitempty" jsonschema:"chrome"`
|
||||||
|
Locale string `json:"locale,omitempty" jsonschema:"BCP-47 locale e.g. en-US, en-GB — use the same locale as generate_fingerprint for a coherent profile"`
|
||||||
|
Engine string `json:"engine,omitempty" jsonschema:"playwright | cdp | generic"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handlers) stealthScript(_ context.Context, _ *mcp.CallToolRequest, in StealthScriptIn) (*mcp.CallToolResult, stealth.ScriptResult, error) {
|
func (h *Handlers) stealthScript(_ context.Context, _ *mcp.CallToolRequest, in StealthScriptIn) (*mcp.CallToolResult, stealth.ScriptResult, error) {
|
||||||
return nil, stealth.StealthScript(stealth.ScriptParams{Engine: in.Engine}), nil
|
return nil, stealth.StealthScript(stealth.ScriptParams{
|
||||||
|
OS: in.OS,
|
||||||
|
Browser: in.Browser,
|
||||||
|
Locale: in.Locale,
|
||||||
|
Seed: in.Seed,
|
||||||
|
Engine: in.Engine,
|
||||||
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type FingerprintIn struct {
|
type FingerprintIn struct {
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
(() => {
|
||||||
|
// navigator.webdriver
|
||||||
|
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
||||||
|
// languages
|
||||||
|
Object.defineProperty(navigator, 'languages', { get: () => [{{.Languages}}] });
|
||||||
|
// navigator.platform
|
||||||
|
Object.defineProperty(navigator, 'platform', { get: () => '{{.Platform}}' });
|
||||||
|
// plugins (non-empty)
|
||||||
|
Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });
|
||||||
|
// chrome runtime stub
|
||||||
|
window.chrome = window.chrome || { runtime: {} };
|
||||||
|
// permissions query consistency
|
||||||
|
const origQuery = window.navigator.permissions && window.navigator.permissions.query;
|
||||||
|
if (origQuery) {
|
||||||
|
window.navigator.permissions.query = (params) =>
|
||||||
|
params && params.name === 'notifications'
|
||||||
|
? Promise.resolve({ state: Notification.permission })
|
||||||
|
: origQuery(params);
|
||||||
|
}
|
||||||
|
// WebGL vendor/renderer spoof
|
||||||
|
const getParam = WebGLRenderingContext.prototype.getParameter;
|
||||||
|
WebGLRenderingContext.prototype.getParameter = function (p) {
|
||||||
|
if (p === 37445) return '{{.WebGLVendor}}'; // UNMASKED_VENDOR_WEBGL
|
||||||
|
if (p === 37446) return '{{.WebGLRenderer}}'; // UNMASKED_RENDERER_WEBGL
|
||||||
|
return getParam.call(this, p);
|
||||||
|
};
|
||||||
|
})();
|
||||||
+76
-13
@@ -1,13 +1,27 @@
|
|||||||
package stealth
|
package stealth
|
||||||
|
|
||||||
import _ "embed"
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
//go:embed assets/evasions.js
|
//go:embed assets/evasions.js.tmpl
|
||||||
var evasionsJS string
|
var evasionsTmplSrc string
|
||||||
|
|
||||||
// ScriptParams selects the target engine for apply instructions.
|
// evasionTmpl is parsed once at startup; template.Must panics on bad syntax.
|
||||||
|
var evasionTmpl = template.Must(template.New("evasions").Parse(evasionsTmplSrc))
|
||||||
|
|
||||||
|
// ScriptParams selects identity and target engine for apply instructions.
|
||||||
|
// Fields ordered for fieldalignment (largest to smallest, then strings).
|
||||||
type ScriptParams struct {
|
type ScriptParams struct {
|
||||||
Engine string // playwright | cdp | generic
|
Seed uint64 // deterministic generation; 0 → random
|
||||||
|
OS string // windows | macos | linux
|
||||||
|
Browser string // chrome
|
||||||
|
Locale string // e.g. en-US, en-GB
|
||||||
|
Engine string // playwright | cdp | generic
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApplyInfo tells the caller how to inject the script.
|
// ApplyInfo tells the caller how to inject the script.
|
||||||
@@ -18,13 +32,59 @@ type ApplyInfo struct {
|
|||||||
|
|
||||||
// ScriptResult is the injectable evasion bundle plus metadata.
|
// ScriptResult is the injectable evasion bundle plus metadata.
|
||||||
type ScriptResult struct {
|
type ScriptResult struct {
|
||||||
Script string `json:"script"`
|
Script string `json:"script"`
|
||||||
Apply ApplyInfo `json:"apply"`
|
Apply ApplyInfo `json:"apply"`
|
||||||
IncludedEvasions []string `json:"included_evasions"`
|
Fingerprint Fingerprint `json:"fingerprint"`
|
||||||
|
IncludedEvasions []string `json:"included_evasions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// StealthScript returns the evasion init script and engine-specific apply hints.
|
// evasionData is the template data bag.
|
||||||
|
type evasionData struct {
|
||||||
|
WebGLVendor string
|
||||||
|
WebGLRenderer string
|
||||||
|
Platform string
|
||||||
|
Languages string // pre-rendered JS list, e.g. 'en-GB', 'en'
|
||||||
|
}
|
||||||
|
|
||||||
|
// languagesJS converts a locale string to a JS list of quoted language tags.
|
||||||
|
// "en-GB" → `'en-GB', 'en'`; "en" → `'en'`; "" → `'en-US', 'en'`
|
||||||
|
func languagesJS(locale string) string {
|
||||||
|
if locale == "" {
|
||||||
|
locale = "en-US"
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(locale, "-", 2)
|
||||||
|
if len(parts) == 2 && parts[0] != locale {
|
||||||
|
return fmt.Sprintf("'%s', '%s'", locale, parts[0])
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("'%s'", locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StealthScript returns an evasion init script rendered from a coherent
|
||||||
|
// Fingerprint, plus engine-specific apply hints. The Fingerprint used for
|
||||||
|
// rendering is included in ScriptResult so callers can apply it without a
|
||||||
|
// second call. Empty params produce valid output (windows/chrome/en-US defaults).
|
||||||
func StealthScript(p ScriptParams) ScriptResult {
|
func StealthScript(p ScriptParams) ScriptResult {
|
||||||
|
fp := GenerateFingerprint(FingerprintParams{
|
||||||
|
OS: p.OS,
|
||||||
|
Browser: p.Browser,
|
||||||
|
Locale: p.Locale,
|
||||||
|
Seed: p.Seed,
|
||||||
|
})
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
// Execute errors are non-fatal — the template is static aside from the data
|
||||||
|
// substitution; if it somehow fails, fall back to a descriptive comment.
|
||||||
|
if err := evasionTmpl.Execute(&buf, evasionData{
|
||||||
|
WebGLVendor: fp.WebGLVendor,
|
||||||
|
WebGLRenderer: fp.WebGLRenderer,
|
||||||
|
Platform: fp.Platform,
|
||||||
|
Languages: languagesJS(fp.Locale),
|
||||||
|
}); err != nil {
|
||||||
|
// Fallback: return an empty-IIFE comment so the script is valid JS.
|
||||||
|
buf.Reset()
|
||||||
|
fmt.Fprintf(&buf, "(() => { /* evasion render error: %v */ })();", err)
|
||||||
|
}
|
||||||
|
|
||||||
apply := ApplyInfo{How: "Inject before any page script runs.", API: "page.addInitScript(script)"}
|
apply := ApplyInfo{How: "Inject before any page script runs.", API: "page.addInitScript(script)"}
|
||||||
switch p.Engine {
|
switch p.Engine {
|
||||||
case "cdp":
|
case "cdp":
|
||||||
@@ -32,12 +92,15 @@ func StealthScript(p ScriptParams) ScriptResult {
|
|||||||
case "generic", "":
|
case "generic", "":
|
||||||
apply.API = "evaluate the script on every new document before navigation"
|
apply.API = "evaluate the script on every new document before navigation"
|
||||||
}
|
}
|
||||||
|
|
||||||
return ScriptResult{
|
return ScriptResult{
|
||||||
Script: evasionsJS,
|
Script: buf.String(),
|
||||||
Apply: apply,
|
Apply: apply,
|
||||||
|
Fingerprint: fp,
|
||||||
IncludedEvasions: []string{
|
IncludedEvasions: []string{
|
||||||
"navigator.webdriver", "navigator.languages", "navigator.plugins",
|
"navigator.webdriver", "navigator.languages", "navigator.platform",
|
||||||
"chrome.runtime", "permissions.query", "WebGL vendor/renderer",
|
"navigator.plugins", "chrome.runtime", "permissions.query",
|
||||||
|
"WebGL vendor/renderer",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,3 +19,30 @@ func TestStealthScript(t *testing.T) {
|
|||||||
t.Fatalf("missing apply metadata: %+v", res)
|
t.Fatalf("missing apply metadata: %+v", res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStealthScriptCoherence(t *testing.T) {
|
||||||
|
p := ScriptParams{OS: "macos", Locale: "en-GB", Seed: 7}
|
||||||
|
res := StealthScript(p)
|
||||||
|
|
||||||
|
// Independently generate the same fingerprint.
|
||||||
|
fp := GenerateFingerprint(FingerprintParams{OS: "macos", Locale: "en-GB", Seed: 7})
|
||||||
|
|
||||||
|
// Script must contain the fingerprint's WebGL and platform values.
|
||||||
|
for _, want := range []string{fp.WebGLVendor, fp.WebGLRenderer, fp.Platform} {
|
||||||
|
if !strings.Contains(res.Script, want) {
|
||||||
|
t.Errorf("script missing %q", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Script must contain navigator.platform spoof.
|
||||||
|
if !strings.Contains(res.Script, "navigator.platform") {
|
||||||
|
t.Error("script missing navigator.platform spoof")
|
||||||
|
}
|
||||||
|
// ScriptResult.Fingerprint must equal independently generated fingerprint.
|
||||||
|
if res.Fingerprint != fp {
|
||||||
|
t.Errorf("fingerprint mismatch:\n got %+v\n want %+v", res.Fingerprint, fp)
|
||||||
|
}
|
||||||
|
// Template must not produce empty script.
|
||||||
|
if res.Script == "" {
|
||||||
|
t.Fatal("empty rendered script")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user