package proxy import ( "crypto/tls" "crypto/x509" "fmt" "log/slog" "net/http" "net/http/httputil" "net/url" "os" "yobble-gateway-go/internal/config" "yobble-gateway-go/internal/logger" "yobble-gateway-go/internal/middleware" "yobble-gateway-go/pkg/geoip" ) // NewProxyHandler создает новый HTTP-обработчик, который выполняет обратное проксирование запросов. func NewProxyHandler(cfg *config.Settings, geoIPService *geoip.GeoIPService) http.Handler { op := "proxy.NewProxyHandler" log := logger.NewLoggerWithOp(op) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Проверка GeoIP realIP := middleware.GetRealIP(r) log.Debug("real IP", slog.String("ip", realIP)) if realIP != "" { isBlocked, countryCode := geoIPService.IsCountryBlocked(realIP) if isBlocked { log.Warn("access denied due to blocked country", slog.String("ip", realIP), slog.String("country_code", countryCode)) http.Error(w, "Access denied due to service policy.\n", http.StatusForbidden) return } } backend, tailPath := ResolveBackend(r.URL.Path, cfg.RouteConfig) if backend == nil { log.Warn("no route found for path", slog.String("path", r.URL.Path)) http.Error(w, "Route not found", http.StatusNotFound) return } backendURL, err := url.Parse(backend.URL) if err != nil { log.Error("failed to parse backend URL", slog.String("url", backend.URL), slog.Any("error", err)) http.Error(w, "Upstream error", http.StatusBadGateway) return } // Создаем обратный прокси proxy := httputil.NewSingleHostReverseProxy(backendURL) // Изменяем запрос, который будет отправлен на бэкенд originalDirector := proxy.Director proxy.Director = func(req *http.Request) { originalDirector(req) // Устанавливает базовые заголовки и URL // Устанавливаем правильный путь для бэкенда req.URL.Path = tailPath // Устанавливаем заголовки для информирования бэкенда об исходном запросе req.Header.Set("X-Real-IP", realIP) req.Header.Set("X-Forwarded-For", realIP) req.Header.Set("X-Forwarded-Host", r.Host) req.Header.Set("X-Forwarded-Proto", r.URL.Scheme) // Удаляем заголовки, которые могут раскрыть информацию о внутреннем сервере req.Header.Del("Server") req.Header.Del("X-Powered-By") } // Изменяем ответ от бэкенда перед отправкой клиенту proxy.ModifyResponse = func(resp *http.Response) error { resp.Header.Del("Server") resp.Header.Del("X-Powered-By") return nil } if backend.YobbleSigned { tlsConfig, err := newTLSConfig(&cfg.TLS, backendURL.Host, backend.AllowUntrusted) if err != nil { log.Error("failed to create TLS config", slog.Any("error", err)) http.Error(w, "Upstream error", http.StatusBadGateway) return } proxy.Transport = &http.Transport{ TLSClientConfig: tlsConfig, } } else if backend.AllowUntrusted { proxy.Transport = &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, } } log.Info("forwarding request", slog.String("from", r.URL.Path), slog.String("to_backend", backendURL.String()), slog.String("with_path", tailPath)) proxy.ServeHTTP(w, r) }) } func newTLSConfig(cfg *config.TLSConfig, serverName string, allowUntrusted bool) (*tls.Config, error) { clientCert, err := tls.LoadX509KeyPair(cfg.ClientCert, cfg.ClientKey) if err != nil { return nil, fmt.Errorf("failed to load client certificate: %w", err) } caCert, err := os.ReadFile(cfg.RootCA) if err != nil { return nil, fmt.Errorf("failed to read CA certificate: %w", err) } caCertPool := x509.NewCertPool() if !caCertPool.AppendCertsFromPEM(caCert) { return nil, fmt.Errorf("failed to append CA certificate to pool") } return &tls.Config{ Certificates: []tls.Certificate{clientCert}, RootCAs: caCertPool, ServerName: serverName, InsecureSkipVerify: allowUntrusted, }, nil }