Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
182 lines
4.7 KiB
Go
182 lines
4.7 KiB
Go
package upstream
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
"videoweb/app/appg"
|
|
"videoweb/pub/common/useragent"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
const maxBody = 4 << 20 // 4MB
|
|
|
|
var client = &http.Client{Timeout: 20 * time.Second}
|
|
|
|
// BaseURL 上游 API 根地址,来自 conf/app.yaml 的 serverAddr。
|
|
func BaseURL() string {
|
|
return strings.TrimRight(strings.TrimSpace(appg.Conf.ServerAddr), "/")
|
|
}
|
|
|
|
// GetJSON 带上 X-Real-IP / User-Agent / X-User-Agent,并从 cookie 透传 Authorization。
|
|
func GetJSON(c *gin.Context, path string, out any) error {
|
|
_, err := doJSON(c, http.MethodGet, path, nil, out)
|
|
return err
|
|
}
|
|
|
|
// GetPublicJSON 请求无需登录的公共数据,不透传客户端 Authorization。
|
|
// 公共配置接口会在收到过期 token 时返回会话失效,因此必须显式与登录态隔离。
|
|
func GetPublicJSON(c *gin.Context, path string, out any) error {
|
|
_, err := doJSONWithAuth(c, http.MethodGet, path, nil, out, false)
|
|
return err
|
|
}
|
|
|
|
// PostJSON 向上游 POST JSON,解码到 out;返回 HTTP 状态码。
|
|
// 注意:上游业务错误常见为 HTTP 200 + code!=200,此时仍会成功解码。
|
|
func PostJSON(c *gin.Context, path string, body any, out any) (int, error) {
|
|
return doJSON(c, http.MethodPost, path, body, out)
|
|
}
|
|
|
|
// MultipartFile 上游 multipart 文件字段。
|
|
type MultipartFile struct {
|
|
FieldName string
|
|
FileName string
|
|
Reader io.Reader
|
|
}
|
|
|
|
// PostMultipart 向上游 POST multipart/form-data。
|
|
func PostMultipart(c *gin.Context, path string, fields map[string]string, file *MultipartFile, out any) (int, error) {
|
|
base := BaseURL()
|
|
if base == "" {
|
|
return 0, fmt.Errorf("upstream serverAddr is empty")
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
w := multipart.NewWriter(&buf)
|
|
for k, v := range fields {
|
|
if err := w.WriteField(k, v); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
if file != nil && file.Reader != nil {
|
|
part, err := w.CreateFormFile(file.FieldName, file.FileName)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if _, err := io.Copy(part, io.LimitReader(file.Reader, maxBody)); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
if err := w.Close(); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
req, err := http.NewRequest(http.MethodPost, base+path, &buf)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
applyCommonHeaders(c, req)
|
|
req.Header.Set("Content-Type", w.FormDataContentType())
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
raw, err := io.ReadAll(io.LimitReader(resp.Body, maxBody))
|
|
if err != nil {
|
|
return resp.StatusCode, err
|
|
}
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return resp.StatusCode, fmt.Errorf("upstream %s status %d: %s", path, resp.StatusCode, truncate(string(raw), 200))
|
|
}
|
|
if out != nil {
|
|
if err := json.Unmarshal(raw, out); err != nil {
|
|
return resp.StatusCode, fmt.Errorf("decode %s: %w", path, err)
|
|
}
|
|
}
|
|
return resp.StatusCode, nil
|
|
}
|
|
|
|
func doJSON(c *gin.Context, method, path string, body any, out any) (int, error) {
|
|
return doJSONWithAuth(c, method, path, body, out, true)
|
|
}
|
|
|
|
func doJSONWithAuth(c *gin.Context, method, path string, body any, out any, forwardAuth bool) (int, error) {
|
|
base := BaseURL()
|
|
if base == "" {
|
|
return 0, fmt.Errorf("upstream serverAddr is empty")
|
|
}
|
|
fullURL := base + path
|
|
|
|
var bodyReader io.Reader
|
|
if body != nil {
|
|
b, err := json.Marshal(body)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
bodyReader = strings.NewReader(string(b))
|
|
}
|
|
|
|
req, err := http.NewRequest(method, fullURL, bodyReader)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
applyCommonHeaders(c, req)
|
|
if !forwardAuth {
|
|
req.Header.Del("Authorization")
|
|
}
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
raw, err := io.ReadAll(io.LimitReader(resp.Body, maxBody))
|
|
if err != nil {
|
|
return resp.StatusCode, err
|
|
}
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return resp.StatusCode, fmt.Errorf("upstream %s status %d: %s", path, resp.StatusCode, truncate(string(raw), 200))
|
|
}
|
|
if out != nil {
|
|
if err := json.Unmarshal(raw, out); err != nil {
|
|
return resp.StatusCode, fmt.Errorf("decode %s: %w", path, err)
|
|
}
|
|
}
|
|
return resp.StatusCode, nil
|
|
}
|
|
|
|
func applyCommonHeaders(c *gin.Context, req *http.Request) {
|
|
headers := useragent.BuildUpstreamHeader(c, useragent.DefaultVer)
|
|
for k, v := range headers {
|
|
req.Header.Set(k, v)
|
|
}
|
|
req.Header.Set("Accept", "application/json")
|
|
if token := useragent.TokenFromRequest(c.Request); token != "" {
|
|
req.Header.Set("Authorization", token)
|
|
} else if c != nil && c.Request != nil {
|
|
if auth := strings.TrimSpace(c.GetHeader("Authorization")); auth != "" {
|
|
req.Header.Set("Authorization", auth)
|
|
}
|
|
}
|
|
}
|
|
|
|
func truncate(s string, n int) string {
|
|
if len(s) <= n {
|
|
return s
|
|
}
|
|
return s[:n] + "..."
|
|
}
|