Files
opsxapi/apps/server/internal/echo/echo.go
T
logan 045e8c65c3 定时任务 Cron 表达式支持(工作区遗留改动收尾)
- TestSchedule 新增可选 5 段 cron(分 时 日 月 周,本地时区),优先于 intervalMinutes;runLog 调度日志
- cron.ts 解析与匹配引擎 + 单测;scheduler 按最近下次触发时间调度
- ScheduleDialog / TestingView 支持 cron 输入与下次运行提示;AiCaseDialog 微调
- 服务端 internal/echo 调试回显端点;修复 model.ts 注释内 */ 导致的语法错误
- 全量 53 前端用例绿
2026-09-18 11:42:34 +08:00

155 lines
3.5 KiB
Go

// Package echo 提供本地 HTTP 回显,供自动化回归套件使用(不依赖外网 httpbin)。
package echo
import (
"encoding/base64"
"io"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
// Mount registers /v1/echo/* routes.
func Mount(r *gin.Engine) {
e := r.Group("/v1/echo")
e.GET("/get", getEcho)
e.POST("/json", jsonEcho)
e.POST("/form", formEcho)
e.POST("/multipart", multipartEcho)
e.GET("/headers", headersEcho)
e.GET("/cookies", cookiesEcho)
e.GET("/bearer", bearerEcho)
e.GET("/basic", basicEcho)
}
func baseMeta(c *gin.Context) gin.H {
hdrs := gin.H{}
for k, v := range c.Request.Header {
if len(v) == 1 {
hdrs[k] = v[0]
} else {
hdrs[k] = v
}
}
q := gin.H{}
for k, v := range c.Request.URL.Query() {
if len(v) == 1 {
q[k] = v[0]
} else {
q[k] = v
}
}
return gin.H{
"ok": true,
"service": "opsxapi-echo",
"method": c.Request.Method,
"path": c.Request.URL.Path,
"query": q,
"headers": hdrs,
}
}
func getEcho(c *gin.Context) {
c.JSON(http.StatusOK, baseMeta(c))
}
func jsonEcho(c *gin.Context) {
body, _ := io.ReadAll(c.Request.Body)
out := baseMeta(c)
out["bodyRaw"] = string(body)
c.JSON(http.StatusOK, out)
}
func formEcho(c *gin.Context) {
_ = c.Request.ParseForm()
form := gin.H{}
for k, v := range c.Request.PostForm {
if len(v) == 1 {
form[k] = v[0]
} else {
form[k] = v
}
}
out := baseMeta(c)
out["form"] = form
c.JSON(http.StatusOK, out)
}
func multipartEcho(c *gin.Context) {
_ = c.Request.ParseMultipartForm(8 << 20)
form := gin.H{}
if c.Request.MultipartForm != nil {
for k, v := range c.Request.MultipartForm.Value {
if len(v) == 1 {
form[k] = v[0]
} else {
form[k] = v
}
}
}
out := baseMeta(c)
out["form"] = form
c.JSON(http.StatusOK, out)
}
func headersEcho(c *gin.Context) {
out := baseMeta(c)
out["xOpsxProbe"] = c.GetHeader("X-Opsx-Probe")
c.JSON(http.StatusOK, out)
}
func cookiesEcho(c *gin.Context) {
out := baseMeta(c)
cookies := gin.H{}
for _, ck := range c.Request.Cookies() {
cookies[ck.Name] = ck.Value
}
// 浏览器禁止脚本设置 Cookie 头;回归用例改走 X-Opsx-Cookie
if raw := c.GetHeader("X-Opsx-Cookie"); raw != "" {
for _, part := range strings.Split(raw, ";") {
part = strings.TrimSpace(part)
if part == "" {
continue
}
kv := strings.SplitN(part, "=", 2)
if len(kv) == 2 {
cookies[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1])
}
}
}
out["cookies"] = cookies
c.JSON(http.StatusOK, out)
}
func bearerEcho(c *gin.Context) {
auth := c.GetHeader("Authorization")
if !strings.HasPrefix(auth, "Bearer ") || strings.TrimSpace(strings.TrimPrefix(auth, "Bearer ")) == "" {
c.JSON(http.StatusUnauthorized, gin.H{"ok": false, "error": "missing bearer"})
return
}
out := baseMeta(c)
out["authenticated"] = true
out["token"] = strings.TrimSpace(strings.TrimPrefix(auth, "Bearer "))
c.JSON(http.StatusOK, out)
}
func basicEcho(c *gin.Context) {
auth := c.GetHeader("Authorization")
if !strings.HasPrefix(auth, "Basic ") {
c.Header("WWW-Authenticate", `Basic realm="opsxapi"`)
c.JSON(http.StatusUnauthorized, gin.H{"ok": false, "error": "missing basic"})
return
}
raw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(strings.TrimPrefix(auth, "Basic ")))
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"ok": false, "error": "bad basic"})
return
}
parts := strings.SplitN(string(raw), ":", 2)
out := baseMeta(c)
out["authenticated"] = true
out["user"] = parts[0]
c.JSON(http.StatusOK, out)
}