Files
nyoungo 18b2c04006 initial import
- Go service bridging deepseek.de/es/fr V4 models behind an OpenAI-compatible API
- /v1/chat/completions, /v1/responses, /v1/messages (Anthropic) with tool calling
- web admin console: dashboard, models/sites, keys, proxies, Turnstile, playground, live logs
- Turnstile solvers: solver API / local browser via CDP / manual cookie import
- MIT license, README with screenshots, Dockerfile and Makefile
2026-10-07 15:46:45 +08:00

899 lines
33 KiB
Go

package turnstile
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"testing"
"time"
"github.com/nyoungo/dsfree2api/internal/config"
"github.com/nyoungo/dsfree2api/internal/httpx"
)
// fpExpr dumps an exhaustive JS-environment fingerprint (main world) so
// headless vs headed runs can be diffed field by field.
const fpExpr = `(() => {
if (!location.href.startsWith("https://deepseek.")) return "";
const out = {};
const j = (v) => { try { const s = JSON.stringify(v); return s === undefined ? "undef" : s; } catch (e) { return "ERR"; } };
const n = navigator;
out.nav = {};
["appCodeName","appName","platform","vendor","vendorSub","product","productSub","language","webdriver","hardwareConcurrency","deviceMemory","maxTouchPoints","cookieEnabled","doNotTrack","pdfViewerEnabled","onLine","userAgent"].forEach(function(k){
try { out.nav[k] = j(n[k]); } catch (e) { out.nav[k] = "ERR"; }
});
try { out.nav.languages = j(Array.from(n.languages || [])); } catch (e) {}
try { out.nav.plugins = Array.from(n.plugins || []).map(function(p){ return p.name + "|" + p.filename; }).join(";"); } catch (e) { out.nav.plugins = "ERR"; }
try { out.nav.mimeTypes = Array.from(n.mimeTypes || []).map(function(m){ return m.type + "|" + m.suffixes; }).join(";"); } catch (e) { out.nav.mimeTypes = "ERR"; }
try { out.nav.connection = j(n.connection ? {e:n.connection.effectiveType, rtt:n.connection.rtt, dl:n.connection.downlink} : null); } catch (e) {}
try { out.nav.getBattery = typeof n.getBattery; } catch (e) {}
out.screen = {};
["width","height","availWidth","availHeight","colorDepth","pixelDepth"].forEach(function(k){
try { out.screen[k] = j(screen[k]); } catch (e) {}
});
try { out.screen.orientation = j(screen.orientation && screen.orientation.type); } catch (e) {}
out.win = {
dpr: j(window.devicePixelRatio), iw: j(innerWidth), ih: j(innerHeight), ow: j(outerWidth), oh: j(outerHeight),
sx: j(screenX), sy: j(screenY), chrome: j(typeof window.chrome),
chromeRuntime: j(window.chrome ? typeof window.chrome.runtime : "none"),
notif: (typeof Notification !== "undefined") ? Notification.permission : "none",
histo: j(history.length), ref: j(document.referrer.slice(0,60)),
vis: j(document.visibilityState), isTop: j(window.top === window),
docMode: j(document.documentMode), compat: j(document.compatMode)
};
try { out.intl = j(Intl.DateTimeFormat().resolvedOptions()); } catch (e) { out.intl = "ERR"; }
try { out.tzOffset = j(new Date().getTimezoneOffset()); } catch (e) {}
try {
const c = document.createElement("canvas");
c.width = 32; c.height = 32;
const g = c.getContext("2d");
g.fillStyle = "#f0f"; g.fillRect(0,0,32,32);
g.font = "14px Arial"; g.fillText("fp", 2, 20);
out.canvas = c.toDataURL();
} catch (e) { out.canvas = "ERR"; }
try {
const gc = document.createElement("canvas").getContext("webgl");
const dbg = gc.getExtension("WEBGL_debug_renderer_info");
out.gl = {
vendor: String(gc.getParameter(dbg.UNMASKED_VENDOR_WEBGL)),
rend: String(gc.getParameter(dbg.UNMASKED_RENDERER_WEBGL)),
ver: String(gc.getParameter(gc.VERSION)),
rendStr: String(gc.getParameter(gc.RENDERER)),
maxTex: j(gc.getParameter(gc.MAX_TEXTURE_SIZE)),
maxVB: j(gc.getParameter(gc.MAX_VERTEX_ATTRIBS)),
exts: j((gc.getSupportedExtensions() || []).length)
};
} catch (e) { out.gl = "ERR"; }
try {
const ac = new (window.AudioContext || window.webkitAudioContext)();
out.audio = { sr: j(ac.sampleRate), base: j(ac.baseLatency), out: j(ac.outputLatency) };
ac.close();
} catch (e) { out.audio = "ERR"; }
try { out.fonts = j(document.fonts.size); } catch (e) {}
try { out.voices = j((window.speechSynthesis.getVoices() || []).length); } catch (e) {}
try {
out.mm = {
coarse: matchMedia("(pointer: coarse)").matches,
anyCoarse: matchMedia("(any-pointer: coarse)").matches,
dark: matchMedia("(prefers-color-scheme: dark)").matches,
reduce: matchMedia("(prefers-reduced-motion: reduce)").matches,
hover: matchMedia("(any-hover: hover)").matches
};
} catch (e) { out.mm = "ERR"; }
try {
out.leaks = Object.getOwnPropertyNames(window).filter(function(k){
return /_cdc_|cdc_|webdriver|selenium|puppeteer|phantom|playwright|__nightmare|callPhantom|domAutomation|__selenium/.test(k);
});
} catch (e) { out.leaks = "ERR"; }
try { out.cookie = j(document.cookie.slice(0,80)); } catch (e) {}
try { out.lsLen = j(localStorage.length); } catch (e) { out.lsLen = "ERR"; }
return JSON.stringify(out);
})()`
// netTapJS logs challenge-platform request/response bodies to the console so
// the TRACE hook can print the full rch conversation.
const netTapJS = `(() => {
const ok = u => /cdn-cgi|challenges\.cloudflare/.test(String(u));
const log = (tag, u, extra) => { try { console.log("NET|" + tag + "|" + u + "|" + extra); } catch (e) {} };
try {
const odce = Document.prototype.createElement;
Document.prototype.createElement = function(t) {
const r = odce.apply(this, arguments);
try {
const tt = String(t).toLowerCase();
if (tt === "iframe" || tt === "object" || tt === "embed") {
const src = (typeof arguments[1] === "object" && arguments[1] && arguments[1].src) || "";
log("create" + tt, src, new Error().stack ? String(new Error().stack).split("\n").slice(1,4).join(" <- ") : "");
if (tt === "iframe") {
const d = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, "src");
if (d && d.set) {
Object.defineProperty(r, "src", {
configurable: true,
get: function() { return d.get.call(this); },
set: function(v) { try { log("iframesrc", String(v).slice(0,140), ""); } catch (e) {} d.set.call(this, v); }
});
}
}
}
} catch (e) {}
return r;
};
} catch (e) {}
try {
const oal = window.addEventListener;
window.addEventListener = function(type, fn, opts) {
if (String(type) === "message" && typeof fn === "function") {
const wrapped = function(ev) {
try {
const d = ev.data;
if (d && typeof d === "object") {
log("msg", (d.event || d.type || "obj") + "|" + JSON.stringify(d).slice(0, 600), String(ev.origin || "").slice(0, 80));
} else if (typeof d === "string" && d.length < 400) {
log("msg", d.slice(0, 400), String(ev.origin || "").slice(0, 80));
}
} catch (e) {}
return fn.apply(this, arguments);
};
return oal.call(this, type, wrapped, opts);
}
return oal.apply(this, arguments);
};
} catch (e) {}
try {
const of = window.fetch;
if (of) {
window.fetch = function(input, init) {
const u = (typeof input === "string") ? input : (input && input.url) || "";
if (!ok(u)) return of.apply(this, arguments);
try {
let hdr = "";
if (init && init.headers) { hdr = JSON.stringify(init.headers); }
let body = "";
if (init && init.body) { body = String(init.body).slice(0, 700); }
log("freq", (init && init.method || "GET") + "|" + hdr.slice(0, 400) + "|" + body, u);
} catch (e) {}
const p = of.apply(this, arguments);
try {
p.then(resp => {
const c = resp.clone();
c.text().then(t => log("fresp", resp.status + "|" + t.slice(0, 900)), () => {});
try { log("fhdr", String(c.headers).slice(0, 300), u); } catch (e) {}
}, () => {});
} catch (e) {}
return p;
};
}
} catch (e) {}
try {
const XP = XMLHttpRequest.prototype;
const oopen = XP.open, osend = XP.send;
XP.open = function(m, u) { this.__tm = m; this.__tu = u; return oopen.apply(this, arguments); };
XP.send = function(b) {
if (ok(this.__tu)) {
try {
this.setRequestHeader = function(k, v) {
try { log("xhdr", this.__tu, k + ": " + v); } catch (e) {}
return XMLHttpRequest.prototype.setRequestHeader.apply(this, arguments);
};
} catch (e) {}
try {
this.addEventListener("load", () => {
try {
log("xresp", this.status + "|" + this.getAllResponseHeaders().replace(/\n/g, ";").slice(0, 300), this.__tu);
log("xbody", String(this.responseText).slice(0, 900), this.__tu);
} catch (e) {}
});
log("xreq", this.__tm + "|" + (b ? String(b).slice(0, 700) : ""), this.__tu);
} catch (e) {}
}
return osend.apply(this, arguments);
};
} catch (e) {}
})()`
// domExpr reports the widget/iframe situation in the main document (shadow
// DOM included) so we can tell where the challenge actually executes.
const domExpr = `(() => {
if (!location.href.startsWith("https://deepseek.")) return "";
const out = { winLen: window.length, iframes: [], shadowRoots: [], body: "" };
const walkRoot = (root, depth) => {
if (depth > 6 || !root.querySelectorAll) return;
root.querySelectorAll("iframe").forEach(f => {
let u = f.src || "";
if (!u) { try { u = f.contentWindow.location.href; } catch (e) { u = "no-src-xo"; } }
out.iframes.push(String(u).slice(0, 120));
});
root.querySelectorAll("*").forEach(el => {
if (el.shadowRoot) {
out.shadowRoots.push((el.tagName + (el.id ? "#" + el.id : "")) + ":open");
walkRoot(el.shadowRoot, depth + 1);
} else if (el.attachShadow && Object.keys(el).some(k => k.indexOf("shadow") >= 0)) {
out.shadowRoots.push((el.tagName + (el.id ? "#" + el.id : "")) + ":closed?");
}
});
};
walkRoot(document, 0);
try {
const w = document.getElementById("__ds_ts_widget");
if (w) {
const own = Object.getOwnPropertyNames(w).filter(k => /shadow/i.test(k));
out.widget = (w.shadowRoot ? "open" : (own.length ? own.join(",") : "none"));
} else { out.widget = "no-widget"; }
} catch (e) { out.widget = "err:" + e; }
try { out.body = Array.from(document.body.children).map(e => e.tagName + (e.id ? "#" + e.id : "")).join(","); } catch (e) {}
return JSON.stringify(out);
})()`
func providerSite() *config.Site {
return &config.Site{
Code: "de", BaseURL: "https://deepseek.de",
Language: "de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7",
}
}
func TestSolveTokenProviderManual(t *testing.T) {
solver := New(config.Turnstile{Enabled: true, Provider: "manual", Retries: 1}, httpx.DefaultUserAgent)
_, err := solver.solveToken(context.Background(), providerSite(), "0xdead", solver.coreFor(""))
if err == nil {
t.Fatal("manual provider must not solve")
}
if Retryable(err) {
t.Errorf("manual failure should be terminal, got: %v", err)
}
if !strings.Contains(err.Error(), "manual") {
t.Errorf("error should mention the provider: %v", err)
}
}
func TestSolveTokenProviderBrowserNeedsPath(t *testing.T) {
solver := New(config.Turnstile{Enabled: true, Provider: "browser", Retries: 1}, httpx.DefaultUserAgent)
_, err := solver.solveToken(context.Background(), providerSite(), "0xdead", solver.coreFor(""))
if err == nil || Retryable(err) {
t.Fatalf("empty browser_path should fail fast, got: %v", err)
}
if !strings.Contains(err.Error(), "browser_path") {
t.Errorf("error should mention browser_path: %v", err)
}
solver.SetConfig(config.Turnstile{
Enabled: true, Provider: "browser", Retries: 1,
BrowserPath: filepath.Join(t.TempDir(), "missing.exe"),
})
_, err = solver.solveToken(context.Background(), providerSite(), "0xdead", solver.coreFor(""))
if err == nil || Retryable(err) {
t.Fatalf("missing browser executable should fail fast, got: %v", err)
}
}
func TestSolveTokenProviderUnknown(t *testing.T) {
solver := New(config.Turnstile{Enabled: true, Provider: "captcha", Retries: 1}, httpx.DefaultUserAgent)
_, err := solver.solveToken(context.Background(), providerSite(), "0xdead", solver.coreFor(""))
if err == nil || Retryable(err) {
t.Fatalf("unknown provider should fail fast, got: %v", err)
}
if !strings.Contains(err.Error(), "captcha") {
t.Errorf("error should echo the provider: %v", err)
}
}
// TestBrowserSolveLive drives the real browser against deepseek.de. Opt in:
//
// DSFREE_BROWSER_E2E=1 go test ./internal/turnstile -run Live -v
//
// Optional: DSFREE_BROWSER_E2E_EXE (defaults to Edge, then Chrome) and the
// proxy/site come from ../../config.toml.
func TestBrowserSolveLive(t *testing.T) {
if os.Getenv("DSFREE_BROWSER_E2E") == "" {
t.Skip("set DSFREE_BROWSER_E2E=1 to run the live browser solve")
}
cfg, err := config.Load("../../config.toml")
if err != nil {
t.Fatalf("load config: %v", err)
}
exe := os.Getenv("DSFREE_BROWSER_E2E_EXE")
if exe == "" {
exe = cfg.Turnstile.BrowserPath
}
for _, candidate := range []string{
exe,
`C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`,
`C:\Program Files\Microsoft\Edge\Application\msedge.exe`,
`C:\Program Files\Google\Chrome\Application\chrome.exe`,
} {
if candidate != "" {
if _, err := os.Stat(candidate); err == nil {
exe = candidate
break
}
}
}
if exe == "" {
t.Skip("no browser executable found — set DSFREE_BROWSER_E2E_EXE")
}
ts := cfg.Turnstile
ts.Enabled = true
ts.Provider = config.ProviderBrowser
ts.BrowserPath = exe
ts.Retries = 1
ts.TimeoutSeconds = 30 // faster feedback while diagnosing
ts.BrowserHeadless = os.Getenv("DSFREE_BROWSER_E2E_HEADLESS") != ""
solver := New(ts, cfg.Upstream.UserAgent)
browserDebug = func(format string, args ...any) {
t.Logf(format, args...)
}
defer func() { browserDebug = nil }()
if os.Getenv("DSFREE_BROWSER_E2E_TRACE") != "" {
type diag struct {
c *cdp
sid string
}
var logMu sync.Mutex
finished := false
logf := func(format string, args ...any) {
logMu.Lock()
defer logMu.Unlock()
if finished {
return
}
t.Logf(format, args...)
}
defer func() {
logMu.Lock()
finished = true
logMu.Unlock()
}()
done := make(chan struct{})
defer close(done)
type respRef struct{ id, url, sess string }
pendingResp := make(chan respRef, 16)
type fetchRef struct{ id, url, sess, method string }
pendingFetch := make(chan fetchRef, 32)
rchFrames := make(chan string, 8)
diagCh := make(chan diag, 1)
browserDiagHook = func(c *cdp, s string) {
select {
case diagCh <- diag{c: c, sid: s}:
default:
}
}
defer func() { browserDiagHook = nil }()
browserSetupHook = func(c *cdp, s string) {
cctx, ccancel := context.WithTimeout(context.Background(), 5*time.Second)
c.call(cctx, s, "Page.addScriptToEvaluateOnNewDocument", map[string]any{"source": netTapJS})
_, ferr := c.call(cctx, s, "Fetch.enable", map[string]any{
"patterns": []any{
map[string]any{"urlPattern": "*cdn-cgi/challenge-platform*", "requestStage": "Response"},
},
})
if ferr != nil {
logf("Fetch.enable err=%v", ferr)
}
ccancel()
}
defer func() { browserSetupHook = nil }()
seenCtx := sync.Map{}
cfCtx := make(chan int64, 16)
cdpEventHook = func(session, method string, params json.RawMessage) {
switch method {
case "Network.requestWillBeSent":
var p struct {
Request struct {
URL string `json:"url"`
Method string `json:"method"`
Headers map[string]string `json:"headers"`
} `json:"request"`
Type string `json:"type"`
FrameID string `json:"frameId"`
DocumentURL string `json:"documentURL"`
Initiator struct {
Type string `json:"type"`
} `json:"initiator"`
}
if json.Unmarshal(params, &p) == nil {
ua := p.Request.Headers["User-Agent"]
if strings.Contains(p.Request.URL, "cdn-cgi/challenge-platform") || strings.Contains(ua, "HeadlessChrome") {
keys := make([]string, 0, len(p.Request.Headers))
for k := range p.Request.Headers {
keys = append(keys, k)
}
sort.Strings(keys)
var b strings.Builder
for _, k := range keys {
fmt.Fprintf(&b, "%s=%q ", k, p.Request.Headers[k])
}
logf("CHAL %s %s headlessUA=%v type=%s init=%s frame=%s doc=%s %s",
p.Request.Method, p.Request.URL, strings.Contains(ua, "HeadlessChrome"),
p.Type, p.Initiator.Type, p.FrameID, p.DocumentURL, b.String())
if p.Type == "Document" && p.FrameID != "" && strings.Contains(p.Request.URL, "/rch/") {
select {
case rchFrames <- p.FrameID:
default:
}
}
}
}
case "Network.responseReceived":
var p struct {
RequestID string `json:"requestId"`
Response struct {
URL string `json:"url"`
Status int `json:"status"`
Protocol string `json:"protocol"`
ConnectionID any `json:"connectionId"`
Headers map[string]string `json:"headers"`
SetCookie string `json:"setCookie"`
} `json:"response"`
}
if json.Unmarshal(params, &p) == nil && (strings.Contains(p.Response.URL, "cdn-cgi") || strings.Contains(p.Response.URL, "challenges.cloudflare")) {
logf("RESP %d proto=%s conn=%v sess=%s setCookie=%q %s", p.Response.Status, p.Response.Protocol, p.Response.ConnectionID, session, p.Response.SetCookie, p.Response.URL)
keys := make([]string, 0, len(p.Response.Headers))
for k := range p.Response.Headers {
if !strings.EqualFold(k, "Content-Security-Policy") && !strings.EqualFold(k, "Date") {
keys = append(keys, k)
}
}
sort.Strings(keys)
var b strings.Builder
for _, k := range keys {
v := p.Response.Headers[k]
if len(v) > 200 {
v = v[:200] + "..."
}
fmt.Fprintf(&b, "%s=%q ", k, v)
}
logf("RESPH %s %s", p.Response.URL, b.String())
select {
case pendingResp <- respRef{id: p.RequestID, url: p.Response.URL, sess: session}:
default:
}
}
case "Runtime.executionContextCreated":
var p struct {
ExecutionContextID int64 `json:"executionContextId"`
Origin string `json:"origin"`
}
if json.Unmarshal(params, &p) == nil && strings.Contains(p.Origin, "cloudflare") {
if _, loaded := seenCtx.LoadOrStore(p.ExecutionContextID, true); !loaded {
logf("CFCTX id=%d origin=%s", p.ExecutionContextID, p.Origin)
select {
case cfCtx <- p.ExecutionContextID:
default:
}
}
}
case "Fetch.requestPaused":
var p struct {
RequestID string `json:"requestId"`
Request struct {
URL string `json:"url"`
Method string `json:"method"`
} `json:"request"`
ResponseStatusCode int `json:"responseStatusCode"`
}
if json.Unmarshal(params, &p) == nil && p.RequestID != "" {
select {
case pendingFetch <- fetchRef{id: p.RequestID, url: p.Request.URL, sess: session, method: p.Request.Method}:
default:
logf("FETCH channel full, dropping %s", p.Request.URL)
}
}
case "Runtime.consoleAPICalled":
var p struct {
Type string `json:"type"`
Args []struct {
Value json.RawMessage `json:"value"`
} `json:"args"`
}
if json.Unmarshal(params, &p) == nil && len(p.Args) > 0 {
var s string
if json.Unmarshal(p.Args[0].Value, &s) == nil && strings.HasPrefix(s, "NET|") {
logf("CONSOLE %s", s)
}
}
case "Target.attachedToTarget":
logf("ATTACH params=%s", params)
}
}
defer func() { cdpEventHook = nil }()
type frT struct {
Frame struct {
ID string `json:"id"`
URL string `json:"url"`
} `json:"frame"`
ChildFrames []frT `json:"childFrames"`
}
go func() {
var d diag
select {
case d = <-diagCh:
case <-done:
return
}
probed := map[string]bool{}
logf("MAINSID %s", d.sid)
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-done:
return
case f := <-pendingFetch:
func() {
fctx, fcancel := context.WithTimeout(context.Background(), 5*time.Second)
defer fcancel()
raw, err := d.c.call(fctx, f.sess, "Fetch.getResponseBody", map[string]any{"requestId": f.id})
if err != nil {
logf("FCHBODY %s %s err=%v", f.method, f.url, err)
} else {
var bo struct {
Body string `json:"body"`
Base64 bool `json:"base64Encoded"`
MIMEType string `json:"mimeType"`
}
if json.Unmarshal(raw, &bo) == nil {
body := bo.Body
if bo.Base64 {
body = "(b64)" + body
}
if strings.Contains(f.url, "/rch/") && bo.Body != "" && !probed["rchjs:"+f.url] {
probed["rchjs:"+f.url] = true
n := 0
for k := range probed {
if strings.HasPrefix(k, "rchfile:") {
n++
}
}
probed[fmt.Sprintf("rchfile:%d", n)] = true
dst := filepath.Join(os.TempDir(), fmt.Sprintf("turnstile_rch_%s_%02d.txt", map[bool]string{true: "hl", false: "hd"}[os.Getenv("DSFREE_BROWSER_E2E_HEADLESS") != ""], n))
if err := os.WriteFile(dst, []byte(bo.Body), 0o644); err == nil {
logf("RCHFILE %d bytes -> %s", len(bo.Body), dst)
}
}
logf("FCHBODY mime=%s %s %s %s", bo.MIMEType, f.method, f.url, body[:min(600, len(body))])
}
}
rctx, rcancel := context.WithTimeout(context.Background(), 5*time.Second)
defer rcancel()
if _, err := d.c.call(rctx, f.sess, "Fetch.continueResponse", map[string]any{"requestId": f.id}); err != nil {
if _, err2 := d.c.call(rctx, f.sess, "Fetch.continueRequest", map[string]any{"requestId": f.id}); err2 != nil {
logf("FETCH continue failed id=%s: %v / %v", f.id, err, err2)
}
}
}()
case r := <-pendingResp:
time.Sleep(300 * time.Millisecond)
useSid := r.sess
if useSid == "" {
useSid = d.sid
}
bctx, bcancel := context.WithTimeout(context.Background(), 5*time.Second)
braw, berr := d.c.call(bctx, useSid, "Network.getResponseBody", map[string]any{"requestId": r.id})
bcancel()
if berr != nil {
logf("RCHBODY %s err=%v", r.url, berr)
continue
}
var bo struct {
Body string `json:"body"`
Base64 bool `json:"base64Encoded"`
MIMEType string `json:"mimeType"`
}
if json.Unmarshal(braw, &bo) == nil {
if strings.Contains(r.url, "api.js") && !probed["apijs"] && bo.Body != "" {
probed["apijs"] = true
dst := filepath.Join(os.TempDir(), "turnstile_api_js.txt")
if err := os.WriteFile(dst, []byte(bo.Body), 0o644); err == nil {
logf("APIJS saved %d bytes -> %s", len(bo.Body), dst)
}
}
body := bo.Body
if bo.Base64 {
body = "(b64)" + body[:min(400, len(body))]
}
logf("RCHBODY mime=%s %s %s", bo.MIMEType, r.url, body[:min(900, len(body))])
}
case fid := <-rchFrames:
go func() {
var last string
for i := 0; i < 15; i++ {
select {
case <-done:
return
default:
}
time.Sleep(700 * time.Millisecond)
func() {
wctx, wcancel := context.WithTimeout(context.Background(), 5*time.Second)
defer wcancel()
raw, err := d.c.call(wctx, d.sid, "Page.createIsolatedWorld", map[string]any{
"frameId": fid, "worldName": "dsrchprobe",
})
if err != nil {
if i == 0 {
logf("RCHFRAME %s world err=%v", fid, err)
}
return
}
var w struct {
ExecutionContextID int64 `json:"executionContextId"`
}
if json.Unmarshal(raw, &w) != nil || w.ExecutionContextID == 0 {
return
}
eraw, eerr := d.c.call(wctx, d.sid, "Runtime.evaluate", map[string]any{
"expression": `JSON.stringify({href:(location.href||"").slice(0,95), rs:document.readyState,
focus:document.hasFocus(), vis:document.visibilityState,
inner:[innerWidth,innerHeight], win:[window.outerWidth,window.outerHeight],
wd:String(navigator.webdriver), ua:navigator.userAgent.slice(-24)})`,
"returnByValue": true,
"contextId": w.ExecutionContextID,
})
if eerr != nil {
return
}
var out struct {
Result struct {
Value json.RawMessage `json:"value"`
} `json:"result"`
}
if json.Unmarshal(eraw, &out) == nil {
s := string(out.Result.Value)
if s != last {
last = s
logf("RCHFRAME i=%d %s -> %s", i, fid, s)
}
}
}()
}
}()
case id := <-cfCtx:
cctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
raw, err := d.c.call(cctx, d.sid, "Runtime.evaluate", map[string]any{
"expression": `JSON.stringify({ua:navigator.userAgent, wd:String(navigator.webdriver), focus:document.hasFocus(), sl:screen.height+"x"+screen.availHeight, cw:document.documentElement.clientWidth, href:window.location.href.slice(0,70)})`,
"returnByValue": true,
"contextId": id,
})
cancel()
if err != nil {
logf("CFCTX id=%d evaluate err=%v", id, err)
continue
}
var out struct {
Result struct {
Value json.RawMessage `json:"value"`
} `json:"result"`
}
if json.Unmarshal(raw, &out) == nil {
logf("CFCTX id=%d -> %s", id, out.Result.Value)
}
case <-ticker.C:
func() {
rctx2, rcancel2 := context.WithTimeout(context.Background(), 4*time.Second)
defer rcancel2()
traw, terr := d.c.call(rctx2, "", "Target.getTargets", nil)
if terr != nil {
return
}
var tl struct {
TargetInfos []struct {
TargetID string `json:"targetId"`
URL string `json:"url"`
Type string `json:"type"`
} `json:"targetInfos"`
}
if json.Unmarshal(traw, &tl) != nil {
return
}
for _, ti := range tl.TargetInfos {
if !strings.Contains(ti.URL, "challenges.cloudflare.com") || probed["tg:"+ti.TargetID] {
continue
}
if probed["tga:"+ti.TargetID] {
// already attached+probed once; only retry if still about:blank
}
alog, aerr := d.c.call(rctx2, "", "Target.attachToTarget", map[string]any{
"targetId": ti.TargetID, "flatten": true,
})
if aerr != nil {
probed["tg:"+ti.TargetID] = true
logf("RCHTGT attach err=%v", aerr)
continue
}
var atg struct {
SessionID string `json:"sessionId"`
}
if json.Unmarshal(alog, &atg) != nil || atg.SessionID == "" {
probed["tg:"+ti.TargetID] = true
continue
}
eraw, eerr := d.c.call(rctx2, atg.SessionID, "Runtime.evaluate", map[string]any{
"expression": `JSON.stringify({href:(location.href||"").slice(0,95), rs:document.readyState,
focus:document.hasFocus(), vis:document.visibilityState, hist:history.length,
ref:(document.referrer||"").slice(0,60),
inner:[innerWidth,innerHeight], outer:[outerWidth,outerHeight], dpr:devicePixelRatio,
wd:String(navigator.webdriver), ua:navigator.userAgent,
tz:Intl.DateTimeFormat().resolvedOptions().timeZone,
dsp:String(window.__dsSp),
swd:String((Object.getOwnPropertyDescriptor(Screen.prototype,"width")||{}).get||"none"),
screen:[screen.width,screen.height,screen.availWidth,screen.availHeight],
chrome:typeof window.chrome, plugs:(navigator.plugins||[]).length,
topWin:(function(){try{return window.top===window}catch(e){return "xo"}})(),
leaks:Object.getOwnPropertyNames(window).filter(function(k){return /cdc_|webdriver|selenium|phantom/i.test(k)}).join(",")})`,
"returnByValue": true,
})
if eerr != nil {
logf("RCHTGT eval err=%v", eerr)
continue
}
var out struct {
Result struct {
Value json.RawMessage `json:"value"`
} `json:"result"`
}
if json.Unmarshal(eraw, &out) == nil {
val := string(out.Result.Value)
if !strings.Contains(val, "about:blank") {
probed["tg:"+ti.TargetID] = true
}
logf("RCHTGT type=%s %s -> %s", ti.Type, ti.URL[:min(70, len(ti.URL))], val)
}
}
}()
rctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
raw, err := d.c.call(rctx, d.sid, "Page.getFrameTree", nil)
if err != nil {
cancel()
logf("FRAMETREE err=%v", err)
return
}
cancel()
var ftRes struct {
FrameTree frT `json:"frameTree"`
}
if json.Unmarshal(raw, &ftRes) != nil {
continue
}
if !probed["_log"] {
probed["_log"] = true
var n int
var collect func(f frT)
collect = func(f frT) {
n++
for _, c := range f.ChildFrames {
collect(c)
}
}
collect(ftRes.FrameTree)
logf("FRAMETREE n=%d", n)
}
var walk func(f frT)
walk = func(f frT) {
if f.Frame.URL != "" {
ukey := "u:" + f.Frame.URL
if !probed[ukey] {
probed[ukey] = true
logf("FRAME %s", f.Frame.URL[:min(70, len(f.Frame.URL))])
}
}
if strings.Contains(f.Frame.URL, "challenges.cloudflare") && f.Frame.ID != "" && !probed[f.Frame.ID] {
probed[f.Frame.ID] = true
wctx, wcancel := context.WithTimeout(context.Background(), 5*time.Second)
wraw, werr := d.c.call(wctx, d.sid, "Page.createIsolatedWorld", map[string]any{
"frameId": f.Frame.ID, "worldName": "dsprobe",
})
if werr == nil {
var w struct {
ExecutionContextID int64 `json:"executionContextId"`
}
if json.Unmarshal(wraw, &w) == nil && w.ExecutionContextID != 0 {
eraw, eerr := d.c.call(wctx, d.sid, "Runtime.evaluate", map[string]any{
"expression": `JSON.stringify({ua:navigator.userAgent, wd:String(navigator.webdriver), focus:document.hasFocus(), sl:screen.height+"x"+screen.availHeight, cw:document.documentElement.clientWidth, href:location.href.slice(0,70)})`,
"returnByValue": true,
"contextId": w.ExecutionContextID,
})
if eerr == nil {
var out struct {
Result struct {
Value json.RawMessage `json:"value"`
} `json:"result"`
}
if json.Unmarshal(eraw, &out) == nil {
logf("CFFRAME %s -> %s", f.Frame.URL[:min(60, len(f.Frame.URL))], out.Result.Value)
}
} else {
logf("CFFRAME eval err=%v", eerr)
}
}
} else {
logf("CFFRAME world err=%v", werr)
}
wcancel()
}
for _, c := range f.ChildFrames {
walk(c)
}
}
walk(ftRes.FrameTree)
dctx, dcancel := context.WithTimeout(context.Background(), 5*time.Second)
draw, derr := d.c.call(dctx, d.sid, "Runtime.evaluate", map[string]any{
"expression": domExpr, "returnByValue": true,
})
dcancel()
if derr == nil {
var dout struct {
Result struct {
Value json.RawMessage `json:"value"`
} `json:"result"`
}
if json.Unmarshal(draw, &dout) == nil {
var s string
if json.Unmarshal(dout.Result.Value, &s) == nil && s != "" && !probed["dom:"+s] {
probed["dom:"+s] = true
logf("DOM %s", s)
}
}
}
if !probed["fp"] {
fctx, fcancel := context.WithTimeout(context.Background(), 5*time.Second)
fraw, ferr := d.c.call(fctx, d.sid, "Runtime.evaluate", map[string]any{
"expression": fpExpr, "returnByValue": true,
})
fcancel()
if ferr == nil {
var fout struct {
Result struct {
Value json.RawMessage `json:"value"`
} `json:"result"`
}
if json.Unmarshal(fraw, &fout) == nil {
var s string
if json.Unmarshal(fout.Result.Value, &s) == nil && s != "" {
probed["fp"] = true
logf("FP %s", s)
}
}
}
}
}
}
}()
}
site := cfg.Sites["de"]
c := solver.coreFor(cfg.Proxy.URL)
t.Logf("browser=%s proxy=%s site=%s", exe, cfg.Proxy.URL, site.BaseURL)
ctx := context.Background()
token, err := solver.solveToken(ctx, site, site.SiteKey, c)
if err != nil {
t.Fatalf("solve: %v", err)
}
if len(token) < 20 {
t.Fatalf("token too short (%d chars): %q", len(token), token)
}
t.Logf("token ok: %d chars, prefix %q", len(token), token[:min(12, len(token))])
// the token is only worth something if the site accepts it
sess, err := httpx.NewSession(httpx.Options{ProxyURL: cfg.Proxy.URL, Timeout: 30 * time.Second})
if err != nil {
t.Fatalf("session: %v", err)
}
defer sess.Close()
if err := solver.verifyToken(ctx, sess, site, token); err != nil {
t.Fatalf("verify: %v", err)
}
t.Logf("verify ok: %d cookies for %s", len(sess.Cookies()), site.Code)
for name := range sess.Cookies() {
t.Logf(" cookie: %s", name)
}
}