桌面 GUI 外链修复 + 主题顺序调整(含结构扁平化进行中的工作区基线)
- 集成 tauri-plugin-opener:Cargo/lib.rs/capabilities/npm 四处接线,新增 src/openExternal.ts(桌面走 opener,浏览器回退 window.open,相对路径绝对化) - 关于页 4 个链接、活动栏官网菜单、代理错误横幅 2 处健康检查链接全部改走 openExternal,桌面端可正常跳转 - 设置→外观 主题顺序调整为 深色/浅色/跟随系统 - release 二进制重编并验证(浏览器实测关于页链接可开文档页) - 附带工作区中未提交的扁平化重构现状(另一会话进行中) - homepage 落地页与文档终检通过(8787 / 与 /docs 双 200)
This commit is contained in:
1 parent
6452026fd6
commit
2bb4ef7a95
299 files changed
+6059
-3322
No files matched your search
@@ -0,0 +1,116 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const maxResponseBytes = 10 << 20 // 10 MiB MVP safety cap
|
||||
|
||||
type Handler struct {
|
||||
BaseClient *http.Client
|
||||
}
|
||||
|
||||
func NewHandler() *Handler {
|
||||
return &Handler{BaseClient: &http.Client{Timeout: 30 * time.Second}}
|
||||
}
|
||||
|
||||
func normalizeMethod(raw string) string {
|
||||
m := strings.ToUpper(strings.TrimSpace(raw))
|
||||
switch m {
|
||||
case http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch,
|
||||
http.MethodDelete, http.MethodHead, http.MethodOptions:
|
||||
return m
|
||||
default:
|
||||
return http.MethodGet
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) ProxyHTTP(c *gin.Context) {
|
||||
var spec HttpSpec
|
||||
if err := c.ShouldBindJSON(&spec); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid HttpSpec", "detail": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
method := normalizeMethod(spec.Method)
|
||||
|
||||
u, err := ValidateURL(spec.URL)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
query := u.Query()
|
||||
for key, value := range spec.Query {
|
||||
query.Set(key, value)
|
||||
}
|
||||
u.RawQuery = query.Encode()
|
||||
|
||||
var body io.Reader
|
||||
if spec.Body != nil && spec.Body.Mode != "none" && method != http.MethodGet && method != http.MethodHead {
|
||||
body = bytes.NewBufferString(spec.Body.Raw)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(c.Request.Context(), method, u.String(), body)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "cannot create request", "detail": err.Error()})
|
||||
return
|
||||
}
|
||||
for key, value := range spec.Headers {
|
||||
// Hop-by-hop headers are managed by net/http.
|
||||
if !isHopByHop(key) {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
}
|
||||
if spec.Body != nil && spec.Body.Mode == "json" && req.Header.Get("Content-Type") == "" {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
client := h.BaseClient
|
||||
if spec.TimeoutMS > 0 {
|
||||
client = &http.Client{Timeout: time.Duration(spec.TimeoutMS) * time.Millisecond}
|
||||
}
|
||||
started := time.Now()
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": "upstream request failed", "detail": err.Error()})
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
payload, err := io.ReadAll(io.LimitReader(res.Body, maxResponseBytes+1))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": "cannot read upstream response", "detail": err.Error()})
|
||||
return
|
||||
}
|
||||
if len(payload) > maxResponseBytes {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": "upstream response exceeds 10 MiB limit"})
|
||||
return
|
||||
}
|
||||
|
||||
headers := make(map[string]string, len(res.Header))
|
||||
for key, values := range res.Header {
|
||||
headers[strings.ToLower(key)] = strings.Join(values, ", ")
|
||||
}
|
||||
c.JSON(http.StatusOK, HttpResult{
|
||||
Status: res.StatusCode,
|
||||
Headers: headers,
|
||||
Body: string(payload),
|
||||
TimeMS: time.Since(started).Milliseconds(),
|
||||
Size: len(payload),
|
||||
})
|
||||
}
|
||||
|
||||
func isHopByHop(name string) bool {
|
||||
switch strings.ToLower(name) {
|
||||
case "connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailer", "transfer-encoding", "upgrade", "host", "content-length":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user