diff --git a/internal/provider/vk/internal/captcha/errors.go b/internal/provider/vk/internal/captcha/errors.go index 8825fc9..f0976d0 100644 --- a/internal/provider/vk/internal/captcha/errors.go +++ b/internal/provider/vk/internal/captcha/errors.go @@ -11,7 +11,6 @@ type Error struct { ErrorCode int ErrorMsg string CaptchaSid string - CaptchaImg string RedirectURI string IsSoundCaptchaAvailable bool SessionToken string @@ -42,8 +41,6 @@ func ParseError(errData map[string]any) *Error { } } - captchaImg, _ := errData["captcha_img"].(string) - errorMsg, ok := errData["error_msg"].(string) if !ok { Log.Warnf("[Captcha] missing error_msg in error data") @@ -88,7 +85,6 @@ func ParseError(errData map[string]any) *Error { ErrorCode: code, ErrorMsg: errorMsg, CaptchaSid: captchaSid, - CaptchaImg: captchaImg, RedirectURI: redirectURI, IsSoundCaptchaAvailable: isSound, SessionToken: sessionToken, diff --git a/internal/provider/vk/internal/captcha/manual/inject.js b/internal/provider/vk/internal/captcha/manual/inject.js new file mode 100644 index 0000000..abff877 --- /dev/null +++ b/internal/provider/vk/internal/captcha/manual/inject.js @@ -0,0 +1,154 @@ +(() => { + 'use strict'; + + const { local: LOCAL, upstream: UPSTREAM } = window.__ftpCaptcha ?? {}; + const PROXY = '/generic_proxy?proxy_url='; + const RESULT = '/local-captcha-result'; + const CHECK = 'captchaNotRobot.check'; + + function rewriteUrl(url) { + if (!url || typeof url !== 'string') return url; + if (url.startsWith(LOCAL)) return url; + if (url.startsWith(UPSTREAM)) return LOCAL + url.slice(UPSTREAM.length); + if (url.startsWith('//')) return PROXY + encodeURIComponent(location.protocol + url); + if (url.startsWith('http://') || url.startsWith('https://')) return PROXY + encodeURIComponent(url); + return url; + } + + function rewriteAttr(el, attr) { + const value = el.getAttribute?.(attr); + if (!value) return; + const rewritten = rewriteUrl(value); + if (rewritten !== value) el.setAttribute(attr, rewritten); + } + + function rewriteTree(root) { + if (!root.querySelectorAll) return; + for (const attr of ['href', 'src', 'action']) { + root.querySelectorAll(`[${attr}]`).forEach((el) => rewriteAttr(el, attr)); + } + } + + const extractToken = (data) => data?.response?.success_token; + + function sendToken(token) { + if (!token) return; + const body = `token=${encodeURIComponent(token)}`; + + // sendBeacon переживает уход со страницы (mobile Safari) + if (navigator.sendBeacon?.(RESULT, new Blob([body], { type: 'application/x-www-form-urlencoded' }))) { + showDone(); + return; + } + + fetch(RESULT, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body, + }).then(showDone).catch(() => { + const form = document.createElement('form'); + form.method = 'POST'; + form.action = RESULT; + const input = document.createElement('input'); + input.type = 'hidden'; + input.name = 'token'; + input.value = token; + form.append(input); + document.body.append(form); + form.submit(); + }); + } + + function showPending() { + document.body.style.background = '#000'; + const banner = document.createElement('div'); + banner.textContent = 'free turn proxy - captcha'; + banner.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:99999;padding:12px;text-align:center;font:13.5px ui-monospace,monospace;color:#e1e1e1;background:#000'; + document.body.append(banner); + } + + function showDone() { + document.body.style.background = '#000'; + document.body.innerHTML = + '
' + + '
free turn proxy
' + + '
gg
'; + setTimeout(() => window.close(), 1000); + } + + const xhrOpen = XMLHttpRequest.prototype.open; + XMLHttpRequest.prototype.open = function (...args) { + if (typeof args[1] === 'string') { + this._origUrl = args[1]; + args[1] = rewriteUrl(args[1]); + } + return xhrOpen.apply(this, args); + }; + + const xhrSend = XMLHttpRequest.prototype.send; + XMLHttpRequest.prototype.send = function (...args) { + if (this._origUrl?.includes(CHECK)) { + this.addEventListener('load', () => { + try { sendToken(extractToken(JSON.parse(this.responseText))); } catch {} + }); + } + return xhrSend.apply(this, args); + }; + + const origFetch = window.fetch; + window.fetch = function (...args) { + const urlStr = typeof args[0] === 'object' ? args[0]?.url : args[0]; + if (typeof args[0] === 'string') args[0] = rewriteUrl(args[0]); + + const promise = origFetch.apply(this, args); + if (typeof urlStr === 'string' && urlStr.includes(CHECK)) { + promise + .then((res) => res.clone().json()) + .then((data) => sendToken(extractToken(data))) + .catch(() => {}); + } + return promise; + }; + + document.addEventListener('submit', (event) => { + if (event.target?.action) event.target.action = rewriteUrl(event.target.action); + }, true); + + document.addEventListener('click', (event) => { + const link = event.target?.closest?.('a[href]'); + if (link?.href) link.href = rewriteUrl(link.href); + }, true); + + const formSubmit = HTMLFormElement.prototype.submit; + HTMLFormElement.prototype.submit = function (...args) { + if (this.action) this.action = rewriteUrl(this.action); + return formSubmit.apply(this, args); + }; + + const windowOpen = window.open; + window.open = function (...args) { + if (typeof args[0] === 'string') args[0] = rewriteUrl(args[0]); + return windowOpen.apply(this, args); + }; + + rewriteTree(document); + new MutationObserver((mutations) => { + for (const { type, target, attributeName, addedNodes } of mutations) { + if (type === 'attributes') { + rewriteAttr(target, attributeName); + continue; + } + for (const node of addedNodes) { + if (node.nodeType === 1) rewriteTree(node); + } + } + }).observe(document.documentElement, { + subtree: true, + childList: true, + attributes: true, + attributeFilter: ['href', 'src', 'action'], + }); + + if (document.body) showPending(); + else document.addEventListener('DOMContentLoaded', showPending); +})(); diff --git a/internal/provider/vk/internal/captcha/manual/manual.go b/internal/provider/vk/internal/captcha/manual/manual.go index 78e53dd..05912ab 100644 --- a/internal/provider/vk/internal/captcha/manual/manual.go +++ b/internal/provider/vk/internal/captcha/manual/manual.go @@ -9,10 +9,10 @@ import ( "bytes" "compress/gzip" "context" + _ "embed" "encoding/json" "errors" "fmt" - "html" "io" "net" "net/http" @@ -42,6 +42,16 @@ func SetLogger(l logx.Logger) { Log = logx.OrNop(l) } const captchaListenPort = "8765" +//go:embed inject.js +var injectJS string + +// injectScriptTag: конфиг отдельной строкой (json.Marshal экранирует, включая "<"), +// сам inject.js статичный, без плейсхолдеров. +func injectScriptTag(localOrigin, upstreamOrigin string) string { + cfg, _ := json.Marshal(map[string]string{"local": localOrigin, "upstream": upstreamOrigin}) + return "\n\n\n" +} + type browserCommand struct { name string args []string @@ -296,199 +306,7 @@ func rewriteCaptchaHTML(html string, targetURL *neturl.URL) string { // сразу при парсинге HTML - раньше любых инжектированных JS-перехватов. html = rewriteHTMLAttrsServerSide(html, targetURL) - script := fmt.Sprintf(` - -`, localOrigin, upstreamOrigin) + script := injectScriptTag(localOrigin, upstreamOrigin) // Шаг 3: инжектируем клиентский скрипт как можно раньше - после , // чтобы XHR/fetch-перехваты были активны до любого inline