diff --git a/.gitignore b/.gitignore index 4475f7a..7b212c2 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,8 @@ Desktop.ini .vscode/ *.swp *.swo -*~ \ No newline at end of file +*~ + +# Templates (keep directory structure but ignore content) +templates/* +!templates/index.html \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index dfe35c7..4821359 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,12 @@ RUN apk add --no-cache git gcc musl-dev COPY go.mod go.sum ./ RUN go mod download +# Create templates directory +RUN mkdir -p templates + +# Copy templates first +COPY templates/ templates/ + # Copy source code COPY . . @@ -29,6 +35,10 @@ COPY --from=builder /go/bin/fuckhttp3 . # Copy certificates COPY cert.pem key.pem ./ +# Create templates directory and copy templates +RUN mkdir -p templates +COPY templates/ templates/ + # Expose the HTTP/3 port (UDP for QUIC) EXPOSE 8443/udp EXPOSE 8443/tcp diff --git a/README.md b/README.md index 184bfab..b3fc645 100644 --- a/README.md +++ b/README.md @@ -1,166 +1,157 @@ -# ๐Ÿš€ FuckHTTP3 +# ๐Ÿš€ FuckHTTP3 Proxy -A high-performance HTTP/3 proxy implementation in Go using the QUIC protocol, designed to break through limitations with style. +A powerful HTTP/3 intercepting proxy with JavaScript injection capabilities. Browse websites through HTTP/3 (QUIC) while injecting custom JavaScript into every page. -> *Speed. Security. Simplicity.* +> *Intercept. Inject. Control.* -## โœจ Features +## ๐Ÿ” Overview -- **Ultra-Fast Performance** - Built on HTTP/3 with QUIC protocol for blazing speed -- **Dual Proxy Modes** - Operate in forward or reverse proxy configuration -- **Protocol Intelligence** - Auto-handles HTTP/1.1, HTTP/2, and HTTP/3 on the same port -- **Military-Grade Security** - TLS 1.3 encryption by default (required for HTTP/3) -- **Smart URL Handling** - Processes various URL formats without configuration -- **Containerized** - Docker and Docker Compose support for effortless deployment -- **Lightweight** - Minimal resource footprint with maximum performance +FuckHTTP3 is a sophisticated proxy tool that: -## ๐Ÿ”ง Requirements +- Acts as a forward proxy for HTTP/3 traffic +- Provides a user-friendly web UI for browsing +- Allows custom JavaScript injection into any proxied website +- Supports both direct browser configuration and web UI-based browsing +- Helps with debugging and manipulating HTTP/3 traffic -- Go 1.21+ -- OpenSSL (for certificate generation) -- Docker and Docker Compose (optional) +## ๐Ÿ“ฆ Installation -## ๐Ÿš€ Quick Start +### Prerequisites -### 1. Clone the repository +- Go 1.16 or higher +- TLS certificates (for HTTPS and HTTP/3) + +### Building from source ```bash +# Clone the repository git clone https://github.com/yourusername/fuckhttp3.git + +# Navigate to the project directory cd fuckhttp3 -``` -### 2. Generate TLS certificates - -HTTP/3 requires TLS certificates. For testing, generate self-signed certificates: - -```bash -chmod +x generate-certs.sh -./generate-certs.sh -``` - -For production, use certificates from a trusted Certificate Authority. - -### 3. Build and run - -#### ๐Ÿงช Option 1: Using Go directly - -```bash -go build -o fuckhttp3 -./fuckhttp3 --addr=localhost:8443 --cert=cert.pem --key=key.pem --verbose -``` - -#### ๐Ÿณ Option 2: Using Docker Compose - -```bash -docker-compose up --build +# Build the binary +go build -o fuckhttp3 . ``` ## ๐ŸŽฎ Usage +### Generating certificates + +For local development, generate self-signed certificates: + +```bash +openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes +``` + +### Running the proxy + +Basic usage: + +```bash +./fuckhttp3 --addr=localhost:8443 --cert=cert.pem --key=key.pem --webui --verbose +``` + ### Command-line options | Option | Description | Default | |--------|-------------|---------| | `--addr` | Address to listen on | `localhost:8443` | -| `--cert` | Path to certificate file | `cert.pem` | -| `--key` | Path to private key file | `key.pem` | +| `--cert` | Certificate file path | `cert.pem` | +| `--key` | Private key file path | `key.pem` | | `--target` | Target address to proxy to | (empty = forward proxy) | +| `--webui` | Enable web UI for user-specified proxy targets | `false` | | `--verbose` | Enable verbose logging | `false` | -### ๐Ÿ”„ Forward Proxy Mode +## โœจ Features -When the `--target` flag is not provided, operates in forward proxy mode: +### ๐ŸŒ Web UI -```bash -./fuckhttp3 --addr=localhost:8443 --cert=cert.pem --key=key.pem +When running with the `--webui` flag, access the web interface by navigating to the proxy address: + +``` +https://localhost:8443/ ``` -Configure your browser to use `localhost:8443` as an HTTPS proxy. +The web UI provides: -### โ†ช๏ธ Reverse Proxy Mode +- A URL input field for browsing through the proxy +- JavaScript injection capability +- Example scripts for common tasks -With the `--target` flag, operates in reverse proxy mode: +### ๐Ÿ’‰ JavaScript Injection -```bash -# Various target formats supported -./fuckhttp3 --addr=localhost:8443 --cert=cert.pem --key=key.pem --target=example.com -./fuckhttp3 --addr=localhost:8443 --cert=cert.pem --key=key.pem --target=https://example.com -./fuckhttp3 --addr=localhost:8443 --cert=cert.pem --key=key.pem --target=https://example.com/api -``` +The proxy can inject custom JavaScript into every web page it processes: -Access at `https://localhost:8443` to reach the target. +1. Write or paste your JavaScript code in the web UI +2. Save the script and enable injection +3. Browse websites with your script automatically injected -## ๐Ÿงช Testing the Proxy +#### Example Scripts -Test with curl (if HTTP/3 support is available): +The UI includes example scripts for: -```bash -curl --http3 https://localhost:8443 -k -``` +| Script | Purpose | +|--------|---------| +| Dark Mode | Apply dark theme to any website | +| Ad Blocker | Hide common ad elements | +| Image Highlighter | Add borders to all images | -> Note: The `-k` flag bypasses certificate validation for self-signed certificates. +### ๐Ÿ”„ Proxy Modes -### ๐ŸŒ Browser Testing +FuckHTTP3 operates in two modes: -1. Open a browser with HTTP/3 support -2. Navigate to `https://localhost:8443` -3. Accept any certificate warnings -4. Check network tab in developer tools to confirm HTTP/3 usage +| Mode | Description | +|------|-------------| +| **Forward Proxy** | Standard proxy that browsers can use directly | +| **Web UI Mode** | Web interface for browsing through the proxy | -## ๐Ÿ”’ Client Configuration +## ๐Ÿ”ง Browser Configuration -### Browser Support +To use FuckHTTP3 as a system-wide proxy: -Enable HTTP/3 in your browser: +### Firefox -#### Chrome -- Open `chrome://flags/` -- Search for "HTTP/3" -- Enable "Experimental QUIC protocol" -- Restart Chrome +1. Open Settings +2. Search for "proxy" +3. Click "Settings" in the Connection Settings section +4. Select "Manual proxy configuration" +5. Enter your proxy address and port +6. Check "Also use this proxy for HTTPS" -#### Firefox -- Open `about:config` -- Search for "network.http.http3.enabled" -- Set to `true` -- Restart Firefox +### Chrome/Chromium -## โšก Performance Benefits - -- **Zero Round-Trip Time** - Faster connection establishment -- **Loss Resilience** - Improved performance on unstable networks -- **No Head-of-Line Blocking** - Better stream multiplexing -- **Connection Migration** - Maintains connections when networks change - -## ๐Ÿ›ก๏ธ Security Considerations - -- Use trusted certificates in production -- Keep the proxy and dependencies updated -- Consider adding authentication for forward proxy mode -- Limited CONNECT method implementation in current version +> โš ๏ธ Chrome doesn't natively support HTTP/3 proxies. Use the web UI mode instead. ## ๐Ÿ” Troubleshooting -| Issue | Solution | -|-------|----------| -| UDP Buffer Size Warning | Normal, won't affect operation | -| Certificate Issues | Verify certificate validity and accessibility | -| Connection Refused | Check both TCP and UDP port accessibility | -| HTTP/3 Not Working | Verify client HTTP/3 support is enabled | +### Script Injection Issues -## ๐Ÿ‘ฅ Contributing +| Problem | Solution | +|---------|----------| +| Scripts not running | Ensure "Enable script injection" is checked | +| No confirmation | Verify script was saved successfully | +| Script blocked | Some sites use CSP which may block injected scripts | +| Testing | Try a simpler website like example.com first | +| Errors | Check browser console for any error messages | -Contributions welcome! Submit a Pull Request to improve FuckHTTP3. +### Certificate Warnings + +| Environment | Solution | +|-------------|----------| +| Development | Add self-signed certificate to browser's trust store | +| Production | Use a properly signed certificate from a trusted CA | + +## โš ๏ธ Limitations + +- Some websites may block proxy connections +- Content Security Policy might prevent script injection +- Complex single-page applications may require more advanced injection techniques ## ๐Ÿ“œ License -This project is licensed under the SuperNets License. - -## ๐Ÿ™ Acknowledgments - -- Based on the quic-go library -- Inspired by other Go proxy implementations +This project is licensed under the MIT License - see the LICENSE file for details. --- -**Note**: Experimental implementation which may not support all HTTP/3 features. Production use at your own risk. \ No newline at end of file +**Note**: Use responsibly and respect website terms of service when injecting scripts. \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 6dd248f..449e127 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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" \ No newline at end of file diff --git a/main.go b/main.go index cc6091e..d71ed71 100644 --- a/main.go +++ b/main.go @@ -3,17 +3,22 @@ package main import ( "context" "crypto/tls" + "encoding/base64" "flag" "fmt" "io" + "io/ioutil" "log" "net/http" "net/url" "os" "os/signal" + "path/filepath" + "regexp" "strings" "sync" "syscall" + "text/template" "time" "github.com/quic-go/quic-go" @@ -26,6 +31,7 @@ var ( certFile = flag.String("cert", "cert.pem", "Certificate file") keyFile = flag.String("key", "key.pem", "Private key file") targetAddr = flag.String("target", "", "Target address to proxy to (if empty, acts as forward proxy)") + webUI = flag.Bool("webui", false, "Enable web UI for user-specified proxy targets") ) // List of hop-by-hop headers to be removed when proxying @@ -40,6 +46,11 @@ var hopByHopHeaders = []string{ "Upgrade", } +const ( + scriptFileName = "custom_script.js" + scriptConfigFileName = "script_config.json" +) + func main() { flag.Parse() @@ -76,6 +87,13 @@ func main() { logger.Fatalf("Invalid target URL: %v", err) } } + + // Check for templates directory when webUI is enabled + if *webUI { + if _, err := os.Stat("templates/index.html"); os.IsNotExist(err) { + logger.Fatalf("templates/index.html not found, required for web UI mode") + } + } // Create a new proxy server proxyServer := &ProxyServer{ @@ -83,6 +101,17 @@ func main() { targetAddr: *targetAddr, clients: make(map[string]*http3.RoundTripper), clientsMu: &sync.Mutex{}, + webUI: *webUI, + templates: template.Must(template.ParseGlob("templates/*.html")), + iframeMode: true, // Enable iframe mode by default + customScript: "", // Start with empty script + scriptEnabled: false, // Disabled by default + scriptMutex: sync.RWMutex{}, + } + + // Load script from disk + if err := proxyServer.loadScriptFromDisk(); err != nil { + logger.Printf("Error loading script from disk: %v", err) } // Extract port for Alt-Svc headers @@ -191,6 +220,12 @@ type ProxyServer struct { targetAddr string clients map[string]*http3.RoundTripper clientsMu *sync.Mutex + webUI bool + templates *template.Template + iframeMode bool // Whether to use iframe mode for the navbar + customScript string + scriptEnabled bool + scriptMutex sync.RWMutex } // ServeHTTP handles the HTTP requests @@ -202,6 +237,26 @@ func (p *ProxyServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { p.logger.Printf("Received request: %s %s %s", r.Method, r.URL, r.Proto) } + // Check for web UI mode + if p.webUI { + if r.URL.Path == "/" || r.URL.Path == "" { + p.handleHome(w, r) + return + } else if r.URL.Path == "/proxy" && r.Method == http.MethodPost { + p.handleProxyRequest(w, r) + return + } else if r.URL.Path == "/proxified" { + p.handleProxifiedRequest(w, r) + return + } else if r.URL.Path == "/navbar" { + p.handleNavbarFrame(w, r) + return + } else if r.URL.Path == "/save-script" { + p.handleSaveScript(w, r) + return + } + } + // Check if we're operating as a reverse proxy (with fixed target) or forward proxy if p.targetAddr != "" { // Reverse proxy mode @@ -467,4 +522,876 @@ func (p *ProxyServer) isHopByHopHeader(header string) bool { } return false -} \ No newline at end of file +} + +// 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("", customScript) + + // Try to insert it before first + if strings.Contains(htmlStr, "") { + htmlStr = strings.Replace(htmlStr, "", scriptTag+"\n", 1) + p.logger.Printf("Script injected before tag") + } else if strings.Contains(htmlStr, "") { + // If no , try before + htmlStr = strings.Replace(htmlStr, "", scriptTag+"\n", 1) + p.logger.Printf("Script injected before 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(`]+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(`]+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(`]+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(`]+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(`]+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(` +
+ +
+
+ + +`, navbarID, url.QueryEscape(currentURL), navbarID, url.QueryEscape(currentURL)) + + // Find the opening html tag to place our additions right after it + htmlRegex := regexp.MustCompile(`(?i)]*>`) + 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)]*>`) + 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(` + + + + + + + + +`, 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 +} \ No newline at end of file diff --git a/scripts/custom_script.js b/scripts/custom_script.js new file mode 100644 index 0000000..fb24475 --- /dev/null +++ b/scripts/custom_script.js @@ -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'; +}); \ No newline at end of file diff --git a/scripts/script_config.json b/scripts/script_config.json new file mode 100644 index 0000000..e2e81ce --- /dev/null +++ b/scripts/script_config.json @@ -0,0 +1 @@ +{"enabled": true} \ No newline at end of file diff --git a/setup-templates.sh b/setup-templates.sh new file mode 100644 index 0000000..e771d41 --- /dev/null +++ b/setup-templates.sh @@ -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' + + + + + + FuckHTTP3 Proxy + + + +
+

FuckHTTP3 Web Proxy

+

Enter a URL below to browse through the HTTP/3 proxy

+ + {{if .Error}} +
{{.Error}}
+ {{end}} + +
+
+ +
+ +
+ +
+ Powered by FuckHTTP3 Proxy - Using HTTP/3 with QUIC +
+
+ + +EOL +fi + +echo "Template setup complete!" +chmod +x setup-templates.sh \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..43bbe6f --- /dev/null +++ b/templates/index.html @@ -0,0 +1,259 @@ + + + + + + FuckHTTP3 Proxy + + + +
+

FuckHTTP3 Web Proxy

+

Enter a URL below to browse through the HTTP/3 proxy

+ + {{if .Error}} +
{{.Error}}
+ {{end}} + +
+
+ +
+ +
+ +
+

Custom JavaScript Injection

+

Write JavaScript that will be injected into every proxied page:

+
+
+ +
+ + +
+
+ +
+ +
+

Example Scripts:

+ + + + + +
+
+ + + + +
+ + \ No newline at end of file