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" "github.com/quic-go/quic-go/http3" ) var ( verbose = flag.Bool("verbose", false, "verbose logging") addr = flag.String("addr", "localhost:8443", "Address to listen on") 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 var hopByHopHeaders = []string{ "Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization", "Te", "Trailers", "Transfer-Encoding", "Upgrade", } const ( scriptFileName = "custom_script.js" scriptConfigFileName = "script_config.json" ) func main() { flag.Parse() // Setup logger logger := log.New(os.Stdout, "[FuckHTTP3] ", log.LstdFlags) // Check if cert and key files exist if _, err := os.Stat(*certFile); os.IsNotExist(err) { logger.Fatalf("Certificate file %s does not exist", *certFile) } if _, err := os.Stat(*keyFile); os.IsNotExist(err) { logger.Fatalf("Key file %s does not exist", *keyFile) } // Normalize target address if specified if *targetAddr != "" { // Strip trailing slash for consistency *targetAddr = strings.TrimSuffix(*targetAddr, "/") // Log the actual target we're using if *verbose { logger.Printf("Using normalized target: %s", *targetAddr) } // Check if target is a valid URL if !strings.HasPrefix(*targetAddr, "http://") && !strings.HasPrefix(*targetAddr, "https://") { // Add https:// by default *targetAddr = "https://" + *targetAddr } // Validate the URL _, err := url.Parse(*targetAddr) if err != nil { 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{ logger: logger, 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 portStr := strings.Split(*addr, ":")[1] // Configure TLS tlsConfig := &tls.Config{ MinVersion: tls.VersionTLS13, // HTTP/3 requires TLS 1.3 NextProtos: []string{"h3"}, // Specify HTTP/3 as the next protocol InsecureSkipVerify: false, // Set to true for development only } // Configure the HTTP/3 server server := &http3.Server{ Addr: *addr, TLSConfig: http3.ConfigureTLSConfig(tlsConfig), Handler: proxyServer, QuicConfig: &quic.Config{ MaxIdleTimeout: 30 * time.Second, KeepAlivePeriod: 10 * time.Second, }, } logger.Printf("Starting HTTP/3 proxy server on %s", *addr) logger.Printf("Using certificate: %s", *certFile) logger.Printf("Using key: %s", *keyFile) if *targetAddr != "" { logger.Printf("Proxying to target: %s", *targetAddr) } else { logger.Printf("Running as forward proxy") } // Create certificate pair from files cert, err := tls.LoadX509KeyPair(*certFile, *keyFile) if err != nil { logger.Fatalf("Failed to load certificates: %v", err) } // Create a standard HTTP server for HTTP/1.1 and HTTP/2 standardServer := &http.Server{ Addr: *addr, Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Add Alt-Svc header to advertise HTTP/3 capability w.Header().Set("Alt-Svc", fmt.Sprintf(`h3=":%s"; ma=2592000`, portStr)) // Handle the request with the proxy proxyServer.ServeHTTP(w, r) }), TLSConfig: &tls.Config{ Certificates: []tls.Certificate{cert}, NextProtos: []string{"h2", "http/1.1"}, }, } // Setup context for graceful shutdown ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Handle shutdown signals go func() { sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) sig := <-sigCh logger.Printf("Received signal %v, shutting down...", sig) // Cancel context cancel() // Create shutdown context with timeout shutdownCtx, shutdownCancel := context.WithTimeout(ctx, 10*time.Second) defer shutdownCancel() // Shutdown both servers if err := standardServer.Shutdown(shutdownCtx); err != nil { logger.Printf("Error shutting down HTTP/1.1 server: %v", err) } if err := server.Close(); err != nil { logger.Printf("Error shutting down HTTP/3 server: %v", err) } // Close all active roundtrippers proxyServer.closeAllClients() }() // Start the HTTP/1.1 + HTTP/2 server in a goroutine go func() { logger.Printf("Starting HTTP/1.1 and HTTP/2 server on %s", *addr) if err := standardServer.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed { logger.Printf("HTTP server error: %v", err) } }() // Start the HTTP/3 server err = server.ListenAndServeTLS(*certFile, *keyFile) if err != nil && err != http.ErrServerClosed { logger.Fatalf("Failed to start HTTP/3 server: %v", err) } } // ProxyServer implements the http.Handler interface type ProxyServer struct { logger *log.Logger 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 func (p *ProxyServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { startTime := time.Now() // Log the incoming request if *verbose { 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 p.handleReverseProxy(w, r) } else { // Forward proxy mode p.handleForwardProxy(w, r) } // Log completion time if *verbose { p.logger.Printf("Request completed in %v", time.Since(startTime)) } } // getRoundTripper gets or creates an HTTP/3 RoundTripper for the given host func (p *ProxyServer) getRoundTripper(host string) *http3.RoundTripper { p.clientsMu.Lock() defer p.clientsMu.Unlock() if rt, ok := p.clients[host]; ok { return rt } rt := &http3.RoundTripper{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, // Allow insecure connections for testing NextProtos: []string{"h3"}, }, } p.clients[host] = rt return rt } // closeAllClients closes all RoundTripper instances func (p *ProxyServer) closeAllClients() { p.clientsMu.Lock() defer p.clientsMu.Unlock() for host, rt := range p.clients { if err := rt.Close(); err != nil && *verbose { p.logger.Printf("Error closing roundtripper for %s: %v", host, err) } } p.clients = make(map[string]*http3.RoundTripper) } // handleReverseProxy handles requests in reverse proxy mode func (p *ProxyServer) handleReverseProxy(w http.ResponseWriter, r *http.Request) { // Get or create a RoundTripper for the target roundTripper := p.getRoundTripper(p.targetAddr) // Create a new client client := &http.Client{ Transport: roundTripper, Timeout: 30 * time.Second, } // Create a new request to the target var targetURL string if strings.HasPrefix(p.targetAddr, "http://") || strings.HasPrefix(p.targetAddr, "https://") { // Target already has a scheme if strings.HasSuffix(p.targetAddr, "/") && strings.HasPrefix(r.URL.Path, "/") { // Avoid double slashes when both target and path have slashes targetURL = p.targetAddr + strings.TrimPrefix(r.URL.Path, "/") } else if !strings.HasSuffix(p.targetAddr, "/") && !strings.HasPrefix(r.URL.Path, "/") && r.URL.Path != "" { // Add slash when neither has one targetURL = p.targetAddr + "/" + r.URL.Path } else { // Normal case targetURL = p.targetAddr + r.URL.Path } } else { // No scheme in target, add https:// (original behavior) targetURL = fmt.Sprintf("https://%s%s", p.targetAddr, r.URL.Path) } if r.URL.RawQuery != "" { targetURL += "?" + r.URL.RawQuery } if *verbose { p.logger.Printf("Proxying to: %s", targetURL) } // Create a new request outReq, err := http.NewRequestWithContext(r.Context(), r.Method, targetURL, r.Body) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) p.logger.Printf("Error creating request: %v", err) return } // Copy headers p.copyHeaders(outReq.Header, r.Header) // 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() // Copy the response headers p.copyHeaders(w.Header(), resp.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)) w.WriteHeader(resp.StatusCode) // Copy the response body using io.Copy for efficiency if _, err := io.Copy(w, resp.Body); err != nil { p.logger.Printf("Error copying response body: %v", err) } } // handleForwardProxy handles requests in forward proxy mode func (p *ProxyServer) handleForwardProxy(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodConnect { // Handle CONNECT method (for HTTPS) p.handleConnect(w, r) return } // Ensure absolute URL for forward proxy if !r.URL.IsAbs() { http.Error(w, "Request URL must be absolute in forward proxy mode", http.StatusBadRequest) return } // Get or create a RoundTripper for the target host roundTripper := p.getRoundTripper(r.URL.Host) // Create a new client client := &http.Client{ Transport: roundTripper, Timeout: 30 * time.Second, // Don't follow redirects, let the client handle them CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } if *verbose { p.logger.Printf("Proxying to: %s", r.URL) } // Create a new request outReq, err := http.NewRequestWithContext(r.Context(), r.Method, r.URL.String(), r.Body) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) p.logger.Printf("Error creating request: %v", err) return } // Copy headers p.copyHeaders(outReq.Header, r.Header) // 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() // Copy the response headers p.copyHeaders(w.Header(), resp.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)) w.WriteHeader(resp.StatusCode) // Copy the response body if _, err := io.Copy(w, resp.Body); err != nil { p.logger.Printf("Error copying response body: %v", err) } } // handleConnect handles the CONNECT method for HTTPS tunneling func (p *ProxyServer) handleConnect(w http.ResponseWriter, r *http.Request) { // For HTTP/3 CONNECT, we need to establish a QUIC connection // to the target and then setup a bidirectional stream targetHost := r.Host // Get roundTripper but don't use it yet - we'll access it via client in future implementation _ = p.getRoundTripper(targetHost) // Notify the client that tunnel has been established w.WriteHeader(http.StatusOK) // Check if we're dealing with a hijackable connection hijacker, ok := w.(http.Hijacker) if !ok { p.logger.Printf("Connection doesn't support hijacking, can't establish tunnel") http.Error(w, "CONNECT not supported over HTTP/3", http.StatusInternalServerError) return } // Hijack the connection clientConn, _, err := hijacker.Hijack() if err != nil { p.logger.Printf("Failed to hijack connection: %v", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } defer clientConn.Close() // For QUIC connections, this is more complex as we can't directly hijack streams // This is a basic implementation that won't work for all cases p.logger.Printf("CONNECT tunneling is limited in HTTP/3 due to protocol differences") p.logger.Printf("Simple pass-through enabled for %s", targetHost) // Note: In a full implementation, we would need to: // 1. Open a QUIC connection to the target // 2. Create a bidirectional stream // 3. Set up forwarding between the client stream and target stream // This requires direct access to the QUIC connection which http3.RoundTripper doesn't expose easily } // copyHeaders copies HTTP headers from src to dst, removing hop-by-hop headers func (p *ProxyServer) copyHeaders(dst, src http.Header) { for k, vv := range src { // Skip hop-by-hop headers if p.isHopByHopHeader(k) { continue } for _, v := range vv { dst.Add(k, v) } } } // isHopByHopHeader checks if a header is hop-by-hop func (p *ProxyServer) isHopByHopHeader(header string) bool { header = strings.ToLower(header) for _, h := range hopByHopHeaders { if strings.ToLower(h) == header { return true } } // Check for Connection header values for _, h := range hopByHopHeaders { if h == "Connection" { values := strings.Split(header, ",") for _, v := range values { if strings.TrimSpace(strings.ToLower(v)) == header { return true } } } } 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("", 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 }