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/
*.swp
*.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 ./
RUN go mod download
# Create templates directory
RUN mkdir -p templates
# Copy templates first
COPY templates/ templates/
# Copy source code
COPY . .
@ -29,6 +35,10 @@ COPY --from=builder /go/bin/fuckhttp3 .
# Copy certificates
COPY cert.pem key.pem ./
# Create templates directory and copy templates
RUN mkdir -p templates
COPY templates/ templates/
# Expose the HTTP/3 port (UDP for QUIC)
EXPOSE 8443/udp
EXPOSE 8443/tcp

211
README.md
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
- **Dual Proxy Modes** - Operate in forward or reverse proxy configuration
- **Protocol Intelligence** - Auto-handles HTTP/1.1, HTTP/2, and HTTP/3 on the same port
- **Military-Grade Security** - TLS 1.3 encryption by default (required for HTTP/3)
- **Smart URL Handling** - Processes various URL formats without configuration
- **Containerized** - Docker and Docker Compose support for effortless deployment
- **Lightweight** - Minimal resource footprint with maximum performance
FuckHTTP3 is a sophisticated proxy tool that:
## 🔧 Requirements
- Acts as a forward proxy for HTTP/3 traffic
- Provides a user-friendly web UI for browsing
- Allows custom JavaScript injection into any proxied website
- Supports both direct browser configuration and web UI-based browsing
- Helps with debugging and manipulating HTTP/3 traffic
- Go 1.21+
- OpenSSL (for certificate generation)
- Docker and Docker Compose (optional)
## 📦 Installation
## 🚀 Quick Start
### Prerequisites
### 1. Clone the repository
- Go 1.16 or higher
- TLS certificates (for HTTPS and HTTP/3)
### Building from source
```bash
# Clone the repository
git clone https://github.com/yourusername/fuckhttp3.git
# Navigate to the project directory
cd fuckhttp3
```
### 2. Generate TLS certificates
HTTP/3 requires TLS certificates. For testing, generate self-signed certificates:
```bash
chmod +x generate-certs.sh
./generate-certs.sh
```
For production, use certificates from a trusted Certificate Authority.
### 3. Build and run
#### 🧪 Option 1: Using Go directly
```bash
go build -o fuckhttp3
./fuckhttp3 --addr=localhost:8443 --cert=cert.pem --key=key.pem --verbose
```
#### 🐳 Option 2: Using Docker Compose
```bash
docker-compose up --build
# Build the binary
go build -o fuckhttp3 .
```
## 🎮 Usage
### Generating certificates
For local development, generate self-signed certificates:
```bash
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes
```
### Running the proxy
Basic usage:
```bash
./fuckhttp3 --addr=localhost:8443 --cert=cert.pem --key=key.pem --webui --verbose
```
### Command-line options
| Option | Description | Default |
|--------|-------------|---------|
| `--addr` | Address to listen on | `localhost:8443` |
| `--cert` | Path to certificate file | `cert.pem` |
| `--key` | Path to private key file | `key.pem` |
| `--cert` | Certificate file path | `cert.pem` |
| `--key` | Private key file path | `key.pem` |
| `--target` | Target address to proxy to | (empty = forward proxy) |
| `--webui` | Enable web UI for user-specified proxy targets | `false` |
| `--verbose` | Enable verbose logging | `false` |
### 🔄 Forward Proxy Mode
## ✨ Features
When the `--target` flag is not provided, operates in forward proxy mode:
### 🌐 Web UI
```bash
./fuckhttp3 --addr=localhost:8443 --cert=cert.pem --key=key.pem
When running with the `--webui` flag, access the web interface by navigating to the proxy address:
```
https://localhost:8443/
```
Configure your browser to use `localhost:8443` as an HTTPS proxy.
The web UI provides:
### ↪️ Reverse Proxy Mode
- A URL input field for browsing through the proxy
- JavaScript injection capability
- Example scripts for common tasks
With the `--target` flag, operates in reverse proxy mode:
### 💉 JavaScript Injection
```bash
# Various target formats supported
./fuckhttp3 --addr=localhost:8443 --cert=cert.pem --key=key.pem --target=example.com
./fuckhttp3 --addr=localhost:8443 --cert=cert.pem --key=key.pem --target=https://example.com
./fuckhttp3 --addr=localhost:8443 --cert=cert.pem --key=key.pem --target=https://example.com/api
```
The proxy can inject custom JavaScript into every web page it processes:
Access at `https://localhost:8443` to reach the target.
1. Write or paste your JavaScript code in the web UI
2. Save the script and enable injection
3. Browse websites with your script automatically injected
## 🧪 Testing the Proxy
#### Example Scripts
Test with curl (if HTTP/3 support is available):
The UI includes example scripts for:
```bash
curl --http3 https://localhost:8443 -k
```
| Script | Purpose |
|--------|---------|
| Dark Mode | Apply dark theme to any website |
| Ad Blocker | Hide common ad elements |
| Image Highlighter | Add borders to all images |
> Note: The `-k` flag bypasses certificate validation for self-signed certificates.
### 🔄 Proxy Modes
### 🌐 Browser Testing
FuckHTTP3 operates in two modes:
1. Open a browser with HTTP/3 support
2. Navigate to `https://localhost:8443`
3. Accept any certificate warnings
4. Check network tab in developer tools to confirm HTTP/3 usage
| Mode | Description |
|------|-------------|
| **Forward Proxy** | Standard proxy that browsers can use directly |
| **Web UI Mode** | Web interface for browsing through the proxy |
## 🔒 Client Configuration
## 🔧 Browser Configuration
### Browser Support
To use FuckHTTP3 as a system-wide proxy:
Enable HTTP/3 in your browser:
### Firefox
#### Chrome
- Open `chrome://flags/`
- Search for "HTTP/3"
- Enable "Experimental QUIC protocol"
- Restart Chrome
1. Open Settings
2. Search for "proxy"
3. Click "Settings" in the Connection Settings section
4. Select "Manual proxy configuration"
5. Enter your proxy address and port
6. Check "Also use this proxy for HTTPS"
#### Firefox
- Open `about:config`
- Search for "network.http.http3.enabled"
- Set to `true`
- Restart Firefox
### Chrome/Chromium
## ⚡ Performance Benefits
- **Zero Round-Trip Time** - Faster connection establishment
- **Loss Resilience** - Improved performance on unstable networks
- **No Head-of-Line Blocking** - Better stream multiplexing
- **Connection Migration** - Maintains connections when networks change
## 🛡️ Security Considerations
- Use trusted certificates in production
- Keep the proxy and dependencies updated
- Consider adding authentication for forward proxy mode
- Limited CONNECT method implementation in current version
> ⚠️ Chrome doesn't natively support HTTP/3 proxies. Use the web UI mode instead.
## 🔍 Troubleshooting
| Issue | Solution |
|-------|----------|
| UDP Buffer Size Warning | Normal, won't affect operation |
| Certificate Issues | Verify certificate validity and accessibility |
| Connection Refused | Check both TCP and UDP port accessibility |
| HTTP/3 Not Working | Verify client HTTP/3 support is enabled |
### Script Injection Issues
## 👥 Contributing
| Problem | Solution |
|---------|----------|
| Scripts not running | Ensure "Enable script injection" is checked |
| No confirmation | Verify script was saved successfully |
| Script blocked | Some sites use CSP which may block injected scripts |
| Testing | Try a simpler website like example.com first |
| Errors | Check browser console for any error messages |
Contributions welcome! Submit a Pull Request to improve FuckHTTP3.
### Certificate Warnings
| Environment | Solution |
|-------------|----------|
| Development | Add self-signed certificate to browser's trust store |
| Production | Use a properly signed certificate from a trusted CA |
## ⚠️ Limitations
- Some websites may block proxy connections
- Content Security Policy might prevent script injection
- Complex single-page applications may require more advanced injection techniques
## 📜 License
This project is licensed under the SuperNets License.
## 🙏 Acknowledgments
- Based on the quic-go library
- Inspired by other Go proxy implementations
This project is licensed under the MIT License - see the LICENSE file for details.
---
**Note**: Experimental implementation which may not support all HTTP/3 features. Production use at your own risk.
**Note**: Use responsibly and respect website terms of service when injecting scripts.

View File

@ -9,12 +9,14 @@ services:
volumes:
- ./cert.pem:/app/cert.pem
- ./key.pem:/app/key.pem
- ./templates:/app/templates
command: [
"fuckhttp3",
"--addr=0.0.0.0:8443",
"--cert=/app/cert.pem",
"--key=/app/key.pem",
"--verbose"
"--verbose",
"--webui"
]
# Add target parameter if you want to use as reverse proxy
# Add target parameter if you want to use as reverse proxy with fixed target
# "--target=example.com:443"

929
main.go
View File

@ -3,17 +3,22 @@ package main
import (
"context"
"crypto/tls"
"encoding/base64"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"os/signal"
"path/filepath"
"regexp"
"strings"
"sync"
"syscall"
"text/template"
"time"
"github.com/quic-go/quic-go"
@ -26,6 +31,7 @@ var (
certFile = flag.String("cert", "cert.pem", "Certificate file")
keyFile = flag.String("key", "key.pem", "Private key file")
targetAddr = flag.String("target", "", "Target address to proxy to (if empty, acts as forward proxy)")
webUI = flag.Bool("webui", false, "Enable web UI for user-specified proxy targets")
)
// List of hop-by-hop headers to be removed when proxying
@ -40,6 +46,11 @@ var hopByHopHeaders = []string{
"Upgrade",
}
const (
scriptFileName = "custom_script.js"
scriptConfigFileName = "script_config.json"
)
func main() {
flag.Parse()
@ -76,6 +87,13 @@ func main() {
logger.Fatalf("Invalid target URL: %v", err)
}
}
// Check for templates directory when webUI is enabled
if *webUI {
if _, err := os.Stat("templates/index.html"); os.IsNotExist(err) {
logger.Fatalf("templates/index.html not found, required for web UI mode")
}
}
// Create a new proxy server
proxyServer := &ProxyServer{
@ -83,6 +101,17 @@ func main() {
targetAddr: *targetAddr,
clients: make(map[string]*http3.RoundTripper),
clientsMu: &sync.Mutex{},
webUI: *webUI,
templates: template.Must(template.ParseGlob("templates/*.html")),
iframeMode: true, // Enable iframe mode by default
customScript: "", // Start with empty script
scriptEnabled: false, // Disabled by default
scriptMutex: sync.RWMutex{},
}
// Load script from disk
if err := proxyServer.loadScriptFromDisk(); err != nil {
logger.Printf("Error loading script from disk: %v", err)
}
// Extract port for Alt-Svc headers
@ -191,6 +220,12 @@ type ProxyServer struct {
targetAddr string
clients map[string]*http3.RoundTripper
clientsMu *sync.Mutex
webUI bool
templates *template.Template
iframeMode bool // Whether to use iframe mode for the navbar
customScript string
scriptEnabled bool
scriptMutex sync.RWMutex
}
// ServeHTTP handles the HTTP requests
@ -202,6 +237,26 @@ func (p *ProxyServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
p.logger.Printf("Received request: %s %s %s", r.Method, r.URL, r.Proto)
}
// Check for web UI mode
if p.webUI {
if r.URL.Path == "/" || r.URL.Path == "" {
p.handleHome(w, r)
return
} else if r.URL.Path == "/proxy" && r.Method == http.MethodPost {
p.handleProxyRequest(w, r)
return
} else if r.URL.Path == "/proxified" {
p.handleProxifiedRequest(w, r)
return
} else if r.URL.Path == "/navbar" {
p.handleNavbarFrame(w, r)
return
} else if r.URL.Path == "/save-script" {
p.handleSaveScript(w, r)
return
}
}
// Check if we're operating as a reverse proxy (with fixed target) or forward proxy
if p.targetAddr != "" {
// Reverse proxy mode
@ -467,4 +522,876 @@ func (p *ProxyServer) isHopByHopHeader(header string) bool {
}
return false
}
}
// handleHome renders the web UI
func (p *ProxyServer) handleHome(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
p.scriptMutex.RLock()
scriptEnabled := p.scriptEnabled
customScript := p.customScript
p.scriptMutex.RUnlock()
// Create template data with all needed fields
data := struct {
WebUIEnabled bool
CustomScript string
ScriptEnabled bool
Error string
URL string
}{
WebUIEnabled: *webUI,
CustomScript: customScript,
ScriptEnabled: scriptEnabled,
Error: "",
URL: "",
}
// Render template
err := p.templates.ExecuteTemplate(w, "index.html", data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
p.logger.Printf("Error rendering template: %v", err)
}
}
// handleProxyRequest processes the form submission from the web UI
func (p *ProxyServer) handleProxyRequest(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
p.logger.Printf("Error parsing form: %v", err)
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
targetURL := r.FormValue("url")
if targetURL == "" {
// Handle empty URL
data := struct {
URL string
Error string
}{
URL: "",
Error: "Please enter a URL",
}
err := p.templates.ExecuteTemplate(w, "index.html", data)
if err != nil {
p.logger.Printf("Error rendering template: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
return
}
// Normalize URL
if !strings.HasPrefix(targetURL, "http://") && !strings.HasPrefix(targetURL, "https://") {
targetURL = "https://" + targetURL
}
// Validate URL
parsedURL, err := url.Parse(targetURL)
if err != nil {
data := struct {
URL string
Error string
}{
URL: targetURL,
Error: "Invalid URL: " + err.Error(),
}
err := p.templates.ExecuteTemplate(w, "index.html", data)
if err != nil {
p.logger.Printf("Error rendering template: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
return
}
// Store the target URL in a query parameter and redirect
proxifiedURL := fmt.Sprintf("/proxified?url=%s", url.QueryEscape(parsedURL.String()))
http.Redirect(w, r, proxifiedURL, http.StatusSeeOther)
}
// handleDataURL handles data URLs
func (p *ProxyServer) handleDataURL(w http.ResponseWriter, r *http.Request, targetURL string) {
// Parse the data URL
if strings.HasPrefix(targetURL, "data:") {
// Extract content type and data
parts := strings.SplitN(targetURL[5:], ",", 2)
if len(parts) != 2 {
http.Error(w, "Invalid data URL format", http.StatusBadRequest)
return
}
contentTypeInfo := parts[0]
data := parts[1]
// Set content type
if contentTypeInfo != "" {
if strings.HasSuffix(contentTypeInfo, ";base64") {
w.Header().Set("Content-Type", strings.TrimSuffix(contentTypeInfo, ";base64"))
// Decode base64
decoded, err := base64.StdEncoding.DecodeString(data)
if err != nil {
http.Error(w, "Invalid base64 encoding in data URL", http.StatusBadRequest)
return
}
w.Write(decoded)
} else {
w.Header().Set("Content-Type", contentTypeInfo)
// URL decode the data
decoded, err := url.QueryUnescape(data)
if err != nil {
http.Error(w, "Invalid URL encoding in data URL", http.StatusBadRequest)
return
}
w.Write([]byte(decoded))
}
} else {
// Default to text/plain
w.Header().Set("Content-Type", "text/plain")
decoded, _ := url.QueryUnescape(data)
w.Write([]byte(decoded))
}
return
}
// Not a data URL, return error
http.Error(w, "Not a data URL", http.StatusBadRequest)
}
// handleProxifiedRequest fetches and processes the target URL content
func (p *ProxyServer) handleProxifiedRequest(w http.ResponseWriter, r *http.Request) {
// Extract the URL from the query parameter
targetURL := r.URL.Query().Get("url")
if targetURL == "" {
http.Error(w, "Missing URL parameter", http.StatusBadRequest)
return
}
// Handle data: URLs specially
if strings.HasPrefix(targetURL, "data:") {
p.handleDataURL(w, r, targetURL)
return
}
// Ensure URL is absolute
if !strings.HasPrefix(targetURL, "http://") && !strings.HasPrefix(targetURL, "https://") {
http.Error(w, "Request URL must be absolute in forward proxy mode", http.StatusBadRequest)
return
}
parsedURL, err := url.Parse(targetURL)
if err != nil {
p.logger.Printf("Error parsing URL: %v", err)
http.Error(w, "Invalid URL", http.StatusBadRequest)
return
}
if *verbose {
p.logger.Printf("Proxifying request to: %s", targetURL)
}
// Get or create a RoundTripper for the target host
roundTripper := p.getRoundTripper(parsedURL.Host)
// Create a new client
client := &http.Client{
Transport: roundTripper,
Timeout: 30 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
// Instead of following redirects, we'll return the redirect URL
// so we can process it through our proxy
return http.ErrUseLastResponse
},
}
// Create a new request
outReq, err := http.NewRequestWithContext(r.Context(), http.MethodGet, targetURL, nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
p.logger.Printf("Error creating request: %v", err)
return
}
// Copy original request headers
p.copyHeaders(outReq.Header, r.Header)
// Add common browser headers if not present
if outReq.Header.Get("User-Agent") == "" {
outReq.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
}
if outReq.Header.Get("Accept") == "" {
outReq.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
}
// Send the request
resp, err := client.Do(outReq)
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
p.logger.Printf("Error sending request: %v", err)
return
}
defer resp.Body.Close()
// Handle redirects manually
if resp.StatusCode >= 300 && resp.StatusCode < 400 {
redirectURL := resp.Header.Get("Location")
if redirectURL != "" {
// Make sure the redirect URL is absolute
redirectURL, err = p.makeAbsoluteURL(redirectURL, parsedURL)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Redirect through our proxy
proxifiedRedirect := fmt.Sprintf("/proxified?url=%s", url.QueryEscape(redirectURL))
http.Redirect(w, r, proxifiedRedirect, resp.StatusCode)
return
}
}
// Copy the response headers
p.copyHeaders(w.Header(), resp.Header)
// Modify response headers to allow framing and inlining
p.modifyResponseHeaders(w.Header())
// Add Alt-Svc header with the correct port
portStr := strings.Split(*addr, ":")[1]
w.Header().Add("Alt-Svc", fmt.Sprintf(`h3=":%s"; ma=2592000`, portStr))
// Set appropriate Content-Type headers
contentType := resp.Header.Get("Content-Type")
if strings.Contains(r.URL.Path, ".js") && contentType == "text/plain" {
// Fix JavaScript MIME type
w.Header().Set("Content-Type", "application/javascript")
} else if strings.Contains(r.URL.Path, ".css") && contentType == "text/plain" {
// Fix CSS MIME type
w.Header().Set("Content-Type", "text/css")
} else {
w.Header().Set("Content-Type", contentType)
}
// Determine content type
contentType = resp.Header.Get("Content-Type")
isHTML := strings.Contains(contentType, "text/html") || strings.Contains(contentType, "application/xhtml+xml")
if isHTML {
// Process HTML content to rewrite links
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
p.logger.Printf("Error reading response body: %v", err)
return
}
// Rewrite URLs in the HTML content
processedHTML := p.processHTML(bodyBytes, parsedURL.String())
// Add a navigation bar at the top
processedHTML = p.addProxyNavBar(processedHTML, parsedURL.String())
// Set the processed content type and length
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(processedHTML)))
// Write status code and content
w.WriteHeader(resp.StatusCode)
w.Write(processedHTML)
} else if strings.Contains(contentType, "javascript") {
// If JavaScript, rewrite URLs
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
p.logger.Printf("Error reading response body: %v", err)
return
}
rewrittenBody := p.rewriteJavaScript(bodyBytes, targetURL)
// Set the processed content type and length
w.Header().Set("Content-Type", contentType)
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(rewrittenBody)))
// Write status code and content
w.WriteHeader(resp.StatusCode)
w.Write(rewrittenBody)
} else {
// For non-HTML content, pass through directly
w.WriteHeader(resp.StatusCode)
if _, err := io.Copy(w, resp.Body); err != nil {
p.logger.Printf("Error copying response body: %v", err)
}
}
}
// processHTML rewrites URLs in HTML content and injects JavaScript
func (p *ProxyServer) processHTML(htmlBytes []byte, baseURLStr string) []byte {
// Convert HTML to string for easier manipulation
htmlStr := string(htmlBytes)
p.scriptMutex.RLock()
scriptEnabled := p.scriptEnabled
customScript := p.customScript
p.scriptMutex.RUnlock()
// If script injection is enabled, inject the custom script into the HTML
if scriptEnabled && customScript != "" {
p.logger.Printf("Injecting script into response for URL: %s", baseURLStr)
// Create script tag with the custom script
scriptTag := fmt.Sprintf("<script type=\"text/javascript\">\n// FuckHTTP3 Injected Script\n%s\n</script>", customScript)
// Try to insert it before </body> first
if strings.Contains(htmlStr, "</body>") {
htmlStr = strings.Replace(htmlStr, "</body>", scriptTag+"\n</body>", 1)
p.logger.Printf("Script injected before </body> tag")
} else if strings.Contains(htmlStr, "</html>") {
// If no </body>, try before </html>
htmlStr = strings.Replace(htmlStr, "</html>", scriptTag+"\n</html>", 1)
p.logger.Printf("Script injected before </html> tag")
} else {
// If neither tag exists, append it to the end
htmlStr += "\n" + scriptTag
p.logger.Printf("Script appended to the end of HTML")
}
} else {
if !scriptEnabled {
p.logger.Printf("Script injection disabled")
} else if customScript == "" {
p.logger.Printf("No custom script to inject")
}
}
// Parse the base URL string into a URL object
baseURL, err := url.Parse(baseURLStr)
if err != nil {
p.logger.Printf("Error parsing base URL %s: %v", baseURLStr, err)
baseURL, _ = url.Parse("https://example.com")
}
// Process base URL if present
baseTagRegex := regexp.MustCompile(`<base[^>]+href=["']([^"']+)["'][^>]*>`)
baseMatch := baseTagRegex.FindStringSubmatch(htmlStr)
if len(baseMatch) > 1 {
// There's a base tag, use it for resolving relative URLs
baseHref := baseMatch[1]
newBaseURL, err := url.Parse(baseHref)
if err == nil {
baseURL = baseURL.ResolveReference(newBaseURL)
}
}
// Rewrite hyperlinks
aTagRegex := regexp.MustCompile(`<a[^>]+href=["']([^"']+)["'][^>]*>`)
htmlStr = aTagRegex.ReplaceAllStringFunc(htmlStr, func(match string) string {
submatches := aTagRegex.FindStringSubmatch(match)
if len(submatches) < 2 {
return match
}
href := submatches[1]
// Skip javascript: and mailto: links
if strings.HasPrefix(href, "javascript:") || strings.HasPrefix(href, "mailto:") {
return match
}
// Construct absolute URL
absoluteURL, err := p.makeAbsoluteURL(href, baseURL)
if err != nil {
return match
}
// Replace the href with our proxified URL
proxifiedURL := fmt.Sprintf("/proxified?url=%s", url.QueryEscape(absoluteURL))
return strings.Replace(match, submatches[1], proxifiedURL, 1)
})
// Rewrite image sources
imgTagRegex := regexp.MustCompile(`<img[^>]+src=["']([^"']+)["'][^>]*>`)
htmlStr = imgTagRegex.ReplaceAllStringFunc(htmlStr, func(match string) string {
submatches := imgTagRegex.FindStringSubmatch(match)
if len(submatches) < 2 {
return match
}
src := submatches[1]
// Construct absolute URL
absoluteURL, err := p.makeAbsoluteURL(src, baseURL)
if err != nil {
return match
}
// Replace the src with our proxified URL
proxifiedURL := fmt.Sprintf("/proxified?url=%s", url.QueryEscape(absoluteURL))
return strings.Replace(match, submatches[1], proxifiedURL, 1)
})
// Rewrite CSS links
linkTagRegex := regexp.MustCompile(`<link[^>]+href=["']([^"']+)["'][^>]*>`)
htmlStr = linkTagRegex.ReplaceAllStringFunc(htmlStr, func(match string) string {
submatches := linkTagRegex.FindStringSubmatch(match)
if len(submatches) < 2 {
return match
}
href := submatches[1]
// Construct absolute URL
absoluteURL, err := p.makeAbsoluteURL(href, baseURL)
if err != nil {
return match
}
// Replace the href with our proxified URL
proxifiedURL := fmt.Sprintf("/proxified?url=%s", url.QueryEscape(absoluteURL))
return strings.Replace(match, submatches[1], proxifiedURL, 1)
})
// Rewrite script sources
scriptTagRegex := regexp.MustCompile(`<script[^>]+src=["']([^"']+)["'][^>]*>`)
htmlStr = scriptTagRegex.ReplaceAllStringFunc(htmlStr, func(match string) string {
submatches := scriptTagRegex.FindStringSubmatch(match)
if len(submatches) < 2 {
return match
}
src := submatches[1]
// Construct absolute URL
absoluteURL, err := p.makeAbsoluteURL(src, baseURL)
if err != nil {
return match
}
// Replace the src with our proxified URL
proxifiedURL := fmt.Sprintf("/proxified?url=%s", url.QueryEscape(absoluteURL))
return strings.Replace(match, submatches[1], proxifiedURL, 1)
})
return []byte(htmlStr)
}
// makeAbsoluteURL converts a relative URL to an absolute URL
func (p *ProxyServer) makeAbsoluteURL(href string, base *url.URL) (string, error) {
// Check if already absolute
if strings.HasPrefix(href, "http://") || strings.HasPrefix(href, "https://") {
return href, nil
}
// Parse the href
relativeURL, err := url.Parse(href)
if err != nil {
return "", err
}
// Resolve against base URL
absoluteURL := base.ResolveReference(relativeURL)
return absoluteURL.String(), nil
}
// addProxyNavBar adds a navigation bar at the top of the HTML content
func (p *ProxyServer) addProxyNavBar(html []byte, currentURL string) []byte {
// Generate a unique ID for this navbar instance to avoid conflicts
navbarID := fmt.Sprintf("fuckhttp3_navbar_%d", time.Now().UnixNano())
// Create a more secure and isolated navbar with iframe
navbar := fmt.Sprintf(`
<div id="%s" style="all: initial !important; position: fixed !important; top: 0 !important; left: 0 !important; width: 100%% !important; height: 50px !important; z-index: 2147483647 !important; box-shadow: 0 2px 5px rgba(0,0,0,0.3) !important; background: #333 !important; font-family: Arial, sans-serif !important; color: white !important; display: block !important; border: none !important; margin: 0 !important; padding: 0 !important;">
<iframe src="/navbar?url=%s" style="width: 100%% !important; height: 100%% !important; border: none !important; margin: 0 !important; padding: 0 !important;" frameBorder="0"></iframe>
</div>
<div style="height: 50px !important; width: 100%% !important; display: block !important;"></div>
<script type="text/javascript">
(function() {
// Store the original createElement to prevent hijacking
var originalCreateElement = document.createElement;
var navbarId = "%s";
// Check and restore navbar every 100ms
setInterval(function() {
var navbar = document.getElementById(navbarId);
if (!navbar || !navbar.style || navbar.style.display === "none" ||
!document.body.contains(navbar) ||
navbar.getBoundingClientRect().height < 10) {
// Create a new navbar if it's been removed or hidden
var newNavbar = originalCreateElement.call(document, "div");
newNavbar.id = navbarId;
newNavbar.style = "all: initial !important; position: fixed !important; top: 0 !important; left: 0 !important; width: 100%% !important; height: 50px !important; z-index: 2147483647 !important; box-shadow: 0 2px 5px rgba(0,0,0,0.3) !important; background: #333 !important; font-family: Arial, sans-serif !important; color: white !important; display: block !important; border: none !important; margin: 0 !important; padding: 0 !important;";
var iframe = originalCreateElement.call(document, "iframe");
iframe.src = "/navbar?url=%s";
iframe.style = "width: 100%% !important; height: 100%% !important; border: none !important; margin: 0 !important; padding: 0 !important;";
iframe.frameBorder = "0";
newNavbar.appendChild(iframe);
// Add to the body
document.body.insertBefore(newNavbar, document.body.firstChild);
// Also ensure spacer exists
var spacer = document.querySelector('div[style*="height: 50px"]');
if (!spacer) {
var spacerDiv = originalCreateElement.call(document, "div");
spacerDiv.style = "height: 50px !important; width: 100%% !important; display: block !important;";
document.body.insertBefore(spacerDiv, newNavbar.nextSibling);
}
}
}, 100);
// Prevent removal by MutationObserver
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'childList') {
var navbar = document.getElementById(navbarId);
if (!navbar || !document.body.contains(navbar)) {
observer.disconnect();
// Re-add the navbar
var newNavbar = originalCreateElement.call(document, "div");
// ... (same code as above to recreate the navbar)
// Restart observation
observer.observe(document.body, { childList: true, subtree: true });
}
}
});
});
// Start observing
if (document.body) {
observer.observe(document.body, { childList: true, subtree: true });
}
})();
</script>
`, navbarID, url.QueryEscape(currentURL), navbarID, url.QueryEscape(currentURL))
// Find the opening html tag to place our additions right after it
htmlRegex := regexp.MustCompile(`(?i)<html[^>]*>`)
htmlMatch := htmlRegex.FindIndex(html)
if len(htmlMatch) > 0 {
// Insert after the html tag
insertPos := htmlMatch[1]
result := append(html[:insertPos], append([]byte(navbar), html[insertPos:]...)...)
return result
}
// Find the body tag if html tag wasn't found
bodyRegex := regexp.MustCompile(`(?i)<body[^>]*>`)
bodyMatch := bodyRegex.FindIndex(html)
if len(bodyMatch) > 0 {
// Insert after the body tag
insertPos := bodyMatch[1]
result := append(html[:insertPos], append([]byte(navbar), html[insertPos:]...)...)
return result
}
// Fallback: if no body tag found, add to the beginning
result := append([]byte(navbar), html...)
return result
}
// modifyResponseHeaders modifies the response headers to allow framing and inlining
func (p *ProxyServer) modifyResponseHeaders(header http.Header) {
// Modify CSP header if present
csp := header.Get("Content-Security-Policy")
if csp != "" {
// Modify the CSP to allow our content
cspParts := strings.Split(csp, ";")
// Create a new CSP with modified directives
var newCspParts []string
frameAncestorsFound := false
for _, part := range cspParts {
part = strings.TrimSpace(part)
// Modify frame-ancestors to allow our domain
if strings.HasPrefix(part, "frame-ancestors") {
newCspParts = append(newCspParts, "frame-ancestors 'self'")
frameAncestorsFound = true
} else if strings.HasPrefix(part, "default-src") {
newCspParts = append(newCspParts, part+" 'unsafe-inline'")
} else if strings.HasPrefix(part, "script-src") {
newCspParts = append(newCspParts, part+" 'unsafe-inline'")
} else if strings.HasPrefix(part, "style-src") {
newCspParts = append(newCspParts, part+" 'unsafe-inline'")
} else {
newCspParts = append(newCspParts, part)
}
}
// Add frame-ancestors if not found
if !frameAncestorsFound {
newCspParts = append(newCspParts, "frame-ancestors 'self'")
}
// Set the new CSP
header.Set("Content-Security-Policy", strings.Join(newCspParts, "; "))
}
// Remove headers that might cause issues
header.Del("X-Frame-Options") // This prevents framing
}
// Add a new handler for the navbar iframe
func (p *ProxyServer) handleNavbarFrame(w http.ResponseWriter, r *http.Request) {
currentURL := r.URL.Query().Get("url")
// Set headers to prevent caching and ensure proper content type
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Content-Security-Policy", "default-src 'self'; style-src 'unsafe-inline'; script-src 'unsafe-inline'")
// Create a minimal HTML page with just the navbar
navbar := fmt.Sprintf(`<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
background-color: #333;
color: white;
overflow: hidden;
}
.navbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
height: 30px;
}
.logo {
font-weight: bold;
font-size: 16px;
margin-right: 10px;
white-space: nowrap;
}
.url-form {
flex-grow: 1;
margin: 0 10px;
}
.form-container {
display: flex;
height: 30px;
}
input[type="text"] {
flex-grow: 1;
padding: 5px;
border: none;
border-radius: 3px 0 0 3px;
margin: 0;
height: 20px;
}
button {
background-color: #4CAF50;
color: white;
border: none;
padding: 5px 10px;
border-radius: 0 3px 3px 0;
cursor: pointer;
height: 30px;
}
.home-link {
white-space: nowrap;
margin-left: 10px;
}
.home-link a {
color: white;
text-decoration: none;
display: inline-block;
padding: 5px;
}
</style>
</head>
<body>
<div class="navbar">
<div class="logo">FuckHTTP3 Proxy</div>
<div class="url-form">
<form action="/proxy" method="post" target="_parent" class="form-container">
<input type="text" name="url" value="%s" placeholder="https://example.com">
<button type="submit">Go</button>
</form>
</div>
<div class="home-link">
<a href="/" target="_parent">Home</a>
</div>
</div>
</body>
</html>`, currentURL)
w.Write([]byte(navbar))
}
// Add this function to rewrite URLs in JavaScript
func (p *ProxyServer) rewriteJavaScript(js []byte, baseURL string) []byte {
// Rewrite JavaScript URLs to ensure they go through our proxy
// This is a simplified approach - a more comprehensive one would use proper JS parsing
// Regular expressions to find URLs in JavaScript
urlPatterns := []*regexp.Regexp{
regexp.MustCompile(`(['"])https?://[^'"]+(['"])`),
regexp.MustCompile(`location\.href\s*=\s*(['"])https?://[^'"]+(['"])`),
regexp.MustCompile(`location\.replace\s*\(\s*(['"])https?://[^'"]+(['"])\s*\)`),
regexp.MustCompile(`window\.open\s*\(\s*(['"])https?://[^'"]+(['"])`),
}
result := string(js)
for _, pattern := range urlPatterns {
result = pattern.ReplaceAllStringFunc(result, func(match string) string {
// Extract the URL from the match
urlMatch := regexp.MustCompile(`https?://[^'"]+`).FindString(match)
if urlMatch != "" {
// Replace the URL with a proxied one, but keep the quotes and other parts
return strings.Replace(match, urlMatch, "/proxified?url="+urlMatch, 1)
}
return match
})
}
return []byte(result)
}
// handleSaveScript handles the form submission for saving a custom script
func (p *ProxyServer) handleSaveScript(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
p.sendJSONError(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Parse multipart form with generous max memory
if err := r.ParseMultipartForm(10 << 20); err != nil {
// Fall back to regular form parsing if not multipart
if err := r.ParseForm(); err != nil {
p.logger.Printf("Error parsing form: %v", err)
p.sendJSONError(w, "Failed to parse form", http.StatusBadRequest)
return
}
}
// Get the script from form
script := r.FormValue("customScript")
p.logger.Printf("Received script length: %d chars", len(script))
// Log all form data for debugging
p.logger.Printf("All form values: %+v", r.Form)
p.logger.Printf("All POST form values: %+v", r.PostForm)
// Check if enableScript exists in form
_, enabledExists := r.Form["enableScript"]
enabled := enabledExists
p.logger.Printf("Script enabled checkbox present: %v", enabledExists)
p.logger.Printf("Setting script enabled to: %v", enabled)
// Update the script in the proxy server
p.scriptMutex.Lock()
p.customScript = script
p.scriptEnabled = enabled
p.scriptMutex.Unlock()
// Save to disk
if err := p.saveScriptToDisk(); err != nil {
p.logger.Printf("Error saving script to disk: %v", err)
p.sendJSONError(w, "Failed to save script to disk", http.StatusInternalServerError)
return
}
// Return a JSON response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(fmt.Sprintf(`{"success": true, "enabled": %t}`, enabled)))
}
// Add this error handler method
func (p *ProxyServer) sendJSONError(w http.ResponseWriter, err string, code int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
w.Write([]byte(fmt.Sprintf(`{"success": false, "error": "%s"}`, err)))
}
// Add these methods to save/load scripts
func (p *ProxyServer) saveScriptToDisk() error {
// Create scripts directory if it doesn't exist
scriptsDir := "scripts"
if _, err := os.Stat(scriptsDir); os.IsNotExist(err) {
if err := os.Mkdir(scriptsDir, 0755); err != nil {
return err
}
}
// Save the script
scriptPath := filepath.Join(scriptsDir, scriptFileName)
if err := ioutil.WriteFile(scriptPath, []byte(p.customScript), 0644); err != nil {
return err
}
// Save config
configPath := filepath.Join(scriptsDir, scriptConfigFileName)
configJSON := fmt.Sprintf(`{"enabled": %t}`, p.scriptEnabled)
if err := ioutil.WriteFile(configPath, []byte(configJSON), 0644); err != nil {
return err
}
return nil
}
func (p *ProxyServer) loadScriptFromDisk() error {
scriptsDir := "scripts"
scriptPath := filepath.Join(scriptsDir, scriptFileName)
configPath := filepath.Join(scriptsDir, scriptConfigFileName)
// Check if files exist
if _, err := os.Stat(scriptPath); os.IsNotExist(err) {
// No script saved yet
return nil
}
// Load script
scriptBytes, err := ioutil.ReadFile(scriptPath)
if err != nil {
return err
}
p.customScript = string(scriptBytes)
// Load config if exists
if _, err := os.Stat(configPath); !os.IsNotExist(err) {
configBytes, err := ioutil.ReadFile(configPath)
if err != nil {
return err
}
configStr := string(configBytes)
p.logger.Printf("Loaded script config: %s", configStr)
// More robust parsing
if strings.Contains(configStr, `"enabled": true`) ||
strings.Contains(configStr, `"enabled":true`) {
p.scriptEnabled = true
p.logger.Printf("Script injection enabled from config")
} else {
p.scriptEnabled = false
p.logger.Printf("Script injection disabled from config")
}
}
return nil
}

6
scripts/custom_script.js Normal file
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>