Files
tamabox/route/pixel/pixel.go
T
2026-09-08 10:10:44 +08:00

88 lines
2.4 KiB
Go

// Copyright 2024 E99p1ant. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package pixel
import (
"io"
"net/http"
"path"
"strconv"
"strings"
"time"
"github.com/sirupsen/logrus"
"github.com/tamakyi/TamaBox/internal/conf"
"github.com/tamakyi/TamaBox/internal/context"
"github.com/tamakyi/TamaBox/internal/i18n"
)
// pixelSafeResponseHeaders 允许从上游透传到客户端的响应头白名单。
// 绝不透传 Set-Cookie / Set-Cookie2 等,避免上游借像素代理向客户端注入会话或凭证。
var pixelSafeResponseHeaders = []string{
"Content-Type",
"Content-Length",
"Content-Encoding",
"Content-Disposition",
"Cache-Control",
"ETag",
"Expires",
"Last-Modified",
"Pragma",
"Vary",
}
func Index(ctx context.Context) {
ctx.Success("pixel")
}
func Proxy(ctx context.Context) error {
uri := ctx.Param("**")
// 路径穿越防护:归一化后必须仍位于 /api/ 之下,
// 否则(如含 ../ 跳出 /api)直接拒绝,防止访问到未授权路径。
cleaned := path.Clean("/api/" + uri)
if !strings.HasPrefix(cleaned, "/api/") {
return ctx.JSONError(http.StatusBadRequest, i18n.T(ctx.Lang(), "pixel.errPath"))
}
userID := strconv.Itoa(int(ctx.User.ID))
// 仅允许只读 GET:写方法(POST/PUT/DELETE)在路由层已被限制为 GET,
// 此处不再处理请求体,杜绝被用作对上游的越权写入口。
req, err := http.NewRequest(http.MethodGet, "http://pixel/", nil)
if err != nil {
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to create request")
return ctx.ServerError()
}
req.URL.Host = conf.Pixel.Host
req.URL.Path = cleaned
req.Header.Set("neko-user-id", userID)
client := http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to send request")
return ctx.ServerError()
}
defer func() { _ = resp.Body.Close() }()
// 仅透传白名单内的安全响应头。
for _, k := range pixelSafeResponseHeaders {
if v := resp.Header.Get(k); v != "" {
ctx.ResponseWriter().Header().Set(k, v)
}
}
ctx.ResponseWriter().WriteHeader(resp.StatusCode)
_, err = io.Copy(ctx.ResponseWriter(), resp.Body)
if err != nil {
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to copy response")
return ctx.ServerError()
}
return nil
}