Repo .git was damaged (refs/ missing, index truncated, object store near-empty). Repaired via git init (rebuilt refs), stale index preserved as .git/index.corrupt-20260910.bak, then full worktree committed as a rollback baseline.
88 lines
2.4 KiB
Go
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
|
|
}
|