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) } }