chore: sub-store Go 重写项目初始化

This commit is contained in:
2026-07-27 14:38:13 +08:00
commit 90e53aa754
85 changed files with 15271 additions and 0 deletions
+38
View File
@@ -0,0 +1,38 @@
package middleware
import (
"strings"
"github.com/gofiber/fiber/v3"
"github.com/peterqiu0516/sub-store/internal/util"
)
// ExtractToken extracts the admin/download token from the request.
// Per review-resolution #26: three sources, priority: Bearer header → query ?token → x-sub-store-token header.
func ExtractToken(c fiber.Ctx) string {
// 1. Authorization: Bearer <token>
auth := c.Get("Authorization")
if strings.HasPrefix(strings.ToLower(auth), "bearer ") {
return strings.TrimSpace(auth[7:])
}
// 2. ?token=<token>
if token := c.Query("token"); token != "" {
return token
}
// 3. x-sub-store-token header
return c.Get("X-Sub-Store-Token")
}
// RequireAdmin returns a middleware that checks the admin token.
func RequireAdmin(adminToken string) fiber.Handler {
return func(c fiber.Ctx) error {
if !util.IsTokenValid(ExtractToken(c), adminToken) {
return c.Status(401).JSON(fiber.Map{
"status": "failed",
"error": fiber.Map{"code": 401, "message": "Admin token is invalid"},
})
}
return c.Next()
}
}
+54
View File
@@ -0,0 +1,54 @@
package middleware
import (
"strings"
"github.com/gofiber/fiber/v3"
)
// CORS middleware.
func CORS(allowedOrigins string) fiber.Handler {
allowlist := parseOrigins(allowedOrigins)
return func(c fiber.Ctx) error {
origin := c.Get("Origin")
if origin != "" && (contains(allowlist, "*") || contains(allowlist, origin)) {
c.Set("Access-Control-Allow-Origin", origin)
c.Set("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
c.Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Sub-Store-Token")
}
return c.Next()
}
}
// HandleOptions handles CORS preflight requests.
func HandleOptions() fiber.Handler {
return func(c fiber.Ctx) error {
if c.Method() == "OPTIONS" {
c.Set("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
c.Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Sub-Store-Token")
return c.SendStatus(204)
}
return c.Next()
}
}
func parseOrigins(s string) []string {
parts := strings.Split(s, ",")
result := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
result = append(result, p)
}
}
return result
}
func contains(list []string, s string) bool {
for _, item := range list {
if item == s {
return true
}
}
return false
}
+108
View File
@@ -0,0 +1,108 @@
package middleware
import (
"regexp"
"strings"
"github.com/gofiber/fiber/v3"
)
// SecurityHeaders sets CSP and other security response headers.
// Per review-resolution #27: CSP tightened to script-src 'self' (no unsafe-eval).
func SecurityHeaders() fiber.Handler {
return func(c fiber.Ctx) error {
c.Set("Content-Security-Policy",
"default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; "+
"script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; "+
"font-src 'self' data:; connect-src 'self'; worker-src 'self' blob:; manifest-src 'self'")
c.Set("Referrer-Policy", "no-referrer")
c.Set("X-Content-Type-Options", "nosniff")
c.Set("X-Frame-Options", "DENY")
c.Set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=(), usb=()")
return c.Next()
}
}
// BodyLimit middleware.
func BodyLimit(limit int) fiber.Handler {
return func(c fiber.Ctx) error {
if len(c.Body()) > limit {
return c.Status(413).JSON(fiber.Map{
"status": "failed",
"error": fiber.Map{"code": 413, "message": "Request body is too large"},
})
}
return c.Next()
}
}
// DownloadHostIsolation ensures download hosts only serve /download/ paths.
func DownloadHostIsolation(downloadHosts []string) fiber.Handler {
hostSet := make(map[string]bool, len(downloadHosts))
for _, h := range downloadHosts {
h = strings.ToLower(strings.TrimSpace(h))
if h != "" {
hostSet[h] = true
}
}
return func(c fiber.Ctx) error {
if len(hostSet) == 0 {
return c.Next()
}
host := strings.ToLower(c.Hostname())
if hostSet[host] && !strings.HasPrefix(c.Path(), "/download/") {
return c.Status(404).SendString("Not Found")
}
return c.Next()
}
}
// crlfRe matches CR and LF characters for injection prevention.
var crlfRe = regexp.MustCompile(`[\r\n]`)
// SetSafeResponseHeader sets a header value only if it doesn't contain CRLF.
// Per review-resolution #25: CRLF injection protection.
func SetSafeResponseHeader(c fiber.Ctx, name, value string) {
if value != "" && !crlfRe.MatchString(value) {
c.Set(name, value)
}
}
// SafeContentDisposition sanitizes a filename for Content-Disposition.
// Per review-resolution #25: strip CRLF and control chars, escape special chars.
func SafeContentDisposition(value string) string {
if value == "" || crlfRe.MatchString(value) {
return ""
}
// Extract filename from the header value
filename := extractFilename(value)
if filename == "" {
return ""
}
// Sanitize: keep alphanumerics, dots, hyphens, parens, CJK, spaces
safe := sanitizeFilename(filename)
if safe == "" {
return ""
}
return `attachment; filename="` + safe + `"`
}
var filenameRe = regexp.MustCompile(`filename\*?=(?:UTF-8''|")?([^";]+)`)
func extractFilename(value string) string {
matches := filenameRe.FindStringSubmatch(value)
if len(matches) < 2 {
return ""
}
return strings.TrimSpace(matches[1])
}
var sanitizeRe = regexp.MustCompile(`[^a-zA-Z0-9.()\- \x{4e00}-\x{9fff}]`)
func sanitizeFilename(name string) string {
safe := sanitizeRe.ReplaceAllString(name, "_")
if len(safe) > 120 {
safe = safe[:120]
}
return safe
}