// 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) }