package gateway import ( "encoding/base64" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" "github.com/DeBrosOfficial/network/pkg/anyoneproxy" "github.com/DeBrosOfficial/network/pkg/logging" "go.uber.org/zap" ) // anonProxyRequest represents the JSON payload for proxy requests type anonProxyRequest struct { URL string `json:"url"` Method string `json:"method"` Headers map[string]string `json:"headers,omitempty"` Body string `json:"body,omitempty"` } // anonProxyResponse represents the JSON response from proxy requests type anonProxyResponse struct { StatusCode int `json:"status_code"` Headers map[string]string `json:"headers"` Body string `json:"body"` Error string `json:"error,omitempty"` } const ( maxProxyRequestSize = 10 * 1024 * 1024 // 10MB maxProxyTimeout = 60 * time.Second ) // anonProxyHandler handles proxied HTTP requests through the Anyone network func (g *Gateway) anonProxyHandler(w http.ResponseWriter, r *http.Request) { // Only accept POST requests if r.Method != http.MethodPost { writeError(w, http.StatusMethodNotAllowed, "only POST method is allowed") return } // Limit request body size r.Body = http.MaxBytesReader(w, r.Body, maxProxyRequestSize) // Parse request payload var req anonProxyRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid JSON payload: %v", err)) return } // Validate URL targetURL, err := url.Parse(req.URL) if err != nil { writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid URL: %v", err)) return } // Only allow HTTPS for external requests if targetURL.Scheme != "https" && targetURL.Scheme != "http" { writeError(w, http.StatusBadRequest, "only http/https schemes are allowed") return } // Block requests to private/local addresses if isPrivateOrLocalHost(targetURL.Host) { writeError(w, http.StatusForbidden, "requests to private/local addresses are not allowed") return } // Validate HTTP method method := strings.ToUpper(req.Method) if method == "" { method = "GET" } allowedMethods := map[string]bool{ "GET": true, "POST": true, "PUT": true, "DELETE": true, "PATCH": true, "HEAD": true, } if !allowedMethods[method] { writeError(w, http.StatusBadRequest, fmt.Sprintf("method %s not allowed", method)) return } // Check if Anyone proxy is running (after all validation) if !anyoneproxy.Running() { g.logger.ComponentWarn(logging.ComponentGeneral, "Anyone proxy not available", zap.String("socks_addr", anyoneproxy.Address())) writeJSON(w, http.StatusServiceUnavailable, anonProxyResponse{ Error: fmt.Sprintf("Anyone proxy not available at %s", anyoneproxy.Address()), }) return } // Create HTTP client with Anyone proxy client := anyoneproxy.NewHTTPClient() client.Timeout = maxProxyTimeout // Create the proxied request var bodyReader io.Reader if req.Body != "" { bodyReader = strings.NewReader(req.Body) } proxyReq, err := http.NewRequestWithContext(r.Context(), method, req.URL, bodyReader) if err != nil { writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create request: %v", err)) return } // Copy headers, excluding hop-by-hop headers for key, value := range req.Headers { if !isHopByHopHeader(key) { proxyReq.Header.Set(key, value) } } // Set default User-Agent if not provided if proxyReq.Header.Get("User-Agent") == "" { proxyReq.Header.Set("User-Agent", "DeBros-Gateway/1.0") } // Log the proxy request g.logger.ComponentInfo(logging.ComponentGeneral, "proxying request through Anyone", zap.String("method", method), zap.String("url", req.URL), zap.String("socks_addr", anyoneproxy.Address())) // Execute the request start := time.Now() resp, err := client.Do(proxyReq) duration := time.Since(start) if err != nil { g.logger.ComponentError(logging.ComponentGeneral, "proxy request failed", zap.Error(err), zap.String("url", req.URL), zap.Duration("duration", duration)) writeJSON(w, http.StatusBadGateway, anonProxyResponse{ Error: fmt.Sprintf("proxy request failed: %v", err), }) return } defer resp.Body.Close() // Read response body respBody, err := io.ReadAll(io.LimitReader(resp.Body, maxProxyRequestSize)) if err != nil { g.logger.ComponentError(logging.ComponentGeneral, "failed to read proxy response", zap.Error(err)) writeJSON(w, http.StatusBadGateway, anonProxyResponse{ Error: fmt.Sprintf("failed to read response: %v", err), }) return } // Extract response headers (excluding hop-by-hop) respHeaders := make(map[string]string) for key, values := range resp.Header { if !isHopByHopHeader(key) && len(values) > 0 { respHeaders[key] = values[0] } } g.logger.ComponentInfo(logging.ComponentGeneral, "proxy request completed", zap.String("url", req.URL), zap.Int("status", resp.StatusCode), zap.Int("bytes", len(respBody)), zap.Duration("duration", duration)) // Base64-encode the body to safely handle binary data in JSON // This prevents corruption when binary data is converted to a string bodyBase64 := base64.StdEncoding.EncodeToString(respBody) // Return the proxied response writeJSON(w, http.StatusOK, anonProxyResponse{ StatusCode: resp.StatusCode, Headers: respHeaders, Body: bodyBase64, }) } // isHopByHopHeader returns true for HTTP hop-by-hop headers that should not be forwarded func isHopByHopHeader(header string) bool { hopByHop := map[string]bool{ "Connection": true, "Keep-Alive": true, "Proxy-Authenticate": true, "Proxy-Authorization": true, "Te": true, "Trailers": true, "Transfer-Encoding": true, "Upgrade": true, } return hopByHop[http.CanonicalHeaderKey(header)] } // isPrivateOrLocalHost checks if a host is private, local, or loopback func isPrivateOrLocalHost(host string) bool { // Strip port if present, handling IPv6 addresses properly // IPv6 addresses in URLs are bracketed: [::1]:8080 if strings.HasPrefix(host, "[") { // IPv6 address with brackets if idx := strings.LastIndex(host, "]"); idx != -1 { if idx+1 < len(host) && host[idx+1] == ':' { // Port present, strip it host = host[1:idx] // Remove brackets and port } else { // No port, just remove brackets host = host[1:idx] } } } else { // IPv4 or hostname, check for port if idx := strings.LastIndex(host, ":"); idx != -1 { // Check if it's an IPv6 address without brackets (contains multiple colons) colonCount := strings.Count(host, ":") if colonCount == 1 { // Single colon, likely IPv4 with port host = host[:idx] } // If multiple colons, it's IPv6 without brackets and no port // Leave host as-is } } // Check for localhost variants if host == "localhost" || host == "localhost" || host == "::1" { return true } // Check common private ranges (basic check) if strings.HasPrefix(host, "10.") || strings.HasPrefix(host, "192.168.") || strings.HasPrefix(host, "172.16.") || strings.HasPrefix(host, "172.17.") || strings.HasPrefix(host, "172.18.") || strings.HasPrefix(host, "172.19.") || strings.HasPrefix(host, "172.20.") || strings.HasPrefix(host, "172.21.") || strings.HasPrefix(host, "172.22.") || strings.HasPrefix(host, "172.23.") || strings.HasPrefix(host, "172.24.") || strings.HasPrefix(host, "172.25.") || strings.HasPrefix(host, "172.26.") || strings.HasPrefix(host, "172.27.") || strings.HasPrefix(host, "172.28.") || strings.HasPrefix(host, "172.29.") || strings.HasPrefix(host, "172.30.") || strings.HasPrefix(host, "172.31.") { return true } return false }