10305a095e
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
132 lines
4.9 KiB
Go
132 lines
4.9 KiB
Go
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))
|
|
})
|
|
}
|