131 lines
		
	
	
		
			4.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			131 lines
		
	
	
		
			4.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package proxy
 | 
						||
 | 
						||
import (
 | 
						||
	"crypto/tls"
 | 
						||
	"crypto/x509"
 | 
						||
	"fmt"
 | 
						||
	"log/slog"
 | 
						||
	"net/http"
 | 
						||
	"net/http/httputil"
 | 
						||
	"net/url"
 | 
						||
	"os"
 | 
						||
    "strings"
 | 
						||
 | 
						||
	"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
 | 
						||
 | 
						||
			// Устанавливаем правильный путь для бэкенда
 | 
						||
			if strings.HasPrefix(r.URL.Path, "/socket.io") {
 | 
						||
				req.URL.Path = r.URL.Path
 | 
						||
			} else {
 | 
						||
				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
 | 
						||
}
 |