package lkgateway // Прокси для выхода в интернет (05.10.2026). Во многих организациях сервер ходит в интернет только через // прокси: без него не активировать ключ продукта, не получить обновления, не скачать ИШ и документы НРД. // Прокси из настроек действует на всё, что bj открывает в интернете; адреса локальной сети (шлюз НРД, учёт, // модули, свой кабинет) — напрямую. Не задан — как раньше (переменные окружения службы). import ( "context" "fmt" "net" "net/http" "net/url" "strings" "time" ) // ProxySettings — прокси организации. type ProxySettings struct { Address string `json:"address,omitempty"` // http://proxy.corp.local:3128 User string `json:"user,omitempty"` Password string `json:"password,omitempty"` NoProxy string `json:"no_proxy,omitempty"` // ещё адреса напрямую, через запятую: corp.local, 10.20.0.5 } // url — адрес прокси с логином (nil — прокси не задан). func (p ProxySettings) url() *url.URL { a := strings.TrimSpace(p.Address) if a == "" { return nil } if !strings.Contains(a, "://") { a = "http://" + a } u, err := url.Parse(a) if err != nil || u.Host == "" { return nil } if p.User != "" { u.User = url.UserPassword(p.User, p.Password) } return u } // proxyBypass — адрес локальной сети или из списка «напрямую». func proxyBypass(host, extra string) bool { host = strings.ToLower(strings.TrimSpace(host)) if host == "" || host == "localhost" || !strings.Contains(host, ".") || strings.HasSuffix(host, ".local") { return true } if ip := net.ParseIP(host); ip != nil { return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() } for _, x := range strings.Split(extra, ",") { x = strings.ToLower(strings.TrimPrefix(strings.TrimSpace(x), ".")) if x != "" && (host == x || strings.HasSuffix(host, "."+x)) { return true } } return false } // proxyFunc — прокси из настроек bj (без них — напрямую). func proxyFunc(rc *RuntimeConfig) func(*http.Request) (*url.URL, error) { return func(req *http.Request) (*url.URL, error) { p := rc.Snapshot().Proxy u := p.url() if u == nil || proxyBypass(req.URL.Hostname(), p.NoProxy) { return nil, nil } return u, nil } } // applyProxy — прокси настроек для всех HTTP-запросов bj (сервер лицензий, обновления, сайт НРД). func applyProxy(rc *RuntimeConfig) { if t, ok := http.DefaultTransport.(*http.Transport); ok { env, mine := t.Proxy, proxyFunc(rc) t.Proxy = func(req *http.Request) (*url.URL, error) { if rc.Snapshot().Proxy.url() != nil { return mine(req) } if env != nil { return env(req) } return nil, nil } } if t, ok := noProxyClient.Transport.(*http.Transport); ok { t.Proxy = proxyFunc(rc) // документы НРД: прокси окружения не берём (он может блокировать nsd.ru), прокси bj — да } } func registerProxy(mux *http.ServeMux, rc *RuntimeConfig) { mux.HandleFunc("/admin/setup/proxy", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method", http.StatusMethodNotAllowed) return } p := rc.Snapshot().Proxy p.Address = strings.TrimSpace(r.FormValue("address")) p.User = strings.TrimSpace(r.FormValue("user")) if pw := r.FormValue("password"); pw != "" || p.User == "" { p.Password = pw // пароль в страницу не выводится: пусто — прежний } p.NoProxy = strings.TrimSpace(r.FormValue("no_proxy")) if p.Address != "" && p.url() == nil { setupFlash(w, r, "Прокси: адрес вида http://proxy.example.ru:3128") return } if err := rc.UpdateProxy(p); err != nil { setupFlash(w, r, "Прокси: "+err.Error()) return } if p.Address == "" { setupFlash(w, r, "Прокси выключен: bj выходит в интернет напрямую") return } // проверка: сервер лицензий через прокси ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) defer cancel() req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://lic.zetit.ru/healthz", nil) resp, err := http.DefaultClient.Do(req) if err != nil { setupFlash(w, r, "Прокси сохранён, но сервер лицензий через него недоступен: "+err.Error()) return } resp.Body.Close() setupFlash(w, r, fmt.Sprintf("Прокси сохранён: сервер лицензий через него отвечает (HTTP %d)", resp.StatusCode)) }) }