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>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user