package main import ( "context" "crypto/tls" "flag" "fmt" "log" "net" "net/http" "os" "os/signal" "strings" "syscall" "time" ) var ( version = "1.0.0" ) func parseTLSVersion(v string) (uint16, error) { switch strings.TrimSpace(v) { case "1.0", "tls1.0", "TLS1.0": return tls.VersionTLS10, nil case "1.1", "tls1.1", "TLS1.1": return tls.VersionTLS11, nil case "1.2", "tls1.2", "TLS1.2": return tls.VersionTLS12, nil case "1.3", "tls1.3", "TLS1.3": return tls.VersionTLS13, nil default: return 0, fmt.Errorf("unknown TLS version %q (valid options: 1.0, 1.1, 1.2, 1.3)", v) } } func printUsage() { fmt.Fprintf(os.Stderr, `smallprox v%s - Miniature TLS 1.2 Reverse Proxy Usage: smallprox -backend [options] Options: -backend, -b string Target backend URL (required, e.g. https://tls13-service.example.com) -listen, -l string Address and port to bind to (default ":8443") -tls-min string Minimum incoming TLS version: 1.0, 1.1, 1.2, 1.3 (default "1.2") -tls-max string Maximum incoming TLS version: 1.0, 1.1, 1.2, 1.3 (default "1.2") -cert string Path to TLS certificate PEM file (optional; self-signed generated if omitted) -key string Path to TLS private key PEM file (optional; self-signed generated if omitted) -http Listen in plain HTTP mode instead of HTTPS (default false) -preserve-host Preserve incoming Host header instead of rewriting to backend host (default false) -version, -v Print version information and exit -help, -h Show this help message Examples: # Basic reverse proxy to a TLS 1.3 backend (PowerShell -> TLS 1.2 proxy:8443 -> TLS 1.3 backend) smallprox -backend https://api.example.com # Custom listen port and specific TLS range smallprox -listen :9443 -backend https://api.example.com -tls-min 1.2 -tls-max 1.3 # Using custom certificates smallprox -backend https://api.example.com -cert server.crt -key server.key # PowerShell usage example: # [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true} # Invoke-RestMethod -Uri https://localhost:8443/api/endpoint `, version) } func main() { var ( backendFlag string backendShort string listenAddr string listenShort string certFile string keyFile string tlsMinStr string tlsMaxStr string plainHTTP bool preserveHost bool showVersion bool showVersionShort bool showHelp bool showHelpShort bool ) flag.StringVar(&backendFlag, "backend", "", "Target backend URL") flag.StringVar(&backendShort, "b", "", "Target backend URL (short)") flag.StringVar(&listenAddr, "listen", ":8443", "Listen address") flag.StringVar(&listenShort, "l", ":8443", "Listen address (short)") flag.StringVar(&certFile, "cert", "", "Path to TLS cert file") flag.StringVar(&keyFile, "key", "", "Path to TLS key file") flag.StringVar(&tlsMinStr, "tls-min", "1.2", "Minimum incoming TLS version") flag.StringVar(&tlsMaxStr, "tls-max", "1.2", "Maximum incoming TLS version") flag.BoolVar(&plainHTTP, "http", false, "Listen in plain HTTP mode") flag.BoolVar(&preserveHost, "preserve-host", false, "Preserve incoming Host header") flag.BoolVar(&showVersion, "version", false, "Print version") flag.BoolVar(&showVersionShort, "v", false, "Print version (short)") flag.BoolVar(&showHelp, "help", false, "Show help") flag.BoolVar(&showHelpShort, "h", false, "Show help (short)") flag.Usage = printUsage flag.Parse() if showHelp || showHelpShort { printUsage() os.Exit(0) } if showVersion || showVersionShort { fmt.Printf("smallprox version %s\n", version) os.Exit(0) } // Resolve flags with short versions rawBackend := backendFlag if rawBackend == "" { rawBackend = backendShort } // Also check positional argument if no flag given if rawBackend == "" && flag.NArg() > 0 { rawBackend = flag.Arg(0) } if rawBackend == "" { fmt.Fprintln(os.Stderr, "[ERROR] Backend URL is required.") printUsage() os.Exit(1) } targetURL, err := ParseTargetURL(rawBackend) if err != nil { log.Fatalf("[FATAL] %v", err) } // Determine listen address bindAddr := listenAddr if bindAddr == ":8443" && listenShort != ":8443" { bindAddr = listenShort } // Parse TLS versions minTLS, err := parseTLSVersion(tlsMinStr) if err != nil { log.Fatalf("[FATAL] Invalid -tls-min: %v", err) } maxTLS, err := parseTLSVersion(tlsMaxStr) if err != nil { log.Fatalf("[FATAL] Invalid -tls-max: %v", err) } if minTLS > maxTLS { log.Fatalf("[FATAL] -tls-min (%s) cannot be greater than -tls-max (%s)", tlsMinStr, tlsMaxStr) } proxyHandler := NewProxyHandler(ProxyConfig{ TargetURL: targetURL, PreserveHost: preserveHost, }) server := &http.Server{ Addr: bindAddr, Handler: proxyHandler, ReadTimeout: 60 * time.Second, WriteTimeout: 60 * time.Second, IdleTimeout: 120 * time.Second, } protocol := "HTTPS" if plainHTTP { protocol = "HTTP" } else { tlsConfig := &tls.Config{ MinVersion: minTLS, MaxVersion: maxTLS, } if certFile != "" && keyFile != "" { cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { log.Fatalf("[FATAL] Failed to load certificate/key: %v", err) } tlsConfig.Certificates = []tls.Certificate{cert} log.Printf("[INFO] Loaded TLS certificate from %s and %s", certFile, keyFile) } else { hostPart, _, _ := net.SplitHostPort(bindAddr) extraHosts := []string{} if hostPart != "" { extraHosts = append(extraHosts, hostPart) } cert, err := generateSelfSignedCert(extraHosts) if err != nil { log.Fatalf("[FATAL] Failed to generate self-signed certificate: %v", err) } tlsConfig.Certificates = []tls.Certificate{cert} log.Printf("[INFO] Generated ephemeral self-signed TLS certificate") } server.TLSConfig = tlsConfig } // Channel to listen for shutdown signals stopChan := make(chan os.Signal, 1) signal.Notify(stopChan, os.Interrupt, syscall.SIGTERM) go func() { log.Printf("[INFO] smallprox v%s starting...", version) log.Printf("[INFO] Listening on %s://%s (incoming TLS: %s - %s)", protocol, bindAddr, tlsVersionName(minTLS), tlsVersionName(maxTLS)) log.Printf("[INFO] Proxying to %s (backend TLS verification disabled)", targetURL.String()) var err error if plainHTTP { err = server.ListenAndServe() } else { // TLSConfig already contains the certificate err = server.ListenAndServeTLS("", "") } if err != nil && err != http.ErrServerClosed { log.Fatalf("[FATAL] Server listener error: %v", err) } }() <-stopChan log.Printf("[INFO] Shutting down smallprox gracefully...") shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := server.Shutdown(shutdownCtx); err != nil { log.Printf("[ERROR] Server shutdown error: %v", err) } log.Printf("[INFO] smallprox stopped.") }