chore: sub-store Go 重写项目初始化
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user