package proxy import ( "bytes" "io" "net/http" "strings" "time" "github.com/gin-gonic/gin" ) const maxResponseBytes = 10 << 20 // 10 MiB MVP safety cap type Handler struct { BaseClient *http.Client } func NewHandler() *Handler { return &Handler{BaseClient: &http.Client{Timeout: 30 * time.Second}} } func normalizeMethod(raw string) string { m := strings.ToUpper(strings.TrimSpace(raw)) switch m { case http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete, http.MethodHead, http.MethodOptions: return m default: return http.MethodGet } } func (h *Handler) ProxyHTTP(c *gin.Context) { var spec HttpSpec if err := c.ShouldBindJSON(&spec); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid HttpSpec", "detail": err.Error()}) return } method := normalizeMethod(spec.Method) u, err := ValidateURL(spec.URL) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } query := u.Query() for key, value := range spec.Query { query.Set(key, value) } u.RawQuery = query.Encode() var body io.Reader if spec.Body != nil && spec.Body.Mode != "none" && method != http.MethodGet && method != http.MethodHead { body = bytes.NewBufferString(spec.Body.Raw) } req, err := http.NewRequestWithContext(c.Request.Context(), method, u.String(), body) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "cannot create request", "detail": err.Error()}) return } for key, value := range spec.Headers { // Hop-by-hop headers are managed by net/http. if !isHopByHop(key) { req.Header.Set(key, value) } } if spec.Body != nil && spec.Body.Mode == "json" && req.Header.Get("Content-Type") == "" { req.Header.Set("Content-Type", "application/json") } client := h.BaseClient if spec.TimeoutMS > 0 { client = &http.Client{Timeout: time.Duration(spec.TimeoutMS) * time.Millisecond} } started := time.Now() res, err := client.Do(req) if err != nil { c.JSON(http.StatusBadGateway, gin.H{"error": "upstream request failed", "detail": err.Error()}) return } defer res.Body.Close() payload, err := io.ReadAll(io.LimitReader(res.Body, maxResponseBytes+1)) if err != nil { c.JSON(http.StatusBadGateway, gin.H{"error": "cannot read upstream response", "detail": err.Error()}) return } if len(payload) > maxResponseBytes { c.JSON(http.StatusBadGateway, gin.H{"error": "upstream response exceeds 10 MiB limit"}) return } headers := make(map[string]string, len(res.Header)) for key, values := range res.Header { headers[strings.ToLower(key)] = strings.Join(values, ", ") } c.JSON(http.StatusOK, HttpResult{ Status: res.StatusCode, Headers: headers, Body: string(payload), TimeMS: time.Since(started).Milliseconds(), Size: len(payload), }) } func isHopByHop(name string) bool { switch strings.ToLower(name) { case "connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailer", "transfer-encoding", "upgrade", "host", "content-length": return true default: return false } }