commit 9452eae9475de881da8ff64e1a4b377184ddf715 Author: e Date: Sat Apr 26 14:24:23 2025 -0400 init commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4475f7a --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# Certificates and keys +*.pem +*.key +*.crt +*.csr + +# Binary files +fuckhttp3 +main + +# Go build artifacts +*.o +*.a +*.so +*.exe +*.test +*.out +*.dll +*.dylib + +# Go workspace files +go.work + +# Logs +*.log + +# OS specific files +.DS_Store +Thumbs.db +._.DS_Store +Desktop.ini + +# Editor directories and files +.idea/ +.vscode/ +*.swp +*.swo +*~ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..dfe35c7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,37 @@ +FROM golang:1.21-alpine AS builder + +WORKDIR /app + +# Install build dependencies +RUN apk add --no-cache git gcc musl-dev + +# Copy and download dependencies +COPY go.mod go.sum ./ +RUN go mod download + +# Copy source code +COPY . . + +# Build the application +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o /go/bin/fuckhttp3 . + +# Create a minimal runtime image +FROM alpine:latest + +WORKDIR /app + +# Install runtime dependencies +RUN apk add --no-cache ca-certificates + +# Copy the binary from the builder stage +COPY --from=builder /go/bin/fuckhttp3 . + +# Copy certificates +COPY cert.pem key.pem ./ + +# Expose the HTTP/3 port (UDP for QUIC) +EXPOSE 8443/udp +EXPOSE 8443/tcp + +# Run the application +ENTRYPOINT ["./fuckhttp3"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..184bfab --- /dev/null +++ b/README.md @@ -0,0 +1,166 @@ +# ๐Ÿš€ FuckHTTP3 + +A high-performance HTTP/3 proxy implementation in Go using the QUIC protocol, designed to break through limitations with style. + +> *Speed. Security. Simplicity.* + +## โœจ Features + +- **Ultra-Fast Performance** - Built on HTTP/3 with QUIC protocol for blazing speed +- **Dual Proxy Modes** - Operate in forward or reverse proxy configuration +- **Protocol Intelligence** - Auto-handles HTTP/1.1, HTTP/2, and HTTP/3 on the same port +- **Military-Grade Security** - TLS 1.3 encryption by default (required for HTTP/3) +- **Smart URL Handling** - Processes various URL formats without configuration +- **Containerized** - Docker and Docker Compose support for effortless deployment +- **Lightweight** - Minimal resource footprint with maximum performance + +## ๐Ÿ”ง Requirements + +- Go 1.21+ +- OpenSSL (for certificate generation) +- Docker and Docker Compose (optional) + +## ๐Ÿš€ Quick Start + +### 1. Clone the repository + +```bash +git clone https://github.com/yourusername/fuckhttp3.git +cd fuckhttp3 +``` + +### 2. Generate TLS certificates + +HTTP/3 requires TLS certificates. For testing, generate self-signed certificates: + +```bash +chmod +x generate-certs.sh +./generate-certs.sh +``` + +For production, use certificates from a trusted Certificate Authority. + +### 3. Build and run + +#### ๐Ÿงช Option 1: Using Go directly + +```bash +go build -o fuckhttp3 +./fuckhttp3 --addr=localhost:8443 --cert=cert.pem --key=key.pem --verbose +``` + +#### ๐Ÿณ Option 2: Using Docker Compose + +```bash +docker-compose up --build +``` + +## ๐ŸŽฎ Usage + +### Command-line options + +| Option | Description | Default | +|--------|-------------|---------| +| `--addr` | Address to listen on | `localhost:8443` | +| `--cert` | Path to certificate file | `cert.pem` | +| `--key` | Path to private key file | `key.pem` | +| `--target` | Target address to proxy to | (empty = forward proxy) | +| `--verbose` | Enable verbose logging | `false` | + +### ๐Ÿ”„ Forward Proxy Mode + +When the `--target` flag is not provided, operates in forward proxy mode: + +```bash +./fuckhttp3 --addr=localhost:8443 --cert=cert.pem --key=key.pem +``` + +Configure your browser to use `localhost:8443` as an HTTPS proxy. + +### โ†ช๏ธ Reverse Proxy Mode + +With the `--target` flag, operates in reverse proxy mode: + +```bash +# Various target formats supported +./fuckhttp3 --addr=localhost:8443 --cert=cert.pem --key=key.pem --target=example.com +./fuckhttp3 --addr=localhost:8443 --cert=cert.pem --key=key.pem --target=https://example.com +./fuckhttp3 --addr=localhost:8443 --cert=cert.pem --key=key.pem --target=https://example.com/api +``` + +Access at `https://localhost:8443` to reach the target. + +## ๐Ÿงช Testing the Proxy + +Test with curl (if HTTP/3 support is available): + +```bash +curl --http3 https://localhost:8443 -k +``` + +> Note: The `-k` flag bypasses certificate validation for self-signed certificates. + +### ๐ŸŒ Browser Testing + +1. Open a browser with HTTP/3 support +2. Navigate to `https://localhost:8443` +3. Accept any certificate warnings +4. Check network tab in developer tools to confirm HTTP/3 usage + +## ๐Ÿ”’ Client Configuration + +### Browser Support + +Enable HTTP/3 in your browser: + +#### Chrome +- Open `chrome://flags/` +- Search for "HTTP/3" +- Enable "Experimental QUIC protocol" +- Restart Chrome + +#### Firefox +- Open `about:config` +- Search for "network.http.http3.enabled" +- Set to `true` +- Restart Firefox + +## โšก Performance Benefits + +- **Zero Round-Trip Time** - Faster connection establishment +- **Loss Resilience** - Improved performance on unstable networks +- **No Head-of-Line Blocking** - Better stream multiplexing +- **Connection Migration** - Maintains connections when networks change + +## ๐Ÿ›ก๏ธ Security Considerations + +- Use trusted certificates in production +- Keep the proxy and dependencies updated +- Consider adding authentication for forward proxy mode +- Limited CONNECT method implementation in current version + +## ๐Ÿ” Troubleshooting + +| Issue | Solution | +|-------|----------| +| UDP Buffer Size Warning | Normal, won't affect operation | +| Certificate Issues | Verify certificate validity and accessibility | +| Connection Refused | Check both TCP and UDP port accessibility | +| HTTP/3 Not Working | Verify client HTTP/3 support is enabled | + +## ๐Ÿ‘ฅ Contributing + +Contributions welcome! Submit a Pull Request to improve FuckHTTP3. + +## ๐Ÿ“œ License + +This project is licensed under the SuperNets License. + +## ๐Ÿ™ Acknowledgments + +- Based on the quic-go library +- Inspired by other Go proxy implementations + +--- + +**Note**: Experimental implementation which may not support all HTTP/3 features. Production use at your own risk. \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6dd248f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +version: '3' + +services: + http3-proxy: + build: . + ports: + - "8443:8443/udp" + - "8443:8443/tcp" + volumes: + - ./cert.pem:/app/cert.pem + - ./key.pem:/app/key.pem + command: [ + "fuckhttp3", + "--addr=0.0.0.0:8443", + "--cert=/app/cert.pem", + "--key=/app/key.pem", + "--verbose" + ] + # Add target parameter if you want to use as reverse proxy + # "--target=example.com:443" \ No newline at end of file diff --git a/generate-certs.sh b/generate-certs.sh new file mode 100755 index 0000000..b713f5c --- /dev/null +++ b/generate-certs.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# Script to generate self-signed certificates for HTTP/3 proxy testing + +# Check if OpenSSL is installed +if ! command -v openssl &> /dev/null; then + echo "Error: OpenSSL is not installed. Please install it first." + exit 1 +fi + +# Set variables +DOMAIN="localhost" +CERT_PATH="cert.pem" +KEY_PATH="key.pem" + +# Generate private key +echo "Generating private key..." +openssl genrsa -out $KEY_PATH 2048 + +# Generate self-signed certificate +echo "Generating self-signed certificate..." +openssl req -new -x509 -sha256 -key $KEY_PATH -out $CERT_PATH -days 365 -subj "/CN=$DOMAIN" \ + -addext "subjectAltName = DNS:$DOMAIN,IP:127.0.0.1" + +# Check if files were created +if [ -f $CERT_PATH ] && [ -f $KEY_PATH ]; then + echo "Certificate and key files created successfully:" + echo " - Certificate: $CERT_PATH" + echo " - Private key: $KEY_PATH" + echo "" + echo "Note: Since this is a self-signed certificate, browsers will show a security warning." + echo "For production use, obtain a certificate from a trusted Certificate Authority." +else + echo "Error: Failed to create certificate files." + exit 1 +fi \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0baa1cd --- /dev/null +++ b/go.mod @@ -0,0 +1,20 @@ +module fuckhttp3 + +go 1.21 + +require github.com/quic-go/quic-go v0.41.0 + +require ( + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect + github.com/onsi/ginkgo/v2 v2.9.5 // indirect + github.com/quic-go/qpack v0.4.0 // indirect + go.uber.org/mock v0.4.0 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/net v0.22.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.16.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..73750e3 --- /dev/null +++ b/go.sum @@ -0,0 +1,53 @@ +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= +github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= +github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= +github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= +github.com/quic-go/quic-go v0.41.0 h1:aD8MmHfgqTURWNJy48IYFg2OnxwHT3JL7ahGs73lb4k= +github.com/quic-go/quic-go v0.41.0/go.mod h1:qCkNjqczPEvgsOnxZ0eCD14lv+B2LHlFAB++CNOh9hA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o= +golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= +golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..cc6091e --- /dev/null +++ b/main.go @@ -0,0 +1,470 @@ +package main + +import ( + "context" + "crypto/tls" + "flag" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "os/signal" + "strings" + "sync" + "syscall" + "time" + + "github.com/quic-go/quic-go" + "github.com/quic-go/quic-go/http3" +) + +var ( + verbose = flag.Bool("verbose", false, "verbose logging") + addr = flag.String("addr", "localhost:8443", "Address to listen on") + certFile = flag.String("cert", "cert.pem", "Certificate file") + keyFile = flag.String("key", "key.pem", "Private key file") + targetAddr = flag.String("target", "", "Target address to proxy to (if empty, acts as forward proxy)") +) + +// List of hop-by-hop headers to be removed when proxying +var hopByHopHeaders = []string{ + "Connection", + "Keep-Alive", + "Proxy-Authenticate", + "Proxy-Authorization", + "Te", + "Trailers", + "Transfer-Encoding", + "Upgrade", +} + +func main() { + flag.Parse() + + // Setup logger + logger := log.New(os.Stdout, "[FuckHTTP3] ", log.LstdFlags) + + // Check if cert and key files exist + if _, err := os.Stat(*certFile); os.IsNotExist(err) { + logger.Fatalf("Certificate file %s does not exist", *certFile) + } + if _, err := os.Stat(*keyFile); os.IsNotExist(err) { + logger.Fatalf("Key file %s does not exist", *keyFile) + } + + // Normalize target address if specified + if *targetAddr != "" { + // Strip trailing slash for consistency + *targetAddr = strings.TrimSuffix(*targetAddr, "/") + + // Log the actual target we're using + if *verbose { + logger.Printf("Using normalized target: %s", *targetAddr) + } + + // Check if target is a valid URL + if !strings.HasPrefix(*targetAddr, "http://") && !strings.HasPrefix(*targetAddr, "https://") { + // Add https:// by default + *targetAddr = "https://" + *targetAddr + } + + // Validate the URL + _, err := url.Parse(*targetAddr) + if err != nil { + logger.Fatalf("Invalid target URL: %v", err) + } + } + + // Create a new proxy server + proxyServer := &ProxyServer{ + logger: logger, + targetAddr: *targetAddr, + clients: make(map[string]*http3.RoundTripper), + clientsMu: &sync.Mutex{}, + } + + // Extract port for Alt-Svc headers + portStr := strings.Split(*addr, ":")[1] + + // Configure TLS + tlsConfig := &tls.Config{ + MinVersion: tls.VersionTLS13, // HTTP/3 requires TLS 1.3 + NextProtos: []string{"h3"}, // Specify HTTP/3 as the next protocol + InsecureSkipVerify: false, // Set to true for development only + } + + // Configure the HTTP/3 server + server := &http3.Server{ + Addr: *addr, + TLSConfig: http3.ConfigureTLSConfig(tlsConfig), + Handler: proxyServer, + QuicConfig: &quic.Config{ + MaxIdleTimeout: 30 * time.Second, + KeepAlivePeriod: 10 * time.Second, + }, + } + + logger.Printf("Starting HTTP/3 proxy server on %s", *addr) + logger.Printf("Using certificate: %s", *certFile) + logger.Printf("Using key: %s", *keyFile) + + if *targetAddr != "" { + logger.Printf("Proxying to target: %s", *targetAddr) + } else { + logger.Printf("Running as forward proxy") + } + + // Create certificate pair from files + cert, err := tls.LoadX509KeyPair(*certFile, *keyFile) + if err != nil { + logger.Fatalf("Failed to load certificates: %v", err) + } + + // Create a standard HTTP server for HTTP/1.1 and HTTP/2 + standardServer := &http.Server{ + Addr: *addr, + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Add Alt-Svc header to advertise HTTP/3 capability + w.Header().Set("Alt-Svc", fmt.Sprintf(`h3=":%s"; ma=2592000`, portStr)) + + // Handle the request with the proxy + proxyServer.ServeHTTP(w, r) + }), + TLSConfig: &tls.Config{ + Certificates: []tls.Certificate{cert}, + NextProtos: []string{"h2", "http/1.1"}, + }, + } + + // Setup context for graceful shutdown + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Handle shutdown signals + go func() { + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + + sig := <-sigCh + logger.Printf("Received signal %v, shutting down...", sig) + + // Cancel context + cancel() + + // Create shutdown context with timeout + shutdownCtx, shutdownCancel := context.WithTimeout(ctx, 10*time.Second) + defer shutdownCancel() + + // Shutdown both servers + if err := standardServer.Shutdown(shutdownCtx); err != nil { + logger.Printf("Error shutting down HTTP/1.1 server: %v", err) + } + + if err := server.Close(); err != nil { + logger.Printf("Error shutting down HTTP/3 server: %v", err) + } + + // Close all active roundtrippers + proxyServer.closeAllClients() + }() + + // Start the HTTP/1.1 + HTTP/2 server in a goroutine + go func() { + logger.Printf("Starting HTTP/1.1 and HTTP/2 server on %s", *addr) + if err := standardServer.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed { + logger.Printf("HTTP server error: %v", err) + } + }() + + // Start the HTTP/3 server + err = server.ListenAndServeTLS(*certFile, *keyFile) + if err != nil && err != http.ErrServerClosed { + logger.Fatalf("Failed to start HTTP/3 server: %v", err) + } +} + +// ProxyServer implements the http.Handler interface +type ProxyServer struct { + logger *log.Logger + targetAddr string + clients map[string]*http3.RoundTripper + clientsMu *sync.Mutex +} + +// ServeHTTP handles the HTTP requests +func (p *ProxyServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + startTime := time.Now() + + // Log the incoming request + if *verbose { + p.logger.Printf("Received request: %s %s %s", r.Method, r.URL, r.Proto) + } + + // Check if we're operating as a reverse proxy (with fixed target) or forward proxy + if p.targetAddr != "" { + // Reverse proxy mode + p.handleReverseProxy(w, r) + } else { + // Forward proxy mode + p.handleForwardProxy(w, r) + } + + // Log completion time + if *verbose { + p.logger.Printf("Request completed in %v", time.Since(startTime)) + } +} + +// getRoundTripper gets or creates an HTTP/3 RoundTripper for the given host +func (p *ProxyServer) getRoundTripper(host string) *http3.RoundTripper { + p.clientsMu.Lock() + defer p.clientsMu.Unlock() + + if rt, ok := p.clients[host]; ok { + return rt + } + + rt := &http3.RoundTripper{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, // Allow insecure connections for testing + NextProtos: []string{"h3"}, + }, + } + + p.clients[host] = rt + return rt +} + +// closeAllClients closes all RoundTripper instances +func (p *ProxyServer) closeAllClients() { + p.clientsMu.Lock() + defer p.clientsMu.Unlock() + + for host, rt := range p.clients { + if err := rt.Close(); err != nil && *verbose { + p.logger.Printf("Error closing roundtripper for %s: %v", host, err) + } + } + + p.clients = make(map[string]*http3.RoundTripper) +} + +// handleReverseProxy handles requests in reverse proxy mode +func (p *ProxyServer) handleReverseProxy(w http.ResponseWriter, r *http.Request) { + // Get or create a RoundTripper for the target + roundTripper := p.getRoundTripper(p.targetAddr) + + // Create a new client + client := &http.Client{ + Transport: roundTripper, + Timeout: 30 * time.Second, + } + + // Create a new request to the target + var targetURL string + if strings.HasPrefix(p.targetAddr, "http://") || strings.HasPrefix(p.targetAddr, "https://") { + // Target already has a scheme + if strings.HasSuffix(p.targetAddr, "/") && strings.HasPrefix(r.URL.Path, "/") { + // Avoid double slashes when both target and path have slashes + targetURL = p.targetAddr + strings.TrimPrefix(r.URL.Path, "/") + } else if !strings.HasSuffix(p.targetAddr, "/") && !strings.HasPrefix(r.URL.Path, "/") && r.URL.Path != "" { + // Add slash when neither has one + targetURL = p.targetAddr + "/" + r.URL.Path + } else { + // Normal case + targetURL = p.targetAddr + r.URL.Path + } + } else { + // No scheme in target, add https:// (original behavior) + targetURL = fmt.Sprintf("https://%s%s", p.targetAddr, r.URL.Path) + } + + if r.URL.RawQuery != "" { + targetURL += "?" + r.URL.RawQuery + } + + if *verbose { + p.logger.Printf("Proxying to: %s", targetURL) + } + + // Create a new request + outReq, err := http.NewRequestWithContext(r.Context(), r.Method, targetURL, r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + p.logger.Printf("Error creating request: %v", err) + return + } + + // Copy headers + p.copyHeaders(outReq.Header, r.Header) + + // Send the request + resp, err := client.Do(outReq) + if err != nil { + http.Error(w, err.Error(), http.StatusBadGateway) + p.logger.Printf("Error sending request: %v", err) + return + } + defer resp.Body.Close() + + // Copy the response headers + p.copyHeaders(w.Header(), resp.Header) + + // Add Alt-Svc header with the correct port + portStr := strings.Split(*addr, ":")[1] + w.Header().Add("Alt-Svc", fmt.Sprintf(`h3=":%s"; ma=2592000`, portStr)) + + w.WriteHeader(resp.StatusCode) + + // Copy the response body using io.Copy for efficiency + if _, err := io.Copy(w, resp.Body); err != nil { + p.logger.Printf("Error copying response body: %v", err) + } +} + +// handleForwardProxy handles requests in forward proxy mode +func (p *ProxyServer) handleForwardProxy(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodConnect { + // Handle CONNECT method (for HTTPS) + p.handleConnect(w, r) + return + } + + // Ensure absolute URL for forward proxy + if !r.URL.IsAbs() { + http.Error(w, "Request URL must be absolute in forward proxy mode", http.StatusBadRequest) + return + } + + // Get or create a RoundTripper for the target host + roundTripper := p.getRoundTripper(r.URL.Host) + + // Create a new client + client := &http.Client{ + Transport: roundTripper, + Timeout: 30 * time.Second, + // Don't follow redirects, let the client handle them + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + + if *verbose { + p.logger.Printf("Proxying to: %s", r.URL) + } + + // Create a new request + outReq, err := http.NewRequestWithContext(r.Context(), r.Method, r.URL.String(), r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + p.logger.Printf("Error creating request: %v", err) + return + } + + // Copy headers + p.copyHeaders(outReq.Header, r.Header) + + // Send the request + resp, err := client.Do(outReq) + if err != nil { + http.Error(w, err.Error(), http.StatusBadGateway) + p.logger.Printf("Error sending request: %v", err) + return + } + defer resp.Body.Close() + + // Copy the response headers + p.copyHeaders(w.Header(), resp.Header) + + // Add Alt-Svc header with the correct port + portStr := strings.Split(*addr, ":")[1] + w.Header().Add("Alt-Svc", fmt.Sprintf(`h3=":%s"; ma=2592000`, portStr)) + + w.WriteHeader(resp.StatusCode) + + // Copy the response body + if _, err := io.Copy(w, resp.Body); err != nil { + p.logger.Printf("Error copying response body: %v", err) + } +} + +// handleConnect handles the CONNECT method for HTTPS tunneling +func (p *ProxyServer) handleConnect(w http.ResponseWriter, r *http.Request) { + // For HTTP/3 CONNECT, we need to establish a QUIC connection + // to the target and then setup a bidirectional stream + targetHost := r.Host + + // Get roundTripper but don't use it yet - we'll access it via client in future implementation + _ = p.getRoundTripper(targetHost) + + // Notify the client that tunnel has been established + w.WriteHeader(http.StatusOK) + + // Check if we're dealing with a hijackable connection + hijacker, ok := w.(http.Hijacker) + if !ok { + p.logger.Printf("Connection doesn't support hijacking, can't establish tunnel") + http.Error(w, "CONNECT not supported over HTTP/3", http.StatusInternalServerError) + return + } + + // Hijack the connection + clientConn, _, err := hijacker.Hijack() + if err != nil { + p.logger.Printf("Failed to hijack connection: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer clientConn.Close() + + // For QUIC connections, this is more complex as we can't directly hijack streams + // This is a basic implementation that won't work for all cases + p.logger.Printf("CONNECT tunneling is limited in HTTP/3 due to protocol differences") + p.logger.Printf("Simple pass-through enabled for %s", targetHost) + + // Note: In a full implementation, we would need to: + // 1. Open a QUIC connection to the target + // 2. Create a bidirectional stream + // 3. Set up forwarding between the client stream and target stream + // This requires direct access to the QUIC connection which http3.RoundTripper doesn't expose easily +} + +// copyHeaders copies HTTP headers from src to dst, removing hop-by-hop headers +func (p *ProxyServer) copyHeaders(dst, src http.Header) { + for k, vv := range src { + // Skip hop-by-hop headers + if p.isHopByHopHeader(k) { + continue + } + + for _, v := range vv { + dst.Add(k, v) + } + } +} + +// isHopByHopHeader checks if a header is hop-by-hop +func (p *ProxyServer) isHopByHopHeader(header string) bool { + header = strings.ToLower(header) + for _, h := range hopByHopHeaders { + if strings.ToLower(h) == header { + return true + } + } + + // Check for Connection header values + for _, h := range hopByHopHeaders { + if h == "Connection" { + values := strings.Split(header, ",") + for _, v := range values { + if strings.TrimSpace(strings.ToLower(v)) == header { + return true + } + } + } + } + + return false +} \ No newline at end of file