init commit
This commit is contained in:
commit
9452eae947
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal 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
37
Dockerfile
Normal 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
166
README.md
Normal 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
20
docker-compose.yml
Normal 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
35
generate-certs.sh
Executable 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
20
go.mod
Normal 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
53
go.sum
Normal 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
470
main.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user