120 lines
2.9 KiB
Go
120 lines
2.9 KiB
Go
|
|
package middleware
|
||
|
|
|
||
|
|
import (
|
||
|
|
"net/http"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"github.com/gin-gonic/gin"
|
||
|
|
)
|
||
|
|
|
||
|
|
func SecurityHeaders() gin.HandlerFunc {
|
||
|
|
return func(c *gin.Context) {
|
||
|
|
h := c.Writer.Header()
|
||
|
|
h.Set("X-Content-Type-Options", "nosniff")
|
||
|
|
h.Set("X-Frame-Options", "DENY")
|
||
|
|
h.Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||
|
|
h.Set("X-Permitted-Cross-Domain-Policies", "none")
|
||
|
|
h.Set("Cross-Origin-Opener-Policy", "same-origin")
|
||
|
|
h.Set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=()")
|
||
|
|
if isHTTPSRequest(c) {
|
||
|
|
h.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
||
|
|
}
|
||
|
|
if isSensitivePath(c.Request.URL.Path) {
|
||
|
|
h.Set("Cache-Control", "no-store")
|
||
|
|
h.Set("Pragma", "no-cache")
|
||
|
|
}
|
||
|
|
c.Next()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func isHTTPSRequest(c *gin.Context) bool {
|
||
|
|
if c == nil || c.Request == nil {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
if c.Request.TLS != nil {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
return strings.EqualFold(c.GetHeader("X-Forwarded-Proto"), "https")
|
||
|
|
}
|
||
|
|
|
||
|
|
func isSensitivePath(path string) bool {
|
||
|
|
switch strings.TrimSpace(path) {
|
||
|
|
case "/api/auth/login",
|
||
|
|
"/api/auth/refresh",
|
||
|
|
"/api/auth/me",
|
||
|
|
"/api/auth/logout",
|
||
|
|
"/api/auth/password-key",
|
||
|
|
"/api/ops/auth/login",
|
||
|
|
"/api/ops/auth/me",
|
||
|
|
"/api/ops/auth/password",
|
||
|
|
"/api/ops/auth/password-key":
|
||
|
|
return true
|
||
|
|
default:
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
type CORSConfig struct {
|
||
|
|
AllowedOrigins []string
|
||
|
|
AllowAll bool
|
||
|
|
}
|
||
|
|
|
||
|
|
func CORSWithConfig(cfg CORSConfig) gin.HandlerFunc {
|
||
|
|
allowedOrigins := compactOrigins(cfg.AllowedOrigins)
|
||
|
|
allowedSet := make(map[string]struct{}, len(allowedOrigins))
|
||
|
|
for _, origin := range allowedOrigins {
|
||
|
|
allowedSet[origin] = struct{}{}
|
||
|
|
}
|
||
|
|
|
||
|
|
return func(c *gin.Context) {
|
||
|
|
origin := strings.TrimSpace(c.GetHeader("Origin"))
|
||
|
|
allowedOrigin := ""
|
||
|
|
switch {
|
||
|
|
case cfg.AllowAll:
|
||
|
|
allowedOrigin = "*"
|
||
|
|
case origin != "":
|
||
|
|
if _, ok := allowedSet[origin]; ok {
|
||
|
|
allowedOrigin = origin
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if allowedOrigin != "" {
|
||
|
|
c.Header("Access-Control-Allow-Origin", allowedOrigin)
|
||
|
|
if allowedOrigin != "*" {
|
||
|
|
c.Header("Vary", "Origin")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
|
||
|
|
c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Authorization, X-Request-ID, X-Internal-Metrics-Token")
|
||
|
|
c.Header("Access-Control-Expose-Headers", "X-Request-ID")
|
||
|
|
c.Header("Access-Control-Max-Age", "86400")
|
||
|
|
|
||
|
|
if c.Request.Method == http.MethodOptions {
|
||
|
|
if origin != "" && allowedOrigin == "" {
|
||
|
|
c.AbortWithStatus(http.StatusForbidden)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
c.AbortWithStatus(http.StatusNoContent)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
c.Next()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func compactOrigins(values []string) []string {
|
||
|
|
out := make([]string, 0, len(values))
|
||
|
|
seen := make(map[string]struct{}, len(values))
|
||
|
|
for _, value := range values {
|
||
|
|
value = strings.TrimSpace(value)
|
||
|
|
if value == "" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if _, ok := seen[value]; ok {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
seen[value] = struct{}{}
|
||
|
|
out = append(out, value)
|
||
|
|
}
|
||
|
|
return out
|
||
|
|
}
|