124 lines
4.1 KiB
Go
124 lines
4.1 KiB
Go
package httpapp
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
|
|
"gitea.avt.data-center.id/othman.suseno/atlas/internal/errors"
|
|
)
|
|
|
|
// securityHeadersMiddleware adds security headers to responses
|
|
func (a *App) securityHeadersMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Security headers
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
w.Header().Set("X-Frame-Options", "DENY")
|
|
w.Header().Set("X-XSS-Protection", "1; mode=block")
|
|
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
|
w.Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
|
|
|
|
// HSTS (only for HTTPS)
|
|
if r.TLS != nil {
|
|
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
|
}
|
|
|
|
// Content Security Policy (CSP)
|
|
// Allow Tailwind CDN, unpkg (for htmx), and jsdelivr for external resources
|
|
// Note: Tailwind CDN needs connect-src to fetch config and make network requests
|
|
csp := "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com https://unpkg.com https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com https://unpkg.com https://cdn.jsdelivr.net; img-src 'self' data:; font-src 'self' https://cdn.tailwindcss.com https://unpkg.com https://cdn.jsdelivr.net; connect-src 'self' https://cdn.tailwindcss.com https://unpkg.com https://cdn.jsdelivr.net;"
|
|
w.Header().Set("Content-Security-Policy", csp)
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// corsMiddleware handles CORS requests
|
|
func (a *App) corsMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
origin := r.Header.Get("Origin")
|
|
|
|
// Allow specific origins or all (for development)
|
|
allowedOrigins := []string{
|
|
"http://localhost:8080",
|
|
"http://localhost:3000",
|
|
"http://127.0.0.1:8080",
|
|
}
|
|
|
|
// Check if origin is allowed
|
|
allowed := false
|
|
for _, allowedOrigin := range allowedOrigins {
|
|
if origin == allowedOrigin {
|
|
allowed = true
|
|
break
|
|
}
|
|
}
|
|
|
|
// Allow requests from same origin
|
|
if origin == "" || r.Header.Get("Referer") != "" {
|
|
allowed = true
|
|
}
|
|
|
|
if allowed && origin != "" {
|
|
w.Header().Set("Access-Control-Allow-Origin", origin)
|
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
|
|
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
|
w.Header().Set("Access-Control-Max-Age", "3600")
|
|
}
|
|
|
|
// Handle preflight requests
|
|
if r.Method == http.MethodOptions {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// requestSizeMiddleware limits request body size
|
|
func (a *App) requestSizeMiddleware(maxSize int64) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Limit request body size
|
|
r.Body = http.MaxBytesReader(w, r.Body, maxSize)
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
// validateContentTypeMiddleware validates Content-Type for POST/PUT/PATCH requests
|
|
func (a *App) validateContentTypeMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Skip for GET, HEAD, OPTIONS, DELETE
|
|
if r.Method == http.MethodGet || r.Method == http.MethodHead ||
|
|
r.Method == http.MethodOptions || r.Method == http.MethodDelete {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
// Skip for public endpoints
|
|
if a.isPublicEndpoint(r.URL.Path) {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
// Check Content-Type for POST/PUT/PATCH
|
|
contentType := r.Header.Get("Content-Type")
|
|
if contentType == "" {
|
|
writeError(w, errors.ErrBadRequest("Content-Type header is required"))
|
|
return
|
|
}
|
|
|
|
// Allow JSON and form data
|
|
if !strings.HasPrefix(contentType, "application/json") &&
|
|
!strings.HasPrefix(contentType, "application/x-www-form-urlencoded") &&
|
|
!strings.HasPrefix(contentType, "multipart/form-data") {
|
|
writeError(w, errors.ErrBadRequest("Content-Type must be application/json"))
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|