From 56dfc9aedd0aa5c6cacdc36c4cf4c62dd40eb9f3 Mon Sep 17 00:00:00 2001 From: samosvalishe Date: Mon, 21 Sep 2026 13:39:06 +0300 Subject: [PATCH] =?UTF-8?q?refactor(logx):=20=D0=BE=D0=B1=D1=89=D0=B8?= =?UTF-8?q?=D0=B9=20Holder=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20=D0=BF?= =?UTF-8?q?=D0=B0=D0=BA=D0=B5=D1=82=D0=BD=D1=8B=D1=85=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B=D1=85=20Log?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/client/main.go | 1 + internal/client/dnsdial/doh.go | 26 +++++----- internal/logx/logx.go | 23 ++++++++- .../provider/vk/internal/captcha/captcha.go | 10 ++-- .../provider/vk/internal/captcha/errors.go | 8 ++-- .../vk/internal/captcha/manual/manual.go | 47 ++++++++++--------- .../provider/vk/internal/captcha/slider.go | 4 +- .../vk/internal/vkauth/auto_solver.go | 2 +- internal/provider/vk/vk.go | 2 +- internal/sub/sub.go | 10 +++- mobile/api.go | 1 + 11 files changed, 86 insertions(+), 48 deletions(-) diff --git a/cmd/client/main.go b/cmd/client/main.go index a49f28f..bb1090f 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -29,6 +29,7 @@ func main() { // Резолв подписки до парсинга даёт обязательный peer для валидации. if subURL := config.PeekSubURL(args); subURL != "" { + sub.SetLogger(logx.New(false)) s, ferr := sub.Fetch(context.Background(), subURL) if ferr != nil { log.Fatalf("failed to fetch subscription: %v", ferr) diff --git a/internal/client/dnsdial/doh.go b/internal/client/dnsdial/doh.go index 2393ede..3850e5f 100644 --- a/internal/client/dnsdial/doh.go +++ b/internal/client/dnsdial/doh.go @@ -25,9 +25,11 @@ import ( _ "golang.org/x/crypto/x509roots/fallback" ) -var Log logx.Logger = logx.Nop() +var logHolder logx.Holder -func SetLogger(l logx.Logger) { Log = logx.OrNop(l) } +func SetLogger(l logx.Logger) { logHolder.Set(l) } + +func Log() logx.Logger { return logHolder.Get() } const ( dohQueryTimeout = 6 * time.Second @@ -161,7 +163,7 @@ func (r *DohResolver) forwardRaw(ctx context.Context, query []byte) ([]byte, err body, err := r.postWire(epCtx, ep, query) cancel() if err != nil { - Log.Warnf("[DoH] %s: %v", ep.Hostname, err) + Log().Warnf("[DoH] %s: %v", ep.Hostname, err) lastErr = err continue } @@ -238,7 +240,7 @@ func startDohForwarder(r *DohResolver) (_ *dohForwarder, err error) { udpAddr: udpConn.LocalAddr().String(), tcpAddr: tcpLn.Addr().String(), } - Log.Infof("[DoH] forwarder listening udp=%s tcp=%s", fwd.udpAddr, fwd.tcpAddr) + Log().Infof("[DoH] forwarder listening udp=%s tcp=%s", fwd.udpAddr, fwd.tcpAddr) go fwd.serveUDP(udpConn, r) go fwd.serveTCP(tcpLn, r) @@ -251,7 +253,7 @@ func (*dohForwarder) serveUDP(conn *net.UDPConn, r *DohResolver) { for { n, client, err := conn.ReadFromUDP(buf) if err != nil { - Log.Errorf("[DoH] udp read: %v", err) + Log().Errorf("[DoH] udp read: %v", err) return } query := append([]byte(nil), buf[:n]...) @@ -260,11 +262,11 @@ func (*dohForwarder) serveUDP(conn *net.UDPConn, r *DohResolver) { defer cancel() resp, err := r.forwardRaw(ctx, q) if err != nil { - Log.Warnf("[DoH] udp forward failed: %v", err) + Log().Warnf("[DoH] udp forward failed: %v", err) return } if _, err := conn.WriteToUDP(resp, c); err != nil { - Log.Warnf("[DoH] udp write: %v", err) + Log().Warnf("[DoH] udp write: %v", err) } }(query, client) } @@ -275,7 +277,7 @@ func (*dohForwarder) serveTCP(ln *net.TCPListener, r *DohResolver) { for { conn, err := ln.Accept() if err != nil { - Log.Errorf("[DoH] tcp accept: %v", err) + Log().Errorf("[DoH] tcp accept: %v", err) return } go handleDohForwarderTCP(conn, r) @@ -303,11 +305,11 @@ func handleDohForwarderTCP(conn net.Conn, r *DohResolver) { resp, err := r.forwardRaw(ctx, query) cancel() if err != nil { - Log.Warnf("[DoH] tcp forward failed: %v", err) + Log().Warnf("[DoH] tcp forward failed: %v", err) return } if len(resp) > 0xFFFF { - Log.Warnf("[DoH] response too large for TCP framing: %d", len(resp)) + Log().Warnf("[DoH] response too large for TCP framing: %d", len(resp)) return } out := make([]byte, 2+len(resp)) @@ -432,9 +434,9 @@ func autoDial(r *DohResolver) dialFunc { return func(ctx context.Context, network, addr string) (net.Conn, error) { probed.Do(func() { if udpProbe(autoUDPBudget) { - Log.Infof("[DNS] UDP/53 probe OK, using UDP") + Log().Infof("[DNS] UDP/53 probe OK, using UDP") } else { - Log.Warnf("[DNS] UDP/53 unreachable; sticky-switching to DoH") + Log().Warnf("[DNS] UDP/53 unreachable; sticky-switching to DoH") useDoH.Store(true) } }) diff --git a/internal/logx/logx.go b/internal/logx/logx.go index 3e744a0..82bb460 100644 --- a/internal/logx/logx.go +++ b/internal/logx/logx.go @@ -1,7 +1,10 @@ // Package logx предоставляет уровневый логгер поверх stdlib log. package logx -import "log" +import ( + "log" + "sync/atomic" +) // Logger - интерфейс уровневого логирования. type Logger interface { @@ -48,3 +51,21 @@ func (nopLogger) Infof(string, ...any) {} func (nopLogger) Warnf(string, ...any) {} func (nopLogger) Errorf(string, ...any) {} func (nopLogger) DebugEnabled() bool { return false } + +// Holder хранит пакетный логгер, подменяемый на лету: ядро перезапускается в том же +// процессе (mobile), поэтому Set гонится с пишущими горутинами. +type Holder struct { + ptr atomic.Pointer[Logger] +} + +func (h *Holder) Set(l Logger) { + v := OrNop(l) + h.ptr.Store(&v) +} + +func (h *Holder) Get() Logger { + if p := h.ptr.Load(); p != nil { + return *p + } + return Nop() +} diff --git a/internal/provider/vk/internal/captcha/captcha.go b/internal/provider/vk/internal/captcha/captcha.go index 3446836..264a2ea 100644 --- a/internal/provider/vk/internal/captcha/captcha.go +++ b/internal/provider/vk/internal/captcha/captcha.go @@ -23,9 +23,11 @@ import ( "github.com/samosvalishe/free-turn-proxy/internal/randx" ) -var Log logx.Logger = logx.Nop() +var logHolder logx.Holder -func SetLogger(l logx.Logger) { Log = logx.OrNop(l) } +func SetLogger(l logx.Logger) { logHolder.Set(l) } + +func Log() logx.Logger { return logHolder.Get() } const ( captchaAPIVersion = "5.131" @@ -119,7 +121,7 @@ func (s *captchaSession) logger() logx.Logger { if s.log != nil { return s.log } - return Log + return Log() } // Solve запускает авторешение captcha против VK captchaNotRobot API. @@ -441,7 +443,7 @@ func parseCaptchaDebugInfo(html string) string { return "" } if len(found) > 1 { - Log.Warnf("[Captcha] window.vk holds %d uuid values, debug_info may be the wrong one", len(found)) + Log().Warnf("[Captcha] window.vk holds %d uuid values, debug_info may be the wrong one", len(found)) } return found[0][1] } diff --git a/internal/provider/vk/internal/captcha/errors.go b/internal/provider/vk/internal/captcha/errors.go index f0976d0..d2ecaf4 100644 --- a/internal/provider/vk/internal/captcha/errors.go +++ b/internal/provider/vk/internal/captcha/errors.go @@ -23,14 +23,14 @@ type Error struct { func ParseError(errData map[string]any) *Error { codeFloat, ok := errData["error_code"].(float64) if !ok { - Log.Warnf("[Captcha] missing error_code in error data") + Log().Warnf("[Captcha] missing error_code in error data") return nil } code := int(codeFloat) redirectURI, ok := errData["redirect_uri"].(string) if !ok { - Log.Warnf("[Captcha] missing redirect_uri in error data") + Log().Warnf("[Captcha] missing redirect_uri in error data") return nil } @@ -43,7 +43,7 @@ func ParseError(errData map[string]any) *Error { errorMsg, ok := errData["error_msg"].(string) if !ok { - Log.Warnf("[Captcha] missing error_msg in error data") + Log().Warnf("[Captcha] missing error_msg in error data") return nil } @@ -52,7 +52,7 @@ func ParseError(errData map[string]any) *Error { if parsed, err := neturl.Parse(redirectURI); err == nil { sessionToken = parsed.Query().Get("session_token") } else { - Log.Warnf("[Captcha] failed to parse redirect_uri: %v", err) + Log().Warnf("[Captcha] failed to parse redirect_uri: %v", err) return nil } } diff --git a/internal/provider/vk/internal/captcha/manual/manual.go b/internal/provider/vk/internal/captcha/manual/manual.go index 5427539..35efd12 100644 --- a/internal/provider/vk/internal/captcha/manual/manual.go +++ b/internal/provider/vk/internal/captcha/manual/manual.go @@ -17,6 +17,7 @@ import ( "regexp" "runtime" "strings" + "sync/atomic" "time" "github.com/samosvalishe/free-turn-proxy/internal/client/ish" @@ -26,12 +27,16 @@ import ( "github.com/samosvalishe/free-turn-proxy/internal/provider/vk/internal/personanet" ) -// Debug включает логирование проксируемого браузерного трафика. -var Debug bool +// debugFlag включает логирование проксируемого браузерного трафика. +var debugFlag atomic.Bool -var Log logx.Logger = logx.Nop() +func SetDebug(v bool) { debugFlag.Store(v) } -func SetLogger(l logx.Logger) { Log = logx.OrNop(l) } +var logHolder logx.Holder + +func SetLogger(l logx.Logger) { logHolder.Set(l) } + +func Log() logx.Logger { return logHolder.Get() } const captchaListenPort = "8765" @@ -343,12 +348,12 @@ func startCaptchaServer(srv *http.Server, logPrefix string) error { listening = true wrappedListener, err := ish.WrapListener(listener) if err != nil { - Log.Warnf("%s: failed to wrap listener for iSH: %v", logPrefix, err) + Log().Warnf("%s: failed to wrap listener for iSH: %v", logPrefix, err) wrappedListener = listener } go func(listener net.Listener) { if err := srv.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { - Log.Errorf("%s: %s", logPrefix, err) + Log().Errorf("%s: %s", logPrefix, err) } }(wrappedListener) } @@ -375,7 +380,7 @@ func runCaptchaServerAndWait(ctx context.Context, handler http.Handler, captchaU shutCtx, shutCancel := context.WithTimeout(context.Background(), 3*time.Second) defer shutCancel() if err := srv.Shutdown(shutCtx); err != nil { - Log.Warnf("%s: shutdown warning: %v", logPrefix, err) + Log().Warnf("%s: shutdown warning: %v", logPrefix, err) } }() @@ -435,18 +440,18 @@ func (t *loggingTransport) RoundTrip(req *http.Request) (*http.Response, error) if isCaptchaRequest { b, err := io.ReadAll(req.Body) if err != nil { - Log.Warnf("[Captcha Proxy] failed to read request body: %v", err) + Log().Warnf("[Captcha Proxy] failed to read request body: %v", err) b = nil } req.Body = io.NopCloser(bytes.NewReader(b)) - if Debug { - Log.Debugf("[Captcha Proxy] real browser sent %s data: %s", req.URL.Path, string(b)) + if debugFlag.Load() { + Log().Debugf("[Captcha Proxy] real browser sent %s data: %s", req.URL.Path, string(b)) if env := browserPowEnvelope(b); env != "" { - Log.Debugf("[Captcha Proxy] real browser pow: %s", env) + Log().Debugf("[Captcha Proxy] real browser pow: %s", env) } for k, v := range req.Header { - Log.Debugf("[Captcha Proxy] header (%s): %s = %s", req.URL.Path, k, strings.Join(v, ", ")) + Log().Debugf("[Captcha Proxy] header (%s): %s = %s", req.URL.Path, k, strings.Join(v, ", ")) } } } @@ -456,11 +461,11 @@ func (t *loggingTransport) RoundTrip(req *http.Request) (*http.Response, error) // Весь трафик виджета, а не только check/componentDone: иначе не видно, зовёт // ли живой браузер то, чего не зовём мы, и с какими паузами. if err != nil { - Log.Debugf("[Captcha Proxy] http %s %s failed t=%s after=%s %s err=%v", + Log().Debugf("[Captcha Proxy] http %s %s failed t=%s after=%s %s err=%v", req.Method, captcha.SafeURL(req.URL.String()), t.elapsed(), time.Since(start).Truncate(time.Millisecond), navSummary(req), err) return nil, err } - Log.Debugf("[Captcha Proxy] http %s %s status=%d t=%s after=%s %s", + Log().Debugf("[Captcha Proxy] http %s %s status=%d t=%s after=%s %s", req.Method, captcha.SafeURL(req.URL.String()), resp.StatusCode, t.elapsed(), time.Since(start).Truncate(time.Millisecond), navSummary(req)) return resp, nil } @@ -509,7 +514,7 @@ func solveViaProxy(ctx context.Context, redirectURI string, dialer net.Dialer, p rewriteProxyRequest(req.Out, targetURL) }, ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { - Log.Errorf("[Captcha Proxy] %s %s: %v", r.Method, r.URL.String(), err) + Log().Errorf("[Captcha Proxy] %s %s: %v", r.Method, r.URL.String(), err) http.Error(w, "ошибка прокси капчи, детали в консоли клиента", http.StatusBadGateway) }, ModifyResponse: func(res *http.Response) error { @@ -528,8 +533,8 @@ func solveViaProxy(ctx context.Context, redirectURI string, dialer net.Dialer, p contentType := res.Header.Get("Content-Type") contentEncoding := res.Header.Get("Content-Encoding") - if Debug { - Log.Debugf("[Captcha Proxy] %s %d | Content-Type: %q, Encoding: %q", res.Request.Method, res.StatusCode, contentType, contentEncoding) + if debugFlag.Load() { + Log().Debugf("[Captcha Proxy] %s %d | Content-Type: %q, Encoding: %q", res.Request.Method, res.StatusCode, contentType, contentEncoding) } shouldInspectBody := strings.Contains(contentType, "text/html") || @@ -547,7 +552,7 @@ func solveViaProxy(ctx context.Context, redirectURI string, dialer net.Dialer, p reader = gzReader defer func() { if err := gzReader.Close(); err != nil { - Log.Warnf("[Captcha Proxy] close gzip reader: %v", err) + Log().Warnf("[Captcha Proxy] close gzip reader: %v", err) } }() } @@ -599,10 +604,10 @@ func solveViaProxy(ctx context.Context, redirectURI string, dialer net.Dialer, p mux.HandleFunc("/local-captcha-result", func(w http.ResponseWriter, r *http.Request) { token := r.FormValue("token") if token != "" { - Log.Infof("[Captcha] received success token from browser (%d bytes)", len(token)) + Log().Infof("[Captcha] received success token from browser (%d bytes)", len(token)) notifyKey(keyCh, token) } else { - Log.Warnf("[Captcha] received empty token from browser") + Log().Warnf("[Captcha] received empty token from browser") } w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Content-Type", "text/plain") @@ -665,7 +670,7 @@ func solveViaProxy(ctx context.Context, redirectURI string, dialer net.Dialer, p }) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - Log.Debugf("[Captcha Proxy] HTTP %s %s", r.Method, r.URL.Path) + Log().Debugf("[Captcha Proxy] HTTP %s %s", r.Method, r.URL.Path) if r.URL.Path == "/" && targetURL.Path != "" && targetURL.Path != "/" && r.URL.RawQuery == "" { // не логируем полный redirect URL - шум в консоли http.Redirect(w, r, localCaptchaURLForTarget(targetURL), http.StatusTemporaryRedirect) diff --git a/internal/provider/vk/internal/captcha/slider.go b/internal/provider/vk/internal/captcha/slider.go index ae0b9bd..2a13de4 100644 --- a/internal/provider/vk/internal/captcha/slider.go +++ b/internal/provider/vk/internal/captcha/slider.go @@ -176,9 +176,9 @@ func splitSliderSteps(steps []int) (int, []int, int, error) { // Штатный формат: [size, ...пары свапов, attempts]. attempts = tail[len(tail)-1] tail = tail[:len(tail)-1] - Log.Debugf("[Captcha] slider attempts from payload=%d", attempts) + Log().Debugf("[Captcha] slider attempts from payload=%d", attempts) } else { - Log.Debugf("[Captcha] slider payload without attempts counter; default=%d", attempts) + Log().Debugf("[Captcha] slider payload without attempts counter; default=%d", attempts) } if attempts <= 0 { attempts = 4 diff --git a/internal/provider/vk/internal/vkauth/auto_solver.go b/internal/provider/vk/internal/vkauth/auto_solver.go index d49929d..78f645b 100644 --- a/internal/provider/vk/internal/vkauth/auto_solver.go +++ b/internal/provider/vk/internal/vkauth/auto_solver.go @@ -17,7 +17,7 @@ func (*Client) defaultAutoSolve( client tlsclient.HttpClient, profile browserprofile.Profile, ) (string, error) { - log := captcha.Log + log := captcha.Log() log.Infof("[STREAM %d] [Captcha] Solving captcha (platform=%s)...", streamID, profile.Platform) if captchaErr.SessionToken == "" { diff --git a/internal/provider/vk/vk.go b/internal/provider/vk/vk.go index 3e17b30..b44f1df 100644 --- a/internal/provider/vk/vk.go +++ b/internal/provider/vk/vk.go @@ -43,7 +43,7 @@ func New(cfg Config, solver ManualSolverFunc) (*Provider, error) { } captcha.SetLogger(cfg.Log) manualcaptcha.SetLogger(cfg.Log) - manualcaptcha.Debug = cfg.Debug + manualcaptcha.SetDebug(cfg.Debug) auth := vkauth.New(vkauth.Config{ Credentials: cfg.Credentials, Dialer: cfg.Dialer, diff --git a/internal/sub/sub.go b/internal/sub/sub.go index 5477a92..652e1b3 100644 --- a/internal/sub/sub.go +++ b/internal/sub/sub.go @@ -5,14 +5,20 @@ import ( "context" "fmt" "io" - "log" "net/http" "strings" "time" + "github.com/samosvalishe/free-turn-proxy/internal/logx" "github.com/samosvalishe/free-turn-proxy/internal/uri" ) +var logHolder logx.Holder + +func SetLogger(l logx.Logger) { logHolder.Set(l) } + +func log() logx.Logger { return logHolder.Get() } + // Sub представляет структуру подписки на серверы. type Sub struct { Name string @@ -132,7 +138,7 @@ func Parse(r io.Reader) (*Sub, error) { s.Nodes = append(s.Nodes, node) lastNode = &s.Nodes[len(s.Nodes)-1] } else { - log.Printf("warning: skipped invalid freeturn URI in subscription: %v", err) + log().Warnf("[Sub] skipped invalid freeturn URI: %v", err) } continue } diff --git a/mobile/api.go b/mobile/api.go index 56898ef..5460005 100644 --- a/mobile/api.go +++ b/mobile/api.go @@ -250,6 +250,7 @@ func startLocked(configJSON string, tunFD int, withTunnel bool) error { // Резолв подписки до парсинга даёт обязательный peer. overlayURI := "" if subURL := config.PeekSubURLJSON(raw); subURL != "" { + sub.SetLogger(coreLog()) s, err := sub.Fetch(context.Background(), subURL) if err != nil { return fmt.Errorf("failed to fetch subscription: %w", err)