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