2026-04-17 11:56:39 +07:00

270 lines
9.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}
}