network/pkg/gateway/anon_proxy_handler.go
anonpenguin23 351ce086bf feat: enhance anonProxyHandler to handle binary data safely
- Added Base64 encoding for the response body in the anonProxyHandler to prevent corruption of binary data when returned in JSON format.
- Updated the response structure to include the encoded body, ensuring compatibility with various data types.
2025-10-30 09:00:24 +02:00

243 lines
6.9 KiB
Go

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
if idx := strings.LastIndex(host, ":"); idx != -1 {
host = host[:idx]
}
// Check for localhost variants
if host == "localhost" || host == "127.0.0.1" || 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
}