Files
opsxapi/server/internal/echo/echo.go
T
logan 2bb4ef7a95 桌面 GUI 外链修复 + 主题顺序调整(含结构扁平化进行中的工作区基线)
- 集成 tauri-plugin-opener:Cargo/lib.rs/capabilities/npm 四处接线,新增 src/openExternal.ts(桌面走 opener,浏览器回退 window.open,相对路径绝对化)
- 关于页 4 个链接、活动栏官网菜单、代理错误横幅 2 处健康检查链接全部改走 openExternal,桌面端可正常跳转
- 设置→外观 主题顺序调整为 深色/浅色/跟随系统
- release 二进制重编并验证(浏览器实测关于页链接可开文档页)
- 附带工作区中未提交的扁平化重构现状(另一会话进行中)
- homepage 落地页与文档终检通过(8787 / 与 /docs 双 200)
2026-10-16 03:03:36 +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)
}