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