2026-06-04 17:32:11 +07:00

361 lines
12 KiB
Go
Raw 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"
)
const (
acmeChallengePrefix = "/.well-known/acme-challenge/"
acmeWellknownPrefix = "/.well-known/acme-wellknown/"
)
// IsAcmeWellKnownPath reports whether path is an ACME HTTP-01 challenge URL.
func IsAcmeWellKnownPath(path string) bool {
return strings.HasPrefix(path, acmeChallengePrefix) || strings.HasPrefix(path, acmeWellknownPrefix)
}
func acmeTokenFromPath(path string) (token string, layout string, ok bool) {
switch {
case strings.HasPrefix(path, acmeChallengePrefix):
layout = acmeChallengePrefix
token = strings.TrimPrefix(path, acmeChallengePrefix)
case strings.HasPrefix(path, acmeWellknownPrefix):
layout = acmeWellknownPrefix
token = strings.TrimPrefix(path, acmeWellknownPrefix)
default:
return "", "", false
}
if token == "" || strings.Contains(token, "..") {
return "", "", false
}
return token, layout, true
}
func acmeFilePaths(wellKnownDir, token string) []string {
return []string{
filepath.Join(wellKnownDir, "acme-challenge", token),
filepath.Join(wellKnownDir, token),
}
}
func defaultAcmeWritePath(wellKnownDir, token, layout string) string {
if layout == acmeWellknownPrefix {
return filepath.Join(wellKnownDir, token)
}
return filepath.Join(wellKnownDir, "acme-challenge", token)
}
// 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, acmeChallengePrefix) || strings.HasPrefix(r.URL.Path, acmeWellknownPrefix) {
token, layout, ok := acmeTokenFromPath(r.URL.Path)
if !ok {
logrus.Warnf("Invalid .well-known path: %s", r.URL.Path)
http.Error(w, "Invalid path", http.StatusBadRequest)
return
}
logrus.Infof("Handling ACME well-known request: %s (layout=%s)", r.URL.Path, layout)
rph.handleWellKnown(w, r, token, layout)
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, token, layout string) {
// Проверяем и при необходимости монтируем 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
}
paths := acmeFilePaths(rph.WellKnownDir, token)
logrus.Infof("Well-known request: method=%s, token=%s, layout=%s, paths=%v",
r.Method, token, layout, paths)
switch r.Method {
case "GET", "HEAD":
data, _, err := readFirstExisting(paths)
if err != nil {
if os.IsNotExist(err) {
http.NotFound(w, r)
return
}
logrus.Errorf("Error reading ACME challenge files %v: %v", paths, 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
}
writePath := existingAcmePath(paths)
if writePath == "" {
writePath = defaultAcmeWritePath(rph.WellKnownDir, token, layout)
}
dir := filepath.Dir(writePath)
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(writePath, data, 0644); err != nil {
logrus.Errorf("Error writing file %s: %v", writePath, 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 := removeFirstExisting(paths); err != nil {
if os.IsNotExist(err) {
http.NotFound(w, r)
return
}
logrus.Errorf("Error deleting ACME challenge files %v: %v", paths, 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)
}
}
func readFirstExisting(paths []string) ([]byte, string, error) {
var lastErr error
for _, path := range paths {
data, err := ioutil.ReadFile(path)
if err == nil {
return data, path, nil
}
if !os.IsNotExist(err) {
return nil, "", err
}
lastErr = err
}
if lastErr == nil {
lastErr = os.ErrNotExist
}
return nil, "", lastErr
}
func existingAcmePath(paths []string) string {
for _, path := range paths {
if _, err := os.Stat(path); err == nil {
return path
}
}
return ""
}
func removeFirstExisting(paths []string) error {
var lastErr error
for _, path := range paths {
err := os.Remove(path)
if err == nil {
return nil
}
if !os.IsNotExist(err) {
return err
}
lastErr = err
}
if lastErr == nil {
lastErr = os.ErrNotExist
}
return lastErr
}