This commit is contained in:
e 2025-04-26 15:09:21 -04:00
parent 9452eae947
commit 7d6e3363c0
9 changed files with 1421 additions and 114 deletions

6
.gitignore vendored
View File

@ -35,4 +35,8 @@ Desktop.ini
.vscode/ .vscode/
*.swp *.swp
*.swo *.swo
*~ *~
# Templates (keep directory structure but ignore content)
templates/*
!templates/index.html

View File

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

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

View File

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

@ -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
View 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';
});

View File

@ -0,0 +1 @@
{"enabled": true}

107
setup-templates.sh Normal file
View 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
View 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*=&quot;ad&quot;], div[class*=&quot;ad&quot;], iframe[src*=&quot;ad&quot;]').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>