145 lines
3.6 KiB
Go
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)
|
|
}
|
|
}
|