updates
This commit is contained in:
parent
9452eae947
commit
7d6e3363c0
6
.gitignore
vendored
6
.gitignore
vendored
@ -35,4 +35,8 @@ Desktop.ini
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
*~
|
||||
|
||||
# Templates (keep directory structure but ignore content)
|
||||
templates/*
|
||||
!templates/index.html
|
10
Dockerfile
10
Dockerfile
@ -9,6 +9,12 @@ RUN apk add --no-cache git gcc musl-dev
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Create templates directory
|
||||
RUN mkdir -p templates
|
||||
|
||||
# Copy templates first
|
||||
COPY templates/ templates/
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
@ -29,6 +35,10 @@ COPY --from=builder /go/bin/fuckhttp3 .
|
||||
# Copy certificates
|
||||
COPY cert.pem key.pem ./
|
||||
|
||||
# Create templates directory and copy templates
|
||||
RUN mkdir -p templates
|
||||
COPY templates/ templates/
|
||||
|
||||
# Expose the HTTP/3 port (UDP for QUIC)
|
||||
EXPOSE 8443/udp
|
||||
EXPOSE 8443/tcp
|
||||
|
211
README.md
211
README.md
@ -1,166 +1,157 @@
|
||||
# 🚀 FuckHTTP3
|
||||
# 🚀 FuckHTTP3 Proxy
|
||||
|
||||
A high-performance HTTP/3 proxy implementation in Go using the QUIC protocol, designed to break through limitations with style.
|
||||
A powerful HTTP/3 intercepting proxy with JavaScript injection capabilities. Browse websites through HTTP/3 (QUIC) while injecting custom JavaScript into every page.
|
||||
|
||||
> *Speed. Security. Simplicity.*
|
||||
> *Intercept. Inject. Control.*
|
||||
|
||||
## ✨ Features
|
||||
## 🔍 Overview
|
||||
|
||||
- **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
|
||||
FuckHTTP3 is a sophisticated proxy tool that:
|
||||
|
||||
## 🔧 Requirements
|
||||
- Acts as a forward proxy for HTTP/3 traffic
|
||||
- Provides a user-friendly web UI for browsing
|
||||
- Allows custom JavaScript injection into any proxied website
|
||||
- Supports both direct browser configuration and web UI-based browsing
|
||||
- Helps with debugging and manipulating HTTP/3 traffic
|
||||
|
||||
- Go 1.21+
|
||||
- OpenSSL (for certificate generation)
|
||||
- Docker and Docker Compose (optional)
|
||||
## 📦 Installation
|
||||
|
||||
## 🚀 Quick Start
|
||||
### Prerequisites
|
||||
|
||||
### 1. Clone the repository
|
||||
- Go 1.16 or higher
|
||||
- TLS certificates (for HTTPS and HTTP/3)
|
||||
|
||||
### Building from source
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/yourusername/fuckhttp3.git
|
||||
|
||||
# Navigate to the project directory
|
||||
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
|
||||
# Build the binary
|
||||
go build -o fuckhttp3 .
|
||||
```
|
||||
|
||||
## 🎮 Usage
|
||||
|
||||
### Generating certificates
|
||||
|
||||
For local development, generate self-signed certificates:
|
||||
|
||||
```bash
|
||||
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes
|
||||
```
|
||||
|
||||
### Running the proxy
|
||||
|
||||
Basic usage:
|
||||
|
||||
```bash
|
||||
./fuckhttp3 --addr=localhost:8443 --cert=cert.pem --key=key.pem --webui --verbose
|
||||
```
|
||||
|
||||
### 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` |
|
||||
| `--cert` | Certificate file path | `cert.pem` |
|
||||
| `--key` | Private key file path | `key.pem` |
|
||||
| `--target` | Target address to proxy to | (empty = forward proxy) |
|
||||
| `--webui` | Enable web UI for user-specified proxy targets | `false` |
|
||||
| `--verbose` | Enable verbose logging | `false` |
|
||||
|
||||
### 🔄 Forward Proxy Mode
|
||||
## ✨ Features
|
||||
|
||||
When the `--target` flag is not provided, operates in forward proxy mode:
|
||||
### 🌐 Web UI
|
||||
|
||||
```bash
|
||||
./fuckhttp3 --addr=localhost:8443 --cert=cert.pem --key=key.pem
|
||||
When running with the `--webui` flag, access the web interface by navigating to the proxy address:
|
||||
|
||||
```
|
||||
https://localhost:8443/
|
||||
```
|
||||
|
||||
Configure your browser to use `localhost:8443` as an HTTPS proxy.
|
||||
The web UI provides:
|
||||
|
||||
### ↪️ Reverse Proxy Mode
|
||||
- A URL input field for browsing through the proxy
|
||||
- JavaScript injection capability
|
||||
- Example scripts for common tasks
|
||||
|
||||
With the `--target` flag, operates in reverse proxy mode:
|
||||
### 💉 JavaScript Injection
|
||||
|
||||
```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
|
||||
```
|
||||
The proxy can inject custom JavaScript into every web page it processes:
|
||||
|
||||
Access at `https://localhost:8443` to reach the target.
|
||||
1. Write or paste your JavaScript code in the web UI
|
||||
2. Save the script and enable injection
|
||||
3. Browse websites with your script automatically injected
|
||||
|
||||
## 🧪 Testing the Proxy
|
||||
#### Example Scripts
|
||||
|
||||
Test with curl (if HTTP/3 support is available):
|
||||
The UI includes example scripts for:
|
||||
|
||||
```bash
|
||||
curl --http3 https://localhost:8443 -k
|
||||
```
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| Dark Mode | Apply dark theme to any website |
|
||||
| Ad Blocker | Hide common ad elements |
|
||||
| Image Highlighter | Add borders to all images |
|
||||
|
||||
> Note: The `-k` flag bypasses certificate validation for self-signed certificates.
|
||||
### 🔄 Proxy Modes
|
||||
|
||||
### 🌐 Browser Testing
|
||||
FuckHTTP3 operates in two modes:
|
||||
|
||||
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
|
||||
| Mode | Description |
|
||||
|------|-------------|
|
||||
| **Forward Proxy** | Standard proxy that browsers can use directly |
|
||||
| **Web UI Mode** | Web interface for browsing through the proxy |
|
||||
|
||||
## 🔒 Client Configuration
|
||||
## 🔧 Browser Configuration
|
||||
|
||||
### Browser Support
|
||||
To use FuckHTTP3 as a system-wide proxy:
|
||||
|
||||
Enable HTTP/3 in your browser:
|
||||
### Firefox
|
||||
|
||||
#### Chrome
|
||||
- Open `chrome://flags/`
|
||||
- Search for "HTTP/3"
|
||||
- Enable "Experimental QUIC protocol"
|
||||
- Restart Chrome
|
||||
1. Open Settings
|
||||
2. Search for "proxy"
|
||||
3. Click "Settings" in the Connection Settings section
|
||||
4. Select "Manual proxy configuration"
|
||||
5. Enter your proxy address and port
|
||||
6. Check "Also use this proxy for HTTPS"
|
||||
|
||||
#### Firefox
|
||||
- Open `about:config`
|
||||
- Search for "network.http.http3.enabled"
|
||||
- Set to `true`
|
||||
- Restart Firefox
|
||||
### Chrome/Chromium
|
||||
|
||||
## ⚡ 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
|
||||
> ⚠️ Chrome doesn't natively support HTTP/3 proxies. Use the web UI mode instead.
|
||||
|
||||
## 🔍 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 |
|
||||
### Script Injection Issues
|
||||
|
||||
## 👥 Contributing
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| Scripts not running | Ensure "Enable script injection" is checked |
|
||||
| No confirmation | Verify script was saved successfully |
|
||||
| Script blocked | Some sites use CSP which may block injected scripts |
|
||||
| Testing | Try a simpler website like example.com first |
|
||||
| Errors | Check browser console for any error messages |
|
||||
|
||||
Contributions welcome! Submit a Pull Request to improve FuckHTTP3.
|
||||
### Certificate Warnings
|
||||
|
||||
| Environment | Solution |
|
||||
|-------------|----------|
|
||||
| Development | Add self-signed certificate to browser's trust store |
|
||||
| Production | Use a properly signed certificate from a trusted CA |
|
||||
|
||||
## ⚠️ Limitations
|
||||
|
||||
- Some websites may block proxy connections
|
||||
- Content Security Policy might prevent script injection
|
||||
- Complex single-page applications may require more advanced injection techniques
|
||||
|
||||
## 📜 License
|
||||
|
||||
This project is licensed under the SuperNets License.
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- Based on the quic-go library
|
||||
- Inspired by other Go proxy implementations
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
|
||||
---
|
||||
|
||||
**Note**: Experimental implementation which may not support all HTTP/3 features. Production use at your own risk.
|
||||
**Note**: Use responsibly and respect website terms of service when injecting scripts.
|
@ -9,12 +9,14 @@ services:
|
||||
volumes:
|
||||
- ./cert.pem:/app/cert.pem
|
||||
- ./key.pem:/app/key.pem
|
||||
- ./templates:/app/templates
|
||||
command: [
|
||||
"fuckhttp3",
|
||||
"--addr=0.0.0.0:8443",
|
||||
"--cert=/app/cert.pem",
|
||||
"--key=/app/key.pem",
|
||||
"--verbose"
|
||||
"--verbose",
|
||||
"--webui"
|
||||
]
|
||||
# Add target parameter if you want to use as reverse proxy
|
||||
# Add target parameter if you want to use as reverse proxy with fixed target
|
||||
# "--target=example.com:443"
|
929
main.go
929
main.go
@ -3,17 +3,22 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
@ -26,6 +31,7 @@ var (
|
||||
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)")
|
||||
webUI = flag.Bool("webui", false, "Enable web UI for user-specified proxy targets")
|
||||
)
|
||||
|
||||
// List of hop-by-hop headers to be removed when proxying
|
||||
@ -40,6 +46,11 @@ var hopByHopHeaders = []string{
|
||||
"Upgrade",
|
||||
}
|
||||
|
||||
const (
|
||||
scriptFileName = "custom_script.js"
|
||||
scriptConfigFileName = "script_config.json"
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
@ -76,6 +87,13 @@ func main() {
|
||||
logger.Fatalf("Invalid target URL: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for templates directory when webUI is enabled
|
||||
if *webUI {
|
||||
if _, err := os.Stat("templates/index.html"); os.IsNotExist(err) {
|
||||
logger.Fatalf("templates/index.html not found, required for web UI mode")
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new proxy server
|
||||
proxyServer := &ProxyServer{
|
||||
@ -83,6 +101,17 @@ func main() {
|
||||
targetAddr: *targetAddr,
|
||||
clients: make(map[string]*http3.RoundTripper),
|
||||
clientsMu: &sync.Mutex{},
|
||||
webUI: *webUI,
|
||||
templates: template.Must(template.ParseGlob("templates/*.html")),
|
||||
iframeMode: true, // Enable iframe mode by default
|
||||
customScript: "", // Start with empty script
|
||||
scriptEnabled: false, // Disabled by default
|
||||
scriptMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
// Load script from disk
|
||||
if err := proxyServer.loadScriptFromDisk(); err != nil {
|
||||
logger.Printf("Error loading script from disk: %v", err)
|
||||
}
|
||||
|
||||
// Extract port for Alt-Svc headers
|
||||
@ -191,6 +220,12 @@ type ProxyServer struct {
|
||||
targetAddr string
|
||||
clients map[string]*http3.RoundTripper
|
||||
clientsMu *sync.Mutex
|
||||
webUI bool
|
||||
templates *template.Template
|
||||
iframeMode bool // Whether to use iframe mode for the navbar
|
||||
customScript string
|
||||
scriptEnabled bool
|
||||
scriptMutex sync.RWMutex
|
||||
}
|
||||
|
||||
// ServeHTTP handles the HTTP requests
|
||||
@ -202,6 +237,26 @@ func (p *ProxyServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
p.logger.Printf("Received request: %s %s %s", r.Method, r.URL, r.Proto)
|
||||
}
|
||||
|
||||
// Check for web UI mode
|
||||
if p.webUI {
|
||||
if r.URL.Path == "/" || r.URL.Path == "" {
|
||||
p.handleHome(w, r)
|
||||
return
|
||||
} else if r.URL.Path == "/proxy" && r.Method == http.MethodPost {
|
||||
p.handleProxyRequest(w, r)
|
||||
return
|
||||
} else if r.URL.Path == "/proxified" {
|
||||
p.handleProxifiedRequest(w, r)
|
||||
return
|
||||
} else if r.URL.Path == "/navbar" {
|
||||
p.handleNavbarFrame(w, r)
|
||||
return
|
||||
} else if r.URL.Path == "/save-script" {
|
||||
p.handleSaveScript(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we're operating as a reverse proxy (with fixed target) or forward proxy
|
||||
if p.targetAddr != "" {
|
||||
// Reverse proxy mode
|
||||
@ -467,4 +522,876 @@ func (p *ProxyServer) isHopByHopHeader(header string) bool {
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// handleHome renders the web UI
|
||||
func (p *ProxyServer) handleHome(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
p.scriptMutex.RLock()
|
||||
scriptEnabled := p.scriptEnabled
|
||||
customScript := p.customScript
|
||||
p.scriptMutex.RUnlock()
|
||||
|
||||
// Create template data with all needed fields
|
||||
data := struct {
|
||||
WebUIEnabled bool
|
||||
CustomScript string
|
||||
ScriptEnabled bool
|
||||
Error string
|
||||
URL string
|
||||
}{
|
||||
WebUIEnabled: *webUI,
|
||||
CustomScript: customScript,
|
||||
ScriptEnabled: scriptEnabled,
|
||||
Error: "",
|
||||
URL: "",
|
||||
}
|
||||
|
||||
// Render template
|
||||
err := p.templates.ExecuteTemplate(w, "index.html", data)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
p.logger.Printf("Error rendering template: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// handleProxyRequest processes the form submission from the web UI
|
||||
func (p *ProxyServer) handleProxyRequest(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
p.logger.Printf("Error parsing form: %v", err)
|
||||
http.Error(w, "Bad Request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
targetURL := r.FormValue("url")
|
||||
if targetURL == "" {
|
||||
// Handle empty URL
|
||||
data := struct {
|
||||
URL string
|
||||
Error string
|
||||
}{
|
||||
URL: "",
|
||||
Error: "Please enter a URL",
|
||||
}
|
||||
|
||||
err := p.templates.ExecuteTemplate(w, "index.html", data)
|
||||
if err != nil {
|
||||
p.logger.Printf("Error rendering template: %v", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Normalize URL
|
||||
if !strings.HasPrefix(targetURL, "http://") && !strings.HasPrefix(targetURL, "https://") {
|
||||
targetURL = "https://" + targetURL
|
||||
}
|
||||
|
||||
// Validate URL
|
||||
parsedURL, err := url.Parse(targetURL)
|
||||
if err != nil {
|
||||
data := struct {
|
||||
URL string
|
||||
Error string
|
||||
}{
|
||||
URL: targetURL,
|
||||
Error: "Invalid URL: " + err.Error(),
|
||||
}
|
||||
|
||||
err := p.templates.ExecuteTemplate(w, "index.html", data)
|
||||
if err != nil {
|
||||
p.logger.Printf("Error rendering template: %v", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Store the target URL in a query parameter and redirect
|
||||
proxifiedURL := fmt.Sprintf("/proxified?url=%s", url.QueryEscape(parsedURL.String()))
|
||||
http.Redirect(w, r, proxifiedURL, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// handleDataURL handles data URLs
|
||||
func (p *ProxyServer) handleDataURL(w http.ResponseWriter, r *http.Request, targetURL string) {
|
||||
// Parse the data URL
|
||||
if strings.HasPrefix(targetURL, "data:") {
|
||||
// Extract content type and data
|
||||
parts := strings.SplitN(targetURL[5:], ",", 2)
|
||||
if len(parts) != 2 {
|
||||
http.Error(w, "Invalid data URL format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
contentTypeInfo := parts[0]
|
||||
data := parts[1]
|
||||
|
||||
// Set content type
|
||||
if contentTypeInfo != "" {
|
||||
if strings.HasSuffix(contentTypeInfo, ";base64") {
|
||||
w.Header().Set("Content-Type", strings.TrimSuffix(contentTypeInfo, ";base64"))
|
||||
// Decode base64
|
||||
decoded, err := base64.StdEncoding.DecodeString(data)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid base64 encoding in data URL", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.Write(decoded)
|
||||
} else {
|
||||
w.Header().Set("Content-Type", contentTypeInfo)
|
||||
// URL decode the data
|
||||
decoded, err := url.QueryUnescape(data)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid URL encoding in data URL", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.Write([]byte(decoded))
|
||||
}
|
||||
} else {
|
||||
// Default to text/plain
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
decoded, _ := url.QueryUnescape(data)
|
||||
w.Write([]byte(decoded))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Not a data URL, return error
|
||||
http.Error(w, "Not a data URL", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// handleProxifiedRequest fetches and processes the target URL content
|
||||
func (p *ProxyServer) handleProxifiedRequest(w http.ResponseWriter, r *http.Request) {
|
||||
// Extract the URL from the query parameter
|
||||
targetURL := r.URL.Query().Get("url")
|
||||
if targetURL == "" {
|
||||
http.Error(w, "Missing URL parameter", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle data: URLs specially
|
||||
if strings.HasPrefix(targetURL, "data:") {
|
||||
p.handleDataURL(w, r, targetURL)
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure URL is absolute
|
||||
if !strings.HasPrefix(targetURL, "http://") && !strings.HasPrefix(targetURL, "https://") {
|
||||
http.Error(w, "Request URL must be absolute in forward proxy mode", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
parsedURL, err := url.Parse(targetURL)
|
||||
if err != nil {
|
||||
p.logger.Printf("Error parsing URL: %v", err)
|
||||
http.Error(w, "Invalid URL", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if *verbose {
|
||||
p.logger.Printf("Proxifying request to: %s", targetURL)
|
||||
}
|
||||
|
||||
// Get or create a RoundTripper for the target host
|
||||
roundTripper := p.getRoundTripper(parsedURL.Host)
|
||||
|
||||
// Create a new client
|
||||
client := &http.Client{
|
||||
Transport: roundTripper,
|
||||
Timeout: 30 * time.Second,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
// Instead of following redirects, we'll return the redirect URL
|
||||
// so we can process it through our proxy
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
// Create a new request
|
||||
outReq, err := http.NewRequestWithContext(r.Context(), http.MethodGet, targetURL, nil)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
p.logger.Printf("Error creating request: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Copy original request headers
|
||||
p.copyHeaders(outReq.Header, r.Header)
|
||||
|
||||
// Add common browser headers if not present
|
||||
if outReq.Header.Get("User-Agent") == "" {
|
||||
outReq.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
|
||||
}
|
||||
if outReq.Header.Get("Accept") == "" {
|
||||
outReq.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
// Handle redirects manually
|
||||
if resp.StatusCode >= 300 && resp.StatusCode < 400 {
|
||||
redirectURL := resp.Header.Get("Location")
|
||||
if redirectURL != "" {
|
||||
// Make sure the redirect URL is absolute
|
||||
redirectURL, err = p.makeAbsoluteURL(redirectURL, parsedURL)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Redirect through our proxy
|
||||
proxifiedRedirect := fmt.Sprintf("/proxified?url=%s", url.QueryEscape(redirectURL))
|
||||
http.Redirect(w, r, proxifiedRedirect, resp.StatusCode)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Copy the response headers
|
||||
p.copyHeaders(w.Header(), resp.Header)
|
||||
|
||||
// Modify response headers to allow framing and inlining
|
||||
p.modifyResponseHeaders(w.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))
|
||||
|
||||
// Set appropriate Content-Type headers
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if strings.Contains(r.URL.Path, ".js") && contentType == "text/plain" {
|
||||
// Fix JavaScript MIME type
|
||||
w.Header().Set("Content-Type", "application/javascript")
|
||||
} else if strings.Contains(r.URL.Path, ".css") && contentType == "text/plain" {
|
||||
// Fix CSS MIME type
|
||||
w.Header().Set("Content-Type", "text/css")
|
||||
} else {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
}
|
||||
|
||||
// Determine content type
|
||||
contentType = resp.Header.Get("Content-Type")
|
||||
isHTML := strings.Contains(contentType, "text/html") || strings.Contains(contentType, "application/xhtml+xml")
|
||||
|
||||
if isHTML {
|
||||
// Process HTML content to rewrite links
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
p.logger.Printf("Error reading response body: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Rewrite URLs in the HTML content
|
||||
processedHTML := p.processHTML(bodyBytes, parsedURL.String())
|
||||
|
||||
// Add a navigation bar at the top
|
||||
processedHTML = p.addProxyNavBar(processedHTML, parsedURL.String())
|
||||
|
||||
// Set the processed content type and length
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(processedHTML)))
|
||||
|
||||
// Write status code and content
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
w.Write(processedHTML)
|
||||
} else if strings.Contains(contentType, "javascript") {
|
||||
// If JavaScript, rewrite URLs
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
p.logger.Printf("Error reading response body: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
rewrittenBody := p.rewriteJavaScript(bodyBytes, targetURL)
|
||||
|
||||
// Set the processed content type and length
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(rewrittenBody)))
|
||||
|
||||
// Write status code and content
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
w.Write(rewrittenBody)
|
||||
} else {
|
||||
// For non-HTML content, pass through directly
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
if _, err := io.Copy(w, resp.Body); err != nil {
|
||||
p.logger.Printf("Error copying response body: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processHTML rewrites URLs in HTML content and injects JavaScript
|
||||
func (p *ProxyServer) processHTML(htmlBytes []byte, baseURLStr string) []byte {
|
||||
// Convert HTML to string for easier manipulation
|
||||
htmlStr := string(htmlBytes)
|
||||
|
||||
p.scriptMutex.RLock()
|
||||
scriptEnabled := p.scriptEnabled
|
||||
customScript := p.customScript
|
||||
p.scriptMutex.RUnlock()
|
||||
|
||||
// If script injection is enabled, inject the custom script into the HTML
|
||||
if scriptEnabled && customScript != "" {
|
||||
p.logger.Printf("Injecting script into response for URL: %s", baseURLStr)
|
||||
|
||||
// Create script tag with the custom script
|
||||
scriptTag := fmt.Sprintf("<script type=\"text/javascript\">\n// FuckHTTP3 Injected Script\n%s\n</script>", customScript)
|
||||
|
||||
// Try to insert it before </body> first
|
||||
if strings.Contains(htmlStr, "</body>") {
|
||||
htmlStr = strings.Replace(htmlStr, "</body>", scriptTag+"\n</body>", 1)
|
||||
p.logger.Printf("Script injected before </body> tag")
|
||||
} else if strings.Contains(htmlStr, "</html>") {
|
||||
// If no </body>, try before </html>
|
||||
htmlStr = strings.Replace(htmlStr, "</html>", scriptTag+"\n</html>", 1)
|
||||
p.logger.Printf("Script injected before </html> tag")
|
||||
} else {
|
||||
// If neither tag exists, append it to the end
|
||||
htmlStr += "\n" + scriptTag
|
||||
p.logger.Printf("Script appended to the end of HTML")
|
||||
}
|
||||
} else {
|
||||
if !scriptEnabled {
|
||||
p.logger.Printf("Script injection disabled")
|
||||
} else if customScript == "" {
|
||||
p.logger.Printf("No custom script to inject")
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the base URL string into a URL object
|
||||
baseURL, err := url.Parse(baseURLStr)
|
||||
if err != nil {
|
||||
p.logger.Printf("Error parsing base URL %s: %v", baseURLStr, err)
|
||||
baseURL, _ = url.Parse("https://example.com")
|
||||
}
|
||||
|
||||
// Process base URL if present
|
||||
baseTagRegex := regexp.MustCompile(`<base[^>]+href=["']([^"']+)["'][^>]*>`)
|
||||
baseMatch := baseTagRegex.FindStringSubmatch(htmlStr)
|
||||
|
||||
if len(baseMatch) > 1 {
|
||||
// There's a base tag, use it for resolving relative URLs
|
||||
baseHref := baseMatch[1]
|
||||
newBaseURL, err := url.Parse(baseHref)
|
||||
if err == nil {
|
||||
baseURL = baseURL.ResolveReference(newBaseURL)
|
||||
}
|
||||
}
|
||||
|
||||
// Rewrite hyperlinks
|
||||
aTagRegex := regexp.MustCompile(`<a[^>]+href=["']([^"']+)["'][^>]*>`)
|
||||
htmlStr = aTagRegex.ReplaceAllStringFunc(htmlStr, func(match string) string {
|
||||
submatches := aTagRegex.FindStringSubmatch(match)
|
||||
if len(submatches) < 2 {
|
||||
return match
|
||||
}
|
||||
|
||||
href := submatches[1]
|
||||
|
||||
// Skip javascript: and mailto: links
|
||||
if strings.HasPrefix(href, "javascript:") || strings.HasPrefix(href, "mailto:") {
|
||||
return match
|
||||
}
|
||||
|
||||
// Construct absolute URL
|
||||
absoluteURL, err := p.makeAbsoluteURL(href, baseURL)
|
||||
if err != nil {
|
||||
return match
|
||||
}
|
||||
|
||||
// Replace the href with our proxified URL
|
||||
proxifiedURL := fmt.Sprintf("/proxified?url=%s", url.QueryEscape(absoluteURL))
|
||||
return strings.Replace(match, submatches[1], proxifiedURL, 1)
|
||||
})
|
||||
|
||||
// Rewrite image sources
|
||||
imgTagRegex := regexp.MustCompile(`<img[^>]+src=["']([^"']+)["'][^>]*>`)
|
||||
htmlStr = imgTagRegex.ReplaceAllStringFunc(htmlStr, func(match string) string {
|
||||
submatches := imgTagRegex.FindStringSubmatch(match)
|
||||
if len(submatches) < 2 {
|
||||
return match
|
||||
}
|
||||
|
||||
src := submatches[1]
|
||||
|
||||
// Construct absolute URL
|
||||
absoluteURL, err := p.makeAbsoluteURL(src, baseURL)
|
||||
if err != nil {
|
||||
return match
|
||||
}
|
||||
|
||||
// Replace the src with our proxified URL
|
||||
proxifiedURL := fmt.Sprintf("/proxified?url=%s", url.QueryEscape(absoluteURL))
|
||||
return strings.Replace(match, submatches[1], proxifiedURL, 1)
|
||||
})
|
||||
|
||||
// Rewrite CSS links
|
||||
linkTagRegex := regexp.MustCompile(`<link[^>]+href=["']([^"']+)["'][^>]*>`)
|
||||
htmlStr = linkTagRegex.ReplaceAllStringFunc(htmlStr, func(match string) string {
|
||||
submatches := linkTagRegex.FindStringSubmatch(match)
|
||||
if len(submatches) < 2 {
|
||||
return match
|
||||
}
|
||||
|
||||
href := submatches[1]
|
||||
|
||||
// Construct absolute URL
|
||||
absoluteURL, err := p.makeAbsoluteURL(href, baseURL)
|
||||
if err != nil {
|
||||
return match
|
||||
}
|
||||
|
||||
// Replace the href with our proxified URL
|
||||
proxifiedURL := fmt.Sprintf("/proxified?url=%s", url.QueryEscape(absoluteURL))
|
||||
return strings.Replace(match, submatches[1], proxifiedURL, 1)
|
||||
})
|
||||
|
||||
// Rewrite script sources
|
||||
scriptTagRegex := regexp.MustCompile(`<script[^>]+src=["']([^"']+)["'][^>]*>`)
|
||||
htmlStr = scriptTagRegex.ReplaceAllStringFunc(htmlStr, func(match string) string {
|
||||
submatches := scriptTagRegex.FindStringSubmatch(match)
|
||||
if len(submatches) < 2 {
|
||||
return match
|
||||
}
|
||||
|
||||
src := submatches[1]
|
||||
|
||||
// Construct absolute URL
|
||||
absoluteURL, err := p.makeAbsoluteURL(src, baseURL)
|
||||
if err != nil {
|
||||
return match
|
||||
}
|
||||
|
||||
// Replace the src with our proxified URL
|
||||
proxifiedURL := fmt.Sprintf("/proxified?url=%s", url.QueryEscape(absoluteURL))
|
||||
return strings.Replace(match, submatches[1], proxifiedURL, 1)
|
||||
})
|
||||
|
||||
return []byte(htmlStr)
|
||||
}
|
||||
|
||||
// makeAbsoluteURL converts a relative URL to an absolute URL
|
||||
func (p *ProxyServer) makeAbsoluteURL(href string, base *url.URL) (string, error) {
|
||||
// Check if already absolute
|
||||
if strings.HasPrefix(href, "http://") || strings.HasPrefix(href, "https://") {
|
||||
return href, nil
|
||||
}
|
||||
|
||||
// Parse the href
|
||||
relativeURL, err := url.Parse(href)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Resolve against base URL
|
||||
absoluteURL := base.ResolveReference(relativeURL)
|
||||
|
||||
return absoluteURL.String(), nil
|
||||
}
|
||||
|
||||
// addProxyNavBar adds a navigation bar at the top of the HTML content
|
||||
func (p *ProxyServer) addProxyNavBar(html []byte, currentURL string) []byte {
|
||||
// Generate a unique ID for this navbar instance to avoid conflicts
|
||||
navbarID := fmt.Sprintf("fuckhttp3_navbar_%d", time.Now().UnixNano())
|
||||
|
||||
// Create a more secure and isolated navbar with iframe
|
||||
navbar := fmt.Sprintf(`
|
||||
<div id="%s" style="all: initial !important; position: fixed !important; top: 0 !important; left: 0 !important; width: 100%% !important; height: 50px !important; z-index: 2147483647 !important; box-shadow: 0 2px 5px rgba(0,0,0,0.3) !important; background: #333 !important; font-family: Arial, sans-serif !important; color: white !important; display: block !important; border: none !important; margin: 0 !important; padding: 0 !important;">
|
||||
<iframe src="/navbar?url=%s" style="width: 100%% !important; height: 100%% !important; border: none !important; margin: 0 !important; padding: 0 !important;" frameBorder="0"></iframe>
|
||||
</div>
|
||||
<div style="height: 50px !important; width: 100%% !important; display: block !important;"></div>
|
||||
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
// Store the original createElement to prevent hijacking
|
||||
var originalCreateElement = document.createElement;
|
||||
var navbarId = "%s";
|
||||
|
||||
// Check and restore navbar every 100ms
|
||||
setInterval(function() {
|
||||
var navbar = document.getElementById(navbarId);
|
||||
if (!navbar || !navbar.style || navbar.style.display === "none" ||
|
||||
!document.body.contains(navbar) ||
|
||||
navbar.getBoundingClientRect().height < 10) {
|
||||
|
||||
// Create a new navbar if it's been removed or hidden
|
||||
var newNavbar = originalCreateElement.call(document, "div");
|
||||
newNavbar.id = navbarId;
|
||||
newNavbar.style = "all: initial !important; position: fixed !important; top: 0 !important; left: 0 !important; width: 100%% !important; height: 50px !important; z-index: 2147483647 !important; box-shadow: 0 2px 5px rgba(0,0,0,0.3) !important; background: #333 !important; font-family: Arial, sans-serif !important; color: white !important; display: block !important; border: none !important; margin: 0 !important; padding: 0 !important;";
|
||||
|
||||
var iframe = originalCreateElement.call(document, "iframe");
|
||||
iframe.src = "/navbar?url=%s";
|
||||
iframe.style = "width: 100%% !important; height: 100%% !important; border: none !important; margin: 0 !important; padding: 0 !important;";
|
||||
iframe.frameBorder = "0";
|
||||
|
||||
newNavbar.appendChild(iframe);
|
||||
|
||||
// Add to the body
|
||||
document.body.insertBefore(newNavbar, document.body.firstChild);
|
||||
|
||||
// Also ensure spacer exists
|
||||
var spacer = document.querySelector('div[style*="height: 50px"]');
|
||||
if (!spacer) {
|
||||
var spacerDiv = originalCreateElement.call(document, "div");
|
||||
spacerDiv.style = "height: 50px !important; width: 100%% !important; display: block !important;";
|
||||
document.body.insertBefore(spacerDiv, newNavbar.nextSibling);
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// Prevent removal by MutationObserver
|
||||
var observer = new MutationObserver(function(mutations) {
|
||||
mutations.forEach(function(mutation) {
|
||||
if (mutation.type === 'childList') {
|
||||
var navbar = document.getElementById(navbarId);
|
||||
if (!navbar || !document.body.contains(navbar)) {
|
||||
observer.disconnect();
|
||||
// Re-add the navbar
|
||||
var newNavbar = originalCreateElement.call(document, "div");
|
||||
// ... (same code as above to recreate the navbar)
|
||||
|
||||
// Restart observation
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Start observing
|
||||
if (document.body) {
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
`, navbarID, url.QueryEscape(currentURL), navbarID, url.QueryEscape(currentURL))
|
||||
|
||||
// Find the opening html tag to place our additions right after it
|
||||
htmlRegex := regexp.MustCompile(`(?i)<html[^>]*>`)
|
||||
htmlMatch := htmlRegex.FindIndex(html)
|
||||
|
||||
if len(htmlMatch) > 0 {
|
||||
// Insert after the html tag
|
||||
insertPos := htmlMatch[1]
|
||||
result := append(html[:insertPos], append([]byte(navbar), html[insertPos:]...)...)
|
||||
return result
|
||||
}
|
||||
|
||||
// Find the body tag if html tag wasn't found
|
||||
bodyRegex := regexp.MustCompile(`(?i)<body[^>]*>`)
|
||||
bodyMatch := bodyRegex.FindIndex(html)
|
||||
|
||||
if len(bodyMatch) > 0 {
|
||||
// Insert after the body tag
|
||||
insertPos := bodyMatch[1]
|
||||
result := append(html[:insertPos], append([]byte(navbar), html[insertPos:]...)...)
|
||||
return result
|
||||
}
|
||||
|
||||
// Fallback: if no body tag found, add to the beginning
|
||||
result := append([]byte(navbar), html...)
|
||||
return result
|
||||
}
|
||||
|
||||
// modifyResponseHeaders modifies the response headers to allow framing and inlining
|
||||
func (p *ProxyServer) modifyResponseHeaders(header http.Header) {
|
||||
// Modify CSP header if present
|
||||
csp := header.Get("Content-Security-Policy")
|
||||
if csp != "" {
|
||||
// Modify the CSP to allow our content
|
||||
cspParts := strings.Split(csp, ";")
|
||||
|
||||
// Create a new CSP with modified directives
|
||||
var newCspParts []string
|
||||
frameAncestorsFound := false
|
||||
|
||||
for _, part := range cspParts {
|
||||
part = strings.TrimSpace(part)
|
||||
|
||||
// Modify frame-ancestors to allow our domain
|
||||
if strings.HasPrefix(part, "frame-ancestors") {
|
||||
newCspParts = append(newCspParts, "frame-ancestors 'self'")
|
||||
frameAncestorsFound = true
|
||||
} else if strings.HasPrefix(part, "default-src") {
|
||||
newCspParts = append(newCspParts, part+" 'unsafe-inline'")
|
||||
} else if strings.HasPrefix(part, "script-src") {
|
||||
newCspParts = append(newCspParts, part+" 'unsafe-inline'")
|
||||
} else if strings.HasPrefix(part, "style-src") {
|
||||
newCspParts = append(newCspParts, part+" 'unsafe-inline'")
|
||||
} else {
|
||||
newCspParts = append(newCspParts, part)
|
||||
}
|
||||
}
|
||||
|
||||
// Add frame-ancestors if not found
|
||||
if !frameAncestorsFound {
|
||||
newCspParts = append(newCspParts, "frame-ancestors 'self'")
|
||||
}
|
||||
|
||||
// Set the new CSP
|
||||
header.Set("Content-Security-Policy", strings.Join(newCspParts, "; "))
|
||||
}
|
||||
|
||||
// Remove headers that might cause issues
|
||||
header.Del("X-Frame-Options") // This prevents framing
|
||||
}
|
||||
|
||||
// Add a new handler for the navbar iframe
|
||||
func (p *ProxyServer) handleNavbarFrame(w http.ResponseWriter, r *http.Request) {
|
||||
currentURL := r.URL.Query().Get("url")
|
||||
|
||||
// Set headers to prevent caching and ensure proper content type
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Header().Set("Expires", "0")
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'self'; style-src 'unsafe-inline'; script-src 'unsafe-inline'")
|
||||
|
||||
// Create a minimal HTML page with just the navbar
|
||||
navbar := fmt.Sprintf(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #333;
|
||||
color: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
.navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
|
||||
height: 30px;
|
||||
}
|
||||
.logo {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
margin-right: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.url-form {
|
||||
flex-grow: 1;
|
||||
margin: 0 10px;
|
||||
}
|
||||
.form-container {
|
||||
display: flex;
|
||||
height: 30px;
|
||||
}
|
||||
input[type="text"] {
|
||||
flex-grow: 1;
|
||||
padding: 5px;
|
||||
border: none;
|
||||
border-radius: 3px 0 0 3px;
|
||||
margin: 0;
|
||||
height: 20px;
|
||||
}
|
||||
button {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
border-radius: 0 3px 3px 0;
|
||||
cursor: pointer;
|
||||
height: 30px;
|
||||
}
|
||||
.home-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.home-link a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
padding: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="navbar">
|
||||
<div class="logo">FuckHTTP3 Proxy</div>
|
||||
<div class="url-form">
|
||||
<form action="/proxy" method="post" target="_parent" class="form-container">
|
||||
<input type="text" name="url" value="%s" placeholder="https://example.com">
|
||||
<button type="submit">Go</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="home-link">
|
||||
<a href="/" target="_parent">Home</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`, currentURL)
|
||||
|
||||
w.Write([]byte(navbar))
|
||||
}
|
||||
|
||||
// Add this function to rewrite URLs in JavaScript
|
||||
func (p *ProxyServer) rewriteJavaScript(js []byte, baseURL string) []byte {
|
||||
// Rewrite JavaScript URLs to ensure they go through our proxy
|
||||
// This is a simplified approach - a more comprehensive one would use proper JS parsing
|
||||
|
||||
// Regular expressions to find URLs in JavaScript
|
||||
urlPatterns := []*regexp.Regexp{
|
||||
regexp.MustCompile(`(['"])https?://[^'"]+(['"])`),
|
||||
regexp.MustCompile(`location\.href\s*=\s*(['"])https?://[^'"]+(['"])`),
|
||||
regexp.MustCompile(`location\.replace\s*\(\s*(['"])https?://[^'"]+(['"])\s*\)`),
|
||||
regexp.MustCompile(`window\.open\s*\(\s*(['"])https?://[^'"]+(['"])`),
|
||||
}
|
||||
|
||||
result := string(js)
|
||||
|
||||
for _, pattern := range urlPatterns {
|
||||
result = pattern.ReplaceAllStringFunc(result, func(match string) string {
|
||||
// Extract the URL from the match
|
||||
urlMatch := regexp.MustCompile(`https?://[^'"]+`).FindString(match)
|
||||
if urlMatch != "" {
|
||||
// Replace the URL with a proxied one, but keep the quotes and other parts
|
||||
return strings.Replace(match, urlMatch, "/proxified?url="+urlMatch, 1)
|
||||
}
|
||||
return match
|
||||
})
|
||||
}
|
||||
|
||||
return []byte(result)
|
||||
}
|
||||
|
||||
// handleSaveScript handles the form submission for saving a custom script
|
||||
func (p *ProxyServer) handleSaveScript(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
p.sendJSONError(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse multipart form with generous max memory
|
||||
if err := r.ParseMultipartForm(10 << 20); err != nil {
|
||||
// Fall back to regular form parsing if not multipart
|
||||
if err := r.ParseForm(); err != nil {
|
||||
p.logger.Printf("Error parsing form: %v", err)
|
||||
p.sendJSONError(w, "Failed to parse form", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Get the script from form
|
||||
script := r.FormValue("customScript")
|
||||
p.logger.Printf("Received script length: %d chars", len(script))
|
||||
|
||||
// Log all form data for debugging
|
||||
p.logger.Printf("All form values: %+v", r.Form)
|
||||
p.logger.Printf("All POST form values: %+v", r.PostForm)
|
||||
|
||||
// Check if enableScript exists in form
|
||||
_, enabledExists := r.Form["enableScript"]
|
||||
enabled := enabledExists
|
||||
|
||||
p.logger.Printf("Script enabled checkbox present: %v", enabledExists)
|
||||
p.logger.Printf("Setting script enabled to: %v", enabled)
|
||||
|
||||
// Update the script in the proxy server
|
||||
p.scriptMutex.Lock()
|
||||
p.customScript = script
|
||||
p.scriptEnabled = enabled
|
||||
p.scriptMutex.Unlock()
|
||||
|
||||
// Save to disk
|
||||
if err := p.saveScriptToDisk(); err != nil {
|
||||
p.logger.Printf("Error saving script to disk: %v", err)
|
||||
p.sendJSONError(w, "Failed to save script to disk", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Return a JSON response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(fmt.Sprintf(`{"success": true, "enabled": %t}`, enabled)))
|
||||
}
|
||||
|
||||
// Add this error handler method
|
||||
func (p *ProxyServer) sendJSONError(w http.ResponseWriter, err string, code int) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
w.Write([]byte(fmt.Sprintf(`{"success": false, "error": "%s"}`, err)))
|
||||
}
|
||||
|
||||
// Add these methods to save/load scripts
|
||||
func (p *ProxyServer) saveScriptToDisk() error {
|
||||
// Create scripts directory if it doesn't exist
|
||||
scriptsDir := "scripts"
|
||||
if _, err := os.Stat(scriptsDir); os.IsNotExist(err) {
|
||||
if err := os.Mkdir(scriptsDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the script
|
||||
scriptPath := filepath.Join(scriptsDir, scriptFileName)
|
||||
if err := ioutil.WriteFile(scriptPath, []byte(p.customScript), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save config
|
||||
configPath := filepath.Join(scriptsDir, scriptConfigFileName)
|
||||
configJSON := fmt.Sprintf(`{"enabled": %t}`, p.scriptEnabled)
|
||||
if err := ioutil.WriteFile(configPath, []byte(configJSON), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ProxyServer) loadScriptFromDisk() error {
|
||||
scriptsDir := "scripts"
|
||||
scriptPath := filepath.Join(scriptsDir, scriptFileName)
|
||||
configPath := filepath.Join(scriptsDir, scriptConfigFileName)
|
||||
|
||||
// Check if files exist
|
||||
if _, err := os.Stat(scriptPath); os.IsNotExist(err) {
|
||||
// No script saved yet
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load script
|
||||
scriptBytes, err := ioutil.ReadFile(scriptPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.customScript = string(scriptBytes)
|
||||
|
||||
// Load config if exists
|
||||
if _, err := os.Stat(configPath); !os.IsNotExist(err) {
|
||||
configBytes, err := ioutil.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configStr := string(configBytes)
|
||||
p.logger.Printf("Loaded script config: %s", configStr)
|
||||
|
||||
// More robust parsing
|
||||
if strings.Contains(configStr, `"enabled": true`) ||
|
||||
strings.Contains(configStr, `"enabled":true`) {
|
||||
p.scriptEnabled = true
|
||||
p.logger.Printf("Script injection enabled from config")
|
||||
} else {
|
||||
p.scriptEnabled = false
|
||||
p.logger.Printf("Script injection disabled from config")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
6
scripts/custom_script.js
Normal file
6
scripts/custom_script.js
Normal file
@ -0,0 +1,6 @@
|
||||
// Dark mode
|
||||
document.body.style.backgroundColor = '#222';
|
||||
document.body.style.color = '#eee';
|
||||
document.querySelectorAll('a').forEach(a => {
|
||||
a.style.color = '#5af';
|
||||
});
|
1
scripts/script_config.json
Normal file
1
scripts/script_config.json
Normal file
@ -0,0 +1 @@
|
||||
{"enabled": true}
|
107
setup-templates.sh
Normal file
107
setup-templates.sh
Normal file
@ -0,0 +1,107 @@
|
||||
#!/bin/bash
|
||||
# Setup script for FuckHTTP3 web proxy
|
||||
|
||||
# Create templates directory if it doesn't exist
|
||||
mkdir -p templates
|
||||
|
||||
# Check if index.html exists in templates directory
|
||||
if [ ! -f templates/index.html ]; then
|
||||
echo "Creating templates/index.html"
|
||||
cat > templates/index.html << 'EOL'
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>FuckHTTP3 Proxy</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
padding: 30px;
|
||||
width: 80%;
|
||||
max-width: 600px;
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
button {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
.error {
|
||||
color: #f44336;
|
||||
margin-top: 20px;
|
||||
}
|
||||
footer {
|
||||
margin-top: 20px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>FuckHTTP3 Web Proxy</h1>
|
||||
<p>Enter a URL below to browse through the HTTP/3 proxy</p>
|
||||
|
||||
{{if .Error}}
|
||||
<div class="error">{{.Error}}</div>
|
||||
{{end}}
|
||||
|
||||
<form action="/proxy" method="post">
|
||||
<div class="form-group">
|
||||
<input type="text" name="url" placeholder="https://example.com"
|
||||
value="{{.URL}}" required autofocus>
|
||||
</div>
|
||||
<button type="submit">Browse</button>
|
||||
</form>
|
||||
|
||||
<footer>
|
||||
Powered by FuckHTTP3 Proxy - Using HTTP/3 with QUIC
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
EOL
|
||||
fi
|
||||
|
||||
echo "Template setup complete!"
|
||||
chmod +x setup-templates.sh
|
259
templates/index.html
Normal file
259
templates/index.html
Normal file
@ -0,0 +1,259 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>FuckHTTP3 Proxy</title>
|
||||
<style>
|
||||
/* Improved Dark Mode Styles */
|
||||
:root {
|
||||
--bg-color: #171717;
|
||||
--container-bg: #242424;
|
||||
--accent-color: #4d8bff;
|
||||
--text-color: #e0e0e0;
|
||||
--border-color: #3a3a3a;
|
||||
--input-bg: #333;
|
||||
--button-hover: #3d75db;
|
||||
--element-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: var(--container-bg);
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--element-shadow);
|
||||
padding: 30px;
|
||||
width: 80%;
|
||||
max-width: 600px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1, h3, h4 {
|
||||
color: var(--accent-color);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
input[type="text"], textarea {
|
||||
background-color: var(--input-bg);
|
||||
color: var(--text-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
input[type="text"]:focus, textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 0 2px rgba(77, 139, 255, 0.2);
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: var(--accent-color);
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 10px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s, transform 0.1s;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--button-hover);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.script-editor-container {
|
||||
background-color: var(--container-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.script-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
#customScript {
|
||||
min-height: 150px;
|
||||
font-family: monospace;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.example-script {
|
||||
background-color: #333;
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-color);
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.example-script:hover {
|
||||
background-color: #444;
|
||||
}
|
||||
|
||||
footer {
|
||||
color: #777;
|
||||
margin-top: 20px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #ff5555;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.checkbox-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin-right: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>FuckHTTP3 Web Proxy</h1>
|
||||
<p>Enter a URL below to browse through the HTTP/3 proxy</p>
|
||||
|
||||
{{if .Error}}
|
||||
<div class="error">{{.Error}}</div>
|
||||
{{end}}
|
||||
|
||||
<form action="/proxy" method="post">
|
||||
<div class="form-group">
|
||||
<input type="text" name="url" placeholder="https://example.com"
|
||||
value="{{.URL}}" required autofocus>
|
||||
</div>
|
||||
<button type="submit">Browse</button>
|
||||
</form>
|
||||
|
||||
<div class="script-editor-container">
|
||||
<h3>Custom JavaScript Injection</h3>
|
||||
<p>Write JavaScript that will be injected into every proxied page:</p>
|
||||
<form id="scriptForm" action="/save-script" method="POST">
|
||||
<div class="script-controls">
|
||||
<button type="submit" id="saveScript">Save Script</button>
|
||||
<div class="checkbox-container">
|
||||
<input type="checkbox" id="enableScript" name="enableScript">
|
||||
<label for="enableScript">Enable script injection</label>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="customScript" name="customScript" rows="10" style="width: 100%; font-family: monospace;">{{.CustomScript}}</textarea>
|
||||
</form>
|
||||
|
||||
<div class="script-examples">
|
||||
<h4>Example Scripts:</h4>
|
||||
<button class="example-script" data-script="// Add a red border to all images
|
||||
document.querySelectorAll('img').forEach(img => {
|
||||
img.style.border = '2px solid red';
|
||||
});">Highlight Images</button>
|
||||
|
||||
<button class="example-script" data-script="// Dark mode
|
||||
document.body.style.backgroundColor = '#222';
|
||||
document.body.style.color = '#eee';
|
||||
document.querySelectorAll('a').forEach(a => {
|
||||
a.style.color = '#5af';
|
||||
});">Dark Mode</button>
|
||||
|
||||
<button class="example-script" data-script="// Remove ads (simplistic example)
|
||||
document.querySelectorAll('div[id*="ad"], div[class*="ad"], iframe[src*="ad"]').forEach(el => {
|
||||
el.style.display = 'none';
|
||||
});">Hide Ads</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Handle example script buttons
|
||||
document.querySelectorAll('.example-script').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
document.getElementById('customScript').value = this.dataset.script;
|
||||
});
|
||||
});
|
||||
|
||||
// Set initial checkbox state based on server setting
|
||||
document.getElementById('enableScript').checked = {{.ScriptEnabled}};
|
||||
|
||||
// Handle form submission with AJAX
|
||||
document.getElementById('scriptForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Get the form values directly
|
||||
const scriptText = document.getElementById('customScript').value;
|
||||
const isEnabled = document.getElementById('enableScript').checked;
|
||||
|
||||
console.log('Submitting script, enabled:', isEnabled);
|
||||
|
||||
// Create form data manually
|
||||
const formData = new FormData();
|
||||
formData.append('customScript', scriptText);
|
||||
|
||||
// Only append enableScript if it's checked
|
||||
if (isEnabled) {
|
||||
formData.append('enableScript', 'on');
|
||||
}
|
||||
|
||||
// Log what we're sending
|
||||
for (let pair of formData.entries()) {
|
||||
console.log(pair[0] + ': ' + pair[1]);
|
||||
}
|
||||
|
||||
fetch('/save-script', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Script saved successfully! Enabled: ' + data.enabled);
|
||||
} else {
|
||||
alert('Error saving script: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error saving script');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<footer>
|
||||
Powered by FuckHTTP3 Proxy - Using HTTP/3 with QUIC
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user