Files
2026-08-27 12:00:08 -06:00

145 lines
3.6 KiB
Go

package main
import (
"crypto/tls"
"fmt"
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"time"
)
// ProxyConfig holds options for the reverse proxy.
type ProxyConfig struct {
TargetURL *url.URL
PreserveHost bool
Verbose bool
}
// loggingResponseWriter wraps http.ResponseWriter to capture the HTTP status code.
type loggingResponseWriter struct {
http.ResponseWriter
statusCode int
}
func (lrw *loggingResponseWriter) WriteHeader(code int) {
lrw.statusCode = code
lrw.ResponseWriter.WriteHeader(code)
}
// NewProxyHandler creates an http.Handler that reverse proxies requests to the target URL.
func NewProxyHandler(cfg ProxyConfig) http.Handler {
proxy := httputil.NewSingleHostReverseProxy(cfg.TargetURL)
// Transport configured with InsecureSkipVerify to trust all backend certificates
proxy.Transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS10,
MaxVersion: tls.VersionTLS13,
},
}
originalDirector := proxy.Director
proxy.Director = func(req *http.Request) {
originalDirector(req)
// Set Host header to target host unless preserve host is explicitly requested.
// This is vital for backend virtual hosting and SNI.
if !cfg.PreserveHost {
req.Host = cfg.TargetURL.Host
}
if clientProto := req.Header.Get("X-Forwarded-Proto"); clientProto == "" {
if req.TLS != nil {
req.Header.Set("X-Forwarded-Proto", "https")
} else {
req.Header.Set("X-Forwarded-Proto", "http")
}
}
}
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("[ERROR] %s %s -> Backend error: %v", r.Method, r.URL.RequestURI(), err)
http.Error(w, fmt.Sprintf("Bad Gateway (smallprox): %v", err), http.StatusBadGateway)
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
lrw := &loggingResponseWriter{ResponseWriter: w, statusCode: http.StatusOK}
proxy.ServeHTTP(lrw, r)
duration := time.Since(start)
tlsVer := "HTTP"
if r.TLS != nil {
tlsVer = tlsVersionName(r.TLS.Version)
}
log.Printf("[%s] %s | %s %s -> %d (%s)",
tlsVer,
r.RemoteAddr,
r.Method,
r.URL.RequestURI(),
lrw.statusCode,
duration.Round(time.Millisecond/10),
)
})
}
// ParseTargetURL cleans and parses the backend target URL string.
func ParseTargetURL(raw string) (*url.URL, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil, fmt.Errorf("backend URL cannot be empty")
}
if !strings.Contains(raw, "://") {
raw = "https://" + raw
}
parsed, err := url.Parse(raw)
if err != nil {
return nil, fmt.Errorf("invalid backend URL %q: %w", raw, err)
}
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return nil, fmt.Errorf("unsupported backend scheme %q (must be http or https)", parsed.Scheme)
}
if parsed.Host == "" {
return nil, fmt.Errorf("backend URL must have a host")
}
return parsed, nil
}
// tlsVersionName returns the human-readable string for a TLS version identifier.
func tlsVersionName(ver uint16) string {
switch ver {
case tls.VersionTLS10:
return "TLS 1.0"
case tls.VersionTLS11:
return "TLS 1.1"
case tls.VersionTLS12:
return "TLS 1.2"
case tls.VersionTLS13:
return "TLS 1.3"
default:
return fmt.Sprintf("TLS 0x%04x", ver)
}
}