mirror of
https://github.com/DeBrosOfficial/network.git
synced 2025-12-11 07:58:50 +00:00
- Added support for installing and configuring the Anyone Relay (Anon) for anonymous networking in the setup process. - Updated the Makefile to include the Anon client in the development stack, allowing it to run alongside other services. - Implemented a new HTTP proxy handler for the Anon service, enabling proxied requests through the Anyone network. - Enhanced the installation script to manage Anon installation, configuration, and firewall settings. - Introduced tests for the Anon proxy handler to ensure proper request validation and error handling. - Updated documentation to reflect the new Anon service and its usage in the development environment.
238 lines
6.7 KiB
Go
238 lines
6.7 KiB
Go
package gateway
|
|
|
|
import (
|
|
"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))
|
|
|
|
// Return the proxied response
|
|
writeJSON(w, http.StatusOK, anonProxyResponse{
|
|
StatusCode: resp.StatusCode,
|
|
Headers: respHeaders,
|
|
Body: string(respBody),
|
|
})
|
|
}
|
|
|
|
// 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
|
|
}
|