package proxymap import ( "crypto/tls" "io/ioutil" "net" "net/http" "net/http/httputil" "net/url" "os" "path/filepath" "strings" "time" "acme-reverseproxy/nfs" "github.com/sirupsen/logrus" ) // stripHostPort возвращает только имя хоста, если в строке был порт. func stripHostPort(host string) string { h, _, err := net.SplitHostPort(host) if err != nil { return host } return h } // tlsServerName задаёт SNI для исходящего TLS к бэкенду. // При подключении по IP многие серверы отклоняют SNI с IP (tls: unrecognized name); // в этом случае используем публичное имя из ключа маппинга — как правило, оно совпадает с CN/SAN сертификата бэкенда. func tlsServerName(frontendKey string, u *url.URL) string { backend := u.Hostname() if ip := net.ParseIP(backend); ip != nil { return stripHostPort(frontendKey) } return backend } func ToReverseProxyMap(m map[string]string) (ReverseProxyMap, error) { rpm := ReverseProxyMap{} for k, v := range m { rpURL, err := url.Parse(v) if err != nil { return nil, err } proxy := httputil.NewSingleHostReverseProxy(rpURL) // Настройка транспорта для поддержки HTTP и HTTPS бэкендов if rpURL.Scheme == "https" { sn := tlsServerName(k, rpURL) logrus.Infof("Configuring HTTPS transport for backend: %s (TLS ServerName/SNI: %s)", rpURL.String(), sn) // Создаем транспорт с поддержкой TLS для HTTPS бэкендов transport := &http.Transport{ Proxy: http.ProxyFromEnvironment, TLSClientConfig: &tls.Config{ ServerName: sn, InsecureSkipVerify: false, }, // Настройки подключения MaxIdleConns: 100, MaxIdleConnsPerHost: 100, IdleConnTimeout: 90 * time.Second, DisableCompression: true, ForceAttemptHTTP2: true, } proxy.Transport = transport } else { logrus.Debugf("Configuring HTTP transport for backend: %s", rpURL.String()) // Для HTTP используем стандартный транспорт transport := &http.Transport{ Proxy: http.ProxyFromEnvironment, // Настройки подключения MaxIdleConns: 100, MaxIdleConnsPerHost: 100, IdleConnTimeout: 90 * time.Second, } proxy.Transport = transport } // Настройка директив для правильной работы прокси originalDirector := proxy.Director proxy.Director = func(req *http.Request) { originalDirector(req) // httputil.NewSingleHostReverseProxy уже правильно устанавливает Host и URL // Логируем проксирование logrus.Infof("Proxying request: %s %s -> %s%s (Request Host: %s, Target: %s, Scheme: %s)", req.Method, req.URL.Path, rpURL.Scheme, req.URL.Path, req.Host, rpURL.Host, rpURL.Scheme) } // Обработка ошибок проксирования proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { logrus.Errorf("Reverse proxy error for %s -> %s: %v", r.Host, rpURL.String(), err) http.Error(w, "Bad Gateway", http.StatusBadGateway) } // Логирование успешных ответов originalModifyResponse := proxy.ModifyResponse proxy.ModifyResponse = func(resp *http.Response) error { if originalModifyResponse != nil { if err := originalModifyResponse(resp); err != nil { return err } } logrus.Debugf("Proxy response: %s %s -> status %d", resp.Request.Method, resp.Request.URL.Path, resp.StatusCode) return nil } rpm[k] = proxy logrus.Infof("Configured reverse proxy: %s -> %s (scheme: %s)", k, rpURL.String(), rpURL.Scheme) } return rpm, nil } type ReverseProxyMap map[string]http.Handler func NewReverseProxiesHandler(rpm ReverseProxyMap) ReverseProxiesHandler { return &reverseProxiesHandler{ Map: rpm, NotFoundHandler: http.NotFoundHandler(), WellKnownDir: "/tmp/.well-known", } } func NewReverseProxiesHandlerWithWellKnown(rpm ReverseProxyMap, wellKnownDir string) ReverseProxiesHandler { return &reverseProxiesHandler{ Map: rpm, NotFoundHandler: http.NotFoundHandler(), WellKnownDir: wellKnownDir, } } type ReverseProxiesHandler interface { http.Handler } type reverseProxiesHandler struct { Map ReverseProxyMap NotFoundHandler http.Handler WellKnownDir string } func (rph reverseProxiesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { logrus.Infof("Incoming request: %s %s from %s (Host: %s)", r.Method, r.URL.Path, r.RemoteAddr, r.Host) if strings.HasPrefix(r.URL.Path, "/.well-known/acme-wellknown/") { logrus.Infof("Handling .well-known/acme-wellknown request: %s", r.URL.Path) rph.handleWellKnown(w, r) return } host, _, err := net.SplitHostPort(r.Host) if err != nil { if addrErr, ok := err.(*net.AddrError); ok && !strings.Contains(addrErr.Err, "missing port") { logrus.Errorf("Error splitting host/port: %T %#v", err, err) http.Error(w, "internal error", http.StatusInternalServerError) return } logrus.Debugf("Using %q as hostname (no port)", r.Host) host = r.Host } logrus.Infof("Proxying request: Host=%s, Path=%s, ResolvedHost=%s", r.Host, r.URL.Path, host) // Try exact match first if v, ok := rph.Map[r.Host]; ok { logrus.Infof("Found exact match for host %s, forwarding to backend", r.Host) v.ServeHTTP(w, r) return } // Try with resolved host (without port) if v, ok := rph.Map[host]; ok { logrus.Infof("Found match for resolved host %s, forwarding to backend", host) v.ServeHTTP(w, r) return } logrus.Warnf("No mapping found for host: %s (resolved: %s). Available hosts: %v", r.Host, host, rph.getHostList()) if rph.NotFoundHandler == nil { http.NotFoundHandler().ServeHTTP(w, r) return } rph.NotFoundHandler.ServeHTTP(w, r) } func (rph reverseProxiesHandler) getHostList() []string { hosts := make([]string, 0, len(rph.Map)) for host := range rph.Map { hosts = append(hosts, host) } return hosts } func (rph reverseProxiesHandler) handleWellKnown(w http.ResponseWriter, r *http.Request) { // Проверяем и при необходимости монтируем NFS перед обработкой if err := nfs.EnsureMounted(); err != nil { logrus.Errorf("Failed to ensure NFS is mounted: %v", err) http.Error(w, "NFS mount error", http.StatusInternalServerError) return } path := strings.TrimPrefix(r.URL.Path, "/.well-known/acme-wellknown/") if path == "" || strings.Contains(path, "..") { logrus.Warnf("Invalid .well-known path: %s", r.URL.Path) http.Error(w, "Invalid path", http.StatusBadRequest) return } fullPath := filepath.Join(rph.WellKnownDir, path) logrus.Infof("Well-known request: method=%s, path=%s, fullPath=%s", r.Method, path, fullPath) switch r.Method { case "GET", "HEAD": data, err := ioutil.ReadFile(fullPath) if err != nil { if os.IsNotExist(err) { http.NotFound(w, r) return } logrus.Errorf("Error reading file %s: %v", fullPath, err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/octet-stream") w.WriteHeader(http.StatusOK) if r.Method == "GET" { w.Write(data) } case "POST", "PUT": data, err := ioutil.ReadAll(r.Body) if err != nil { logrus.Errorf("Error reading request body: %v", err) http.Error(w, "Bad request", http.StatusBadRequest) return } dir := filepath.Dir(fullPath) if err := os.MkdirAll(dir, 0755); err != nil { logrus.Errorf("Error creating directory %s: %v", dir, err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } if err := ioutil.WriteFile(fullPath, data, 0644); err != nil { logrus.Errorf("Error writing file %s: %v", fullPath, err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) if r.Method == "PUT" { w.Write([]byte("File created/updated successfully")) } else { w.Write([]byte("File created successfully")) } case "DELETE": if err := os.Remove(fullPath); err != nil { if os.IsNotExist(err) { http.NotFound(w, r) return } logrus.Errorf("Error deleting file %s: %v", fullPath, err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) w.Write([]byte("File deleted successfully")) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } }