126 lines
4.2 KiB
Go
126 lines
4.2 KiB
Go
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
|
||
}
|