updates
This commit is contained in:
parent
9452eae947
commit
7d6e3363c0
6
.gitignore
vendored
6
.gitignore
vendored
@ -35,4 +35,8 @@ Desktop.ini
|
|||||||
.vscode/
|
.vscode/
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.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 ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
|
# Create templates directory
|
||||||
|
RUN mkdir -p templates
|
||||||
|
|
||||||
|
# Copy templates first
|
||||||
|
COPY templates/ templates/
|
||||||
|
|
||||||
# Copy source code
|
# Copy source code
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
@ -29,6 +35,10 @@ COPY --from=builder /go/bin/fuckhttp3 .
|
|||||||
# Copy certificates
|
# Copy certificates
|
||||||
COPY cert.pem key.pem ./
|
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 the HTTP/3 port (UDP for QUIC)
|
||||||
EXPOSE 8443/udp
|
EXPOSE 8443/udp
|
||||||
EXPOSE 8443/tcp
|
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
|
FuckHTTP3 is a sophisticated proxy tool that:
|
||||||
- **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
|
- 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+
|
## 📦 Installation
|
||||||
- OpenSSL (for certificate generation)
|
|
||||||
- Docker and Docker Compose (optional)
|
|
||||||
|
|
||||||
## 🚀 Quick Start
|
### Prerequisites
|
||||||
|
|
||||||
### 1. Clone the repository
|
- Go 1.16 or higher
|
||||||
|
- TLS certificates (for HTTPS and HTTP/3)
|
||||||
|
|
||||||
|
### Building from source
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Clone the repository
|
||||||
git clone https://github.com/yourusername/fuckhttp3.git
|
git clone https://github.com/yourusername/fuckhttp3.git
|
||||||
|
|
||||||
|
# Navigate to the project directory
|
||||||
cd fuckhttp3
|
cd fuckhttp3
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Generate TLS certificates
|
# Build the binary
|
||||||
|
go build -o fuckhttp3 .
|
||||||
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
|
## 🎮 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
|
### Command-line options
|
||||||
|
|
||||||
| Option | Description | Default |
|
| Option | Description | Default |
|
||||||
|--------|-------------|---------|
|
|--------|-------------|---------|
|
||||||
| `--addr` | Address to listen on | `localhost:8443` |
|
| `--addr` | Address to listen on | `localhost:8443` |
|
||||||
| `--cert` | Path to certificate file | `cert.pem` |
|
| `--cert` | Certificate file path | `cert.pem` |
|
||||||
| `--key` | Path to private key file | `key.pem` |
|
| `--key` | Private key file path | `key.pem` |
|
||||||
| `--target` | Target address to proxy to | (empty = forward proxy) |
|
| `--target` | Target address to proxy to | (empty = forward proxy) |
|
||||||
|
| `--webui` | Enable web UI for user-specified proxy targets | `false` |
|
||||||
| `--verbose` | Enable verbose logging | `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
|
When running with the `--webui` flag, access the web interface by navigating to the proxy address:
|
||||||
./fuckhttp3 --addr=localhost:8443 --cert=cert.pem --key=key.pem
|
|
||||||
|
```
|
||||||
|
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
|
The proxy can inject custom JavaScript into every web page it processes:
|
||||||
# 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.
|
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
|
| Script | Purpose |
|
||||||
curl --http3 https://localhost:8443 -k
|
|--------|---------|
|
||||||
```
|
| 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
|
| Mode | Description |
|
||||||
2. Navigate to `https://localhost:8443`
|
|------|-------------|
|
||||||
3. Accept any certificate warnings
|
| **Forward Proxy** | Standard proxy that browsers can use directly |
|
||||||
4. Check network tab in developer tools to confirm HTTP/3 usage
|
| **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
|
1. Open Settings
|
||||||
- Open `chrome://flags/`
|
2. Search for "proxy"
|
||||||
- Search for "HTTP/3"
|
3. Click "Settings" in the Connection Settings section
|
||||||
- Enable "Experimental QUIC protocol"
|
4. Select "Manual proxy configuration"
|
||||||
- Restart Chrome
|
5. Enter your proxy address and port
|
||||||
|
6. Check "Also use this proxy for HTTPS"
|
||||||
|
|
||||||
#### Firefox
|
### Chrome/Chromium
|
||||||
- Open `about:config`
|
|
||||||
- Search for "network.http.http3.enabled"
|
|
||||||
- Set to `true`
|
|
||||||
- Restart Firefox
|
|
||||||
|
|
||||||
## ⚡ Performance Benefits
|
> ⚠️ Chrome doesn't natively support HTTP/3 proxies. Use the web UI mode instead.
|
||||||
|
|
||||||
- **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
|
## 🔍 Troubleshooting
|
||||||
|
|
||||||
| Issue | Solution |
|
### Script Injection Issues
|
||||||
|-------|----------|
|
|
||||||
| 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
|
| 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
|
## 📜 License
|
||||||
|
|
||||||
This project is licensed under the SuperNets License.
|
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||||
|
|
||||||
## 🙏 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.
|
**Note**: Use responsibly and respect website terms of service when injecting scripts.
|
@ -9,12 +9,14 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./cert.pem:/app/cert.pem
|
- ./cert.pem:/app/cert.pem
|
||||||
- ./key.pem:/app/key.pem
|
- ./key.pem:/app/key.pem
|
||||||
|
- ./templates:/app/templates
|
||||||
command: [
|
command: [
|
||||||
"fuckhttp3",
|
"fuckhttp3",
|
||||||
"--addr=0.0.0.0:8443",
|
"--addr=0.0.0.0:8443",
|
||||||
"--cert=/app/cert.pem",
|
"--cert=/app/cert.pem",
|
||||||
"--key=/app/key.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"
|
# "--target=example.com:443"
|
929
main.go
929
main.go
@ -3,17 +3,22 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"encoding/base64"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/quic-go/quic-go"
|
"github.com/quic-go/quic-go"
|
||||||
@ -26,6 +31,7 @@ var (
|
|||||||
certFile = flag.String("cert", "cert.pem", "Certificate file")
|
certFile = flag.String("cert", "cert.pem", "Certificate file")
|
||||||
keyFile = flag.String("key", "key.pem", "Private key 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)")
|
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
|
// List of hop-by-hop headers to be removed when proxying
|
||||||
@ -40,6 +46,11 @@ var hopByHopHeaders = []string{
|
|||||||
"Upgrade",
|
"Upgrade",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
scriptFileName = "custom_script.js"
|
||||||
|
scriptConfigFileName = "script_config.json"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
@ -76,6 +87,13 @@ func main() {
|
|||||||
logger.Fatalf("Invalid target URL: %v", err)
|
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
|
// Create a new proxy server
|
||||||
proxyServer := &ProxyServer{
|
proxyServer := &ProxyServer{
|
||||||
@ -83,6 +101,17 @@ func main() {
|
|||||||
targetAddr: *targetAddr,
|
targetAddr: *targetAddr,
|
||||||
clients: make(map[string]*http3.RoundTripper),
|
clients: make(map[string]*http3.RoundTripper),
|
||||||
clientsMu: &sync.Mutex{},
|
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
|
// Extract port for Alt-Svc headers
|
||||||
@ -191,6 +220,12 @@ type ProxyServer struct {
|
|||||||
targetAddr string
|
targetAddr string
|
||||||
clients map[string]*http3.RoundTripper
|
clients map[string]*http3.RoundTripper
|
||||||
clientsMu *sync.Mutex
|
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
|
// 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)
|
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
|
// Check if we're operating as a reverse proxy (with fixed target) or forward proxy
|
||||||
if p.targetAddr != "" {
|
if p.targetAddr != "" {
|
||||||
// Reverse proxy mode
|
// Reverse proxy mode
|
||||||
@ -467,4 +522,876 @@ func (p *ProxyServer) isHopByHopHeader(header string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return false
|
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