- 六协议调试:HTTP/GraphQL/WebSocket/SSE/Socket.IO/MQTT
- 集合树/多标签/多环境变量/历史记录,localStorage 持久化
- 导入:OpenAPI 2/3/3.1、Postman、Apifox、Apipost、cURL、HAR、
Insomnia、Hoppscotch、Markdown(自动识别,21 个单测覆盖)
- 导出:OpenAPI 3.0 JSON/YAML、Postman v2.1、Markdown 文档、
全量备份;单请求生成 cURL/fetch/Python/Go 代码
- Gin 服务:HTTP 代理、NDJSON 流式代理(SSE 透传)、
WebSocket 隧道(浏览器自定义握手 Header)
- 内置产品官网 /home 与用户文档站 /docs(SSR,随二进制交付)
- lucide 图标体系 + 品牌标识(Logo/favicon),明暗双主题
- 环境变量:彩色徽标切换器、管理弹窗、{{ 自动补全
117 lines
3.0 KiB
Go
117 lines
3.0 KiB
Go
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
|
|
}
|
|
}
|