init commit

This commit is contained in:
e 2025-04-26 14:24:23 -04:00
commit 9452eae947
8 changed files with 839 additions and 0 deletions

38
.gitignore vendored Normal file
View File

@ -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
*~

37
Dockerfile Normal file
View File

@ -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"]

166
README.md Normal file
View File

@ -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.

20
docker-compose.yml Normal file
View File

@ -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"

35
generate-certs.sh Executable file
View File

@ -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

20
go.mod Normal file
View File

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

53
go.sum Normal file
View File

@ -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=

470
main.go Normal file
View File

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