1397 lines
42 KiB
Go
1397 lines
42 KiB
Go
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("<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
|
|
} |