Files
cyi-box/backend/internal/service/webreq.go
T
chiyiandDevin 834ecacdb9 cyi-box: Memphis-style toolbox admin platform
Go(fun)+SQLite backend, Vue 3 frontend. Modules: dashboard, pay gateway
(Epay-compatible + CDK stock), license/card system (per-app appid, domain
binding), AI memory bank with built-in MCP server (/mcp, FTS5 search,
signed read links), server SSH monitor + terminal + SFTP, proxy pool with
protocol-level alive checks, DB manager (mysql/pg/sqlite + SQL
autocomplete + import/export), disk share, vault (AES-GCM + TOTP),
bookmarks, dev tools.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-01 01:58:47 +08:00

106 lines
2.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"encoding/json"
"strings"
"time"
"github.com/cyi-cc/fun"
"github.com/valyala/fasthttp"
"github.com/cyi-cc/cyi-box/backend/internal/dto"
)
const (
webReqTimeout = 30 * time.Second
webRespMaxBody = 2 << 20 // 响应体截断上限 2MB
webReqMaxBody = 1 << 20 // 请求体上限 1MB
webMaxRedirect = 5
webMaxHeaderPairs = 64
)
// fun autowired 只处理导出字段,client 用包级单例复用
var webClient = &fasthttp.Client{
Name: "cyi-box-webreq",
MaxConnsPerHost: 16,
ReadTimeout: webReqTimeout,
WriteTimeout: webReqTimeout,
MaxConnWaitTimeout: 5 * time.Second,
}
// WebSvc HTTP 请求代理(Postman 工具):服务端代发以绕开浏览器 CORS。
// 只放行 http/https;headersJson 为 {"k":"v"} 对象。
type WebSvc struct {
fun.Ctx
}
func (s *WebSvc) Fetch(d dto.WebFetchDto) (dto.WebFetchView, error) {
method := strings.ToUpper(strings.TrimSpace(d.Method))
switch method {
case "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS":
default:
return dto.WebFetchView{}, fun.Error(4001, "不支持的请求方法: "+method)
}
url := strings.TrimSpace(d.Url)
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
return dto.WebFetchView{}, fun.Error(4001, "URL 必须以 http:// 或 https:// 开头")
}
if len(d.Body) > webReqMaxBody {
return dto.WebFetchView{}, fun.Error(4001, "请求体超过 1MB 上限")
}
var headers map[string]string
if strings.TrimSpace(d.HeadersJson) != "" {
if err := json.Unmarshal([]byte(d.HeadersJson), &headers); err != nil {
return dto.WebFetchView{}, fun.Error(4001, "请求头 JSON 解析失败: "+err.Error())
}
if len(headers) > webMaxHeaderPairs {
return dto.WebFetchView{}, fun.Error(4001, "请求头数量超过上限")
}
}
req := fasthttp.AcquireRequest()
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseRequest(req)
defer fasthttp.ReleaseResponse(resp)
req.Header.SetMethod(method)
req.SetRequestURI(url)
for k, v := range headers {
k = strings.TrimSpace(k)
if k == "" || strings.EqualFold(k, "Host") || strings.EqualFold(k, "Content-Length") {
continue
}
req.Header.Set(k, v)
}
if d.Body != "" {
req.SetBodyString(d.Body)
}
start := time.Now()
err := webClient.DoRedirects(req, resp, webMaxRedirect)
elapsed := time.Since(start).Milliseconds()
if err != nil {
return dto.WebFetchView{}, fun.Error(4002, "请求失败: "+err.Error())
}
view := dto.WebFetchView{
Status: int64(resp.StatusCode()),
ElapsedMs: elapsed,
}
respHeaders := map[string]string{}
resp.Header.VisitAll(func(k, v []byte) {
respHeaders[string(k)] = string(v)
})
if b, e := json.Marshal(respHeaders); e == nil {
view.HeadersJson = string(b)
}
body := resp.Body()
if len(body) > webRespMaxBody {
body = body[:webRespMaxBody]
view.Truncated = true
}
view.Body = string(body)
return view, nil
}