Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
|
|
@ -0,0 +1,74 @@
|
|||
// monorepo/cloud/maplefile-backend/internal/maplefile/interface/http/middleware/jwt.go
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config/constants"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
func (mid *middleware) JWTProcessorMiddleware(fn http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Extract the Authorization header
|
||||
reqToken := r.Header.Get("Authorization")
|
||||
|
||||
// Validate that Authorization header is present
|
||||
if reqToken == "" {
|
||||
problem := httperror.NewUnauthorizedError("Authorization not set")
|
||||
problem.WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract the token from the Authorization header
|
||||
// Support both "Bearer" (RFC 6750 standard) and "JWT" schemes for compatibility
|
||||
var token string
|
||||
if strings.HasPrefix(reqToken, "Bearer ") {
|
||||
token = strings.TrimPrefix(reqToken, "Bearer ")
|
||||
} else if strings.HasPrefix(reqToken, "JWT ") {
|
||||
token = strings.TrimPrefix(reqToken, "JWT ")
|
||||
} else {
|
||||
problem := httperror.NewBadRequestError("Not properly formatted authorization header")
|
||||
problem.WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the token is not empty after prefix removal
|
||||
if token == "" {
|
||||
problem := httperror.NewBadRequestError("Not properly formatted authorization header")
|
||||
problem.WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
// Process the JWT token
|
||||
sessionID, err := mid.jwt.ProcessJWTToken(token)
|
||||
if err != nil {
|
||||
// Log the actual error for debugging but return generic message to client
|
||||
mid.logger.Error("JWT processing failed", zap.Error(err))
|
||||
problem := httperror.NewUnauthorizedError("Invalid or expired token")
|
||||
problem.WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
// Update our context to save our JWT token content information
|
||||
ctx = context.WithValue(ctx, constants.SessionIsAuthorized, true)
|
||||
ctx = context.WithValue(ctx, constants.SessionID, sessionID)
|
||||
|
||||
// Flow to the next middleware with our JWT token saved
|
||||
fn(w, r.WithContext(ctx))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config/constants"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
)
|
||||
|
||||
func (mid *middleware) PostJWTProcessorMiddleware(fn http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Get our authorization information.
|
||||
isAuthorized, ok := ctx.Value(constants.SessionIsAuthorized).(bool)
|
||||
if ok && isAuthorized {
|
||||
// CWE-391: Safe type assertion to prevent panic-based DoS
|
||||
// OWASP A09:2021: Security Logging and Monitoring - Prevents service crashes
|
||||
sessionID, ok := ctx.Value(constants.SessionID).(string)
|
||||
if !ok {
|
||||
mid.logger.Error("Invalid session ID type in context")
|
||||
problem := httperror.NewInternalServerError("Invalid session context")
|
||||
problem.WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the user ID from the session ID (which is actually the user ID string from JWT)
|
||||
userID, err := gocql.ParseUUID(sessionID)
|
||||
if err != nil {
|
||||
problem := httperror.NewUnauthorizedError("Invalid user ID in token")
|
||||
problem.WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
// Lookup our user profile by ID or return 500 error.
|
||||
user, err := mid.userGetByIDUseCase.Execute(ctx, userID)
|
||||
if err != nil {
|
||||
// Log the actual error for debugging but return generic message to client
|
||||
mid.logger.Error("Failed to get user by ID",
|
||||
zap.Error(err),
|
||||
zap.String("user_id", userID.String()))
|
||||
problem := httperror.NewInternalServerError("Unable to verify session")
|
||||
problem.WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
// If no user was found then that means our session expired and the
|
||||
// user needs to login or use the refresh token.
|
||||
if user == nil {
|
||||
problem := httperror.NewUnauthorizedError("Session expired")
|
||||
problem.WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
// // If system administrator disabled the user account then we need
|
||||
// // to generate a 403 error letting the user know their account has
|
||||
// // been disabled and you cannot access the protected API endpoint.
|
||||
// if user.State == 0 {
|
||||
// http.Error(w, "Account disabled - please contact admin", http.StatusForbidden)
|
||||
// return
|
||||
// }
|
||||
|
||||
// Save our user information to the context.
|
||||
// Save our user.
|
||||
ctx = context.WithValue(ctx, constants.SessionUser, user)
|
||||
|
||||
// Save individual pieces of the user profile.
|
||||
ctx = context.WithValue(ctx, constants.SessionID, sessionID)
|
||||
ctx = context.WithValue(ctx, constants.SessionUserID, user.ID)
|
||||
ctx = context.WithValue(ctx, constants.SessionUserRole, user.Role)
|
||||
ctx = context.WithValue(ctx, constants.SessionUserName, user.Name)
|
||||
ctx = context.WithValue(ctx, constants.SessionUserFirstName, user.FirstName)
|
||||
ctx = context.WithValue(ctx, constants.SessionUserLastName, user.LastName)
|
||||
ctx = context.WithValue(ctx, constants.SessionUserTimezone, user.Timezone)
|
||||
// ctx = context.WithValue(ctx, constants.SessionUserStoreID, user.StoreID)
|
||||
// ctx = context.WithValue(ctx, constants.SessionUserStoreName, user.StoreName)
|
||||
// ctx = context.WithValue(ctx, constants.SessionUserStoreLevel, user.StoreLevel)
|
||||
// ctx = context.WithValue(ctx, constants.SessionUserStoreTimezone, user.StoreTimezone)
|
||||
}
|
||||
|
||||
fn(w, r.WithContext(ctx))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware/middleware.go
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
uc_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/user"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/security/jwt"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Middleware interface {
|
||||
Attach(fn http.HandlerFunc) http.HandlerFunc
|
||||
Shutdown(ctx context.Context)
|
||||
}
|
||||
|
||||
type middleware struct {
|
||||
logger *zap.Logger
|
||||
jwt jwt.JWTProvider
|
||||
userGetByIDUseCase uc_user.UserGetByIDUseCase
|
||||
}
|
||||
|
||||
func NewMiddleware(
|
||||
logger *zap.Logger,
|
||||
jwtp jwt.JWTProvider,
|
||||
uc1 uc_user.UserGetByIDUseCase,
|
||||
) Middleware {
|
||||
logger = logger.With(zap.String("module", "maplefile"))
|
||||
logger = logger.Named("MapleFile Middleware")
|
||||
return &middleware{
|
||||
logger: logger,
|
||||
jwt: jwtp,
|
||||
userGetByIDUseCase: uc1,
|
||||
}
|
||||
}
|
||||
|
||||
// Attach function attaches to HTTP router to apply for every API call.
|
||||
func (mid *middleware) Attach(fn http.HandlerFunc) http.HandlerFunc {
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Apply base middleware to all requests
|
||||
handler := mid.applyBaseMiddleware(fn)
|
||||
|
||||
// Check if the path requires authentication
|
||||
if isProtectedPath(mid.logger, r.URL.Path) {
|
||||
|
||||
// Apply auth middleware for protected paths
|
||||
handler = mid.PostJWTProcessorMiddleware(handler)
|
||||
handler = mid.JWTProcessorMiddleware(handler)
|
||||
// handler = mid.EnforceBlacklistMiddleware(handler)
|
||||
}
|
||||
|
||||
handler(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// Attach function attaches to HTTP router to apply for every API call.
|
||||
func (mid *middleware) applyBaseMiddleware(fn http.HandlerFunc) http.HandlerFunc {
|
||||
// Apply middleware in reverse order (bottom up)
|
||||
handler := fn
|
||||
handler = mid.URLProcessorMiddleware(handler)
|
||||
handler = mid.RequestBodySizeLimitMiddleware(handler)
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
// RequestBodySizeLimitMiddleware limits the size of request bodies to prevent DoS attacks.
|
||||
// Default limit is 10MB for most requests, which is sufficient for JSON metadata payloads.
|
||||
func (mid *middleware) RequestBodySizeLimitMiddleware(fn http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// 10MB limit for request bodies
|
||||
// This is sufficient for JSON metadata while preventing abuse
|
||||
const maxBodySize = 10 * 1024 * 1024 // 10MB
|
||||
|
||||
if r.Body != nil {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxBodySize)
|
||||
}
|
||||
|
||||
fn(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown shuts down the middleware.
|
||||
func (mid *middleware) Shutdown(ctx context.Context) {
|
||||
// Log a message to indicate that the HTTP server is shutting down.
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
uc_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/user"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/ratelimit"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/security/jwt"
|
||||
)
|
||||
|
||||
// Wire provider for middleware
|
||||
|
||||
func ProvideMiddleware(
|
||||
logger *zap.Logger,
|
||||
jwtProvider jwt.JWTProvider,
|
||||
userGetByIDUseCase uc_user.UserGetByIDUseCase,
|
||||
) Middleware {
|
||||
return NewMiddleware(logger, jwtProvider, userGetByIDUseCase)
|
||||
}
|
||||
|
||||
// ProvideRateLimitMiddleware provides the rate limit middleware for Wire DI
|
||||
func ProvideRateLimitMiddleware(
|
||||
logger *zap.Logger,
|
||||
loginRateLimiter ratelimit.LoginRateLimiter,
|
||||
) *RateLimitMiddleware {
|
||||
return NewRateLimitMiddleware(logger, loginRateLimiter)
|
||||
}
|
||||
|
||||
// ProvideSecurityHeadersMiddleware provides the security headers middleware for Wire DI
|
||||
func ProvideSecurityHeadersMiddleware(
|
||||
config *config.Config,
|
||||
) *SecurityHeadersMiddleware {
|
||||
return NewSecurityHeadersMiddleware(config)
|
||||
}
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
// Package middleware provides HTTP middleware for the MapleFile backend.
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/ratelimit"
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
|
||||
)
|
||||
|
||||
// RateLimitMiddleware provides rate limiting functionality for HTTP endpoints
|
||||
type RateLimitMiddleware struct {
|
||||
logger *zap.Logger
|
||||
loginRateLimiter ratelimit.LoginRateLimiter
|
||||
}
|
||||
|
||||
// NewRateLimitMiddleware creates a new rate limit middleware
|
||||
func NewRateLimitMiddleware(logger *zap.Logger, loginRateLimiter ratelimit.LoginRateLimiter) *RateLimitMiddleware {
|
||||
return &RateLimitMiddleware{
|
||||
logger: logger.Named("RateLimitMiddleware"),
|
||||
loginRateLimiter: loginRateLimiter,
|
||||
}
|
||||
}
|
||||
|
||||
// LoginRateLimit applies login-specific rate limiting to auth endpoints
|
||||
// CWE-307: Protects against brute force attacks on authentication endpoints
|
||||
func (m *RateLimitMiddleware) LoginRateLimit(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Extract client IP
|
||||
clientIP := m.extractClientIP(r)
|
||||
|
||||
// Extract email from request body (need to buffer and restore)
|
||||
email := m.extractEmailFromRequest(r)
|
||||
|
||||
// Check rate limit
|
||||
allowed, isLocked, remainingAttempts, err := m.loginRateLimiter.CheckAndRecordAttempt(ctx, email, clientIP)
|
||||
if err != nil {
|
||||
// Log error but allow request (fail open for availability)
|
||||
m.logger.Warn("Rate limiter error, allowing request",
|
||||
zap.Error(err),
|
||||
zap.String("ip", validation.MaskIP(clientIP)))
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if account is locked
|
||||
if isLocked {
|
||||
m.logger.Warn("Login attempt on locked account",
|
||||
zap.String("ip", validation.MaskIP(clientIP)),
|
||||
zap.String("path", r.URL.Path))
|
||||
|
||||
problem := httperror.NewTooManyRequestsError(
|
||||
"Account temporarily locked due to too many failed attempts. Please try again later.")
|
||||
problem.WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if IP rate limit exceeded
|
||||
if !allowed {
|
||||
m.logger.Warn("Rate limit exceeded",
|
||||
zap.String("ip", validation.MaskIP(clientIP)),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.Int("remaining_attempts", remainingAttempts))
|
||||
|
||||
problem := httperror.NewTooManyRequestsError(
|
||||
"Too many requests. Please slow down and try again later.")
|
||||
problem.WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
// Add remaining attempts to response header for client awareness
|
||||
if remainingAttempts > 0 && remainingAttempts <= 3 {
|
||||
w.Header().Set("X-RateLimit-Remaining", fmt.Sprintf("%d", remainingAttempts))
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// AuthRateLimit applies general rate limiting to auth endpoints
|
||||
// For endpoints like registration, email verification, etc.
|
||||
func (m *RateLimitMiddleware) AuthRateLimit(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Extract client IP for rate limiting key
|
||||
clientIP := m.extractClientIP(r)
|
||||
|
||||
// Use the login rate limiter for IP-based checking only
|
||||
// This provides basic protection against automated attacks
|
||||
ctx := r.Context()
|
||||
allowed, _, _, err := m.loginRateLimiter.CheckAndRecordAttempt(ctx, "", clientIP)
|
||||
if err != nil {
|
||||
// Fail open
|
||||
m.logger.Warn("Rate limiter error, allowing request", zap.Error(err))
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if !allowed {
|
||||
m.logger.Warn("Auth rate limit exceeded",
|
||||
zap.String("ip", validation.MaskIP(clientIP)),
|
||||
zap.String("path", r.URL.Path))
|
||||
|
||||
problem := httperror.NewTooManyRequestsError(
|
||||
"Too many requests from this IP. Please try again later.")
|
||||
problem.WithInstance(r.URL.Path).
|
||||
WithTraceID(httperror.ExtractRequestID(r))
|
||||
httperror.RespondWithProblem(w, problem)
|
||||
return
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// extractClientIP extracts the real client IP from the request
|
||||
func (m *RateLimitMiddleware) extractClientIP(r *http.Request) string {
|
||||
// Check X-Forwarded-For header first (for reverse proxies)
|
||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
// Take the first IP in the chain
|
||||
ips := strings.Split(xff, ",")
|
||||
if len(ips) > 0 {
|
||||
return strings.TrimSpace(ips[0])
|
||||
}
|
||||
}
|
||||
|
||||
// Check X-Real-IP header
|
||||
if xri := r.Header.Get("X-Real-IP"); xri != "" {
|
||||
return xri
|
||||
}
|
||||
|
||||
// Fall back to RemoteAddr
|
||||
// Remove port if present
|
||||
ip := r.RemoteAddr
|
||||
if idx := strings.LastIndex(ip, ":"); idx != -1 {
|
||||
ip = ip[:idx]
|
||||
}
|
||||
|
||||
return ip
|
||||
}
|
||||
|
||||
// extractEmailFromRequest extracts email from JSON request body
|
||||
// It buffers the body so it can be read again by the handler
|
||||
func (m *RateLimitMiddleware) extractEmailFromRequest(r *http.Request) string {
|
||||
// Read body
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
// Restore body for handler
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
|
||||
// Parse JSON to extract email
|
||||
var req struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.ToLower(strings.TrimSpace(req.Email))
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
|
||||
)
|
||||
|
||||
// SecurityHeadersMiddleware adds security headers to all HTTP responses.
|
||||
// These headers help protect against common web vulnerabilities.
|
||||
type SecurityHeadersMiddleware struct {
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
// NewSecurityHeadersMiddleware creates a new security headers middleware.
|
||||
func NewSecurityHeadersMiddleware(config *config.Config) *SecurityHeadersMiddleware {
|
||||
return &SecurityHeadersMiddleware{
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// Handler wraps an http.Handler to add security headers to all responses.
|
||||
func (m *SecurityHeadersMiddleware) Handler(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// X-Content-Type-Options: Prevents MIME-type sniffing attacks
|
||||
// Browser will strictly follow the declared Content-Type
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
|
||||
// X-Frame-Options: Prevents clickjacking attacks
|
||||
// DENY = page cannot be displayed in any iframe
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
|
||||
// X-XSS-Protection: Enables browser's built-in XSS filter
|
||||
// mode=block = block the entire page if attack is detected
|
||||
// Note: Largely superseded by CSP, but still useful for older browsers
|
||||
w.Header().Set("X-XSS-Protection", "1; mode=block")
|
||||
|
||||
// Referrer-Policy: Controls how much referrer information is sent
|
||||
// strict-origin-when-cross-origin = full URL for same-origin, origin only for cross-origin
|
||||
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
|
||||
// Cache-Control: Prevent caching of sensitive responses
|
||||
// Especially important for auth endpoints
|
||||
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, private")
|
||||
|
||||
// Permissions-Policy: Restricts browser features (formerly Feature-Policy)
|
||||
// Disables potentially dangerous features like geolocation, camera, microphone
|
||||
w.Header().Set("Permissions-Policy", "geolocation=(), camera=(), microphone=()")
|
||||
|
||||
// Content-Security-Policy: Prevents XSS and other code injection attacks
|
||||
// For API-only backend: deny all content sources and frame embedding
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'")
|
||||
|
||||
// Strict-Transport-Security (HSTS): Forces HTTPS for the specified duration
|
||||
// Only set in production where HTTPS is properly configured
|
||||
// max-age=31536000 = 1 year in seconds
|
||||
// includeSubDomains = applies to all subdomains
|
||||
if m.config.App.Environment == "production" {
|
||||
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware/url.go
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// URLProcessorMiddleware Middleware will split the full URL path into slash-sperated parts and save to
|
||||
// the context to flow downstream in the app for this particular request.
|
||||
func (mid *middleware) URLProcessorMiddleware(fn http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Split path into slash-separated parts, for example, path "/foo/bar"
|
||||
// gives p==["foo", "bar"] and path "/" gives p==[""]. Our API starts with
|
||||
// "/api", as a result we will start the array slice at "1".
|
||||
p := strings.Split(r.URL.Path, "/")[1:]
|
||||
|
||||
// log.Println(p) // For debugging purposes only.
|
||||
|
||||
// Open our program's context based on the request and save the
|
||||
// slash-seperated array from our URL path.
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, "url_split", p)
|
||||
|
||||
// Flow to the next middleware.
|
||||
fn(w, r.WithContext(ctx))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware/utils.go
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type protectedRoute struct {
|
||||
pattern string
|
||||
regex *regexp.Regexp
|
||||
}
|
||||
|
||||
var (
|
||||
exactPaths = make(map[string]bool)
|
||||
patternRoutes []protectedRoute
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Exact matches
|
||||
exactPaths = map[string]bool{
|
||||
"/api/v1/me": true,
|
||||
"/api/v1/me/delete": true,
|
||||
"/api/v1/me/blocked-emails": true,
|
||||
"/api/v1/dashboard": true,
|
||||
"/api/v1/collections": true,
|
||||
"/api/v1/collections/filtered": true,
|
||||
"/api/v1/collections/root": true,
|
||||
"/api/v1/collections/shared": true,
|
||||
"/api/v1/collections/sync": true, // Sync collections endpoint
|
||||
"/api/v1/files": true,
|
||||
"/api/v1/files/pending": true, // Three-step workflow file-create endpoint: Start
|
||||
"/api/v1/files/recent": true,
|
||||
"/api/v1/files/sync": true, // Sync files endpoint
|
||||
"/api/v1/files/delete-multiple": true, // Delete multiple files endpoint
|
||||
"/api/v1/invites/send-email": true, // Send invitation email to non-registered user
|
||||
"/api/v1/tags": true, // List and create tags
|
||||
"/api/v1/tags/search": true, // Search by tags
|
||||
"/iam/api/v1/users/lookup": true, // User public key lookup (requires auth)
|
||||
}
|
||||
|
||||
// Pattern matches
|
||||
patterns := []string{
|
||||
// Blocked Email patterns
|
||||
"^/api/v1/me/blocked-emails/[^/]+$", // Delete specific blocked email
|
||||
|
||||
// Collection patterns (plural routes)
|
||||
"^/api/v1/collections/[a-zA-Z0-9-]+$", // Individual collection operations
|
||||
"^/api/v1/collections/[a-zA-Z0-9-]+/move$", // Move collection
|
||||
"^/api/v1/collections/[a-zA-Z0-9-]+/share$", // Share collection
|
||||
"^/api/v1/collections/[a-zA-Z0-9-]+/members$", // Collection members
|
||||
"^/api/v1/collections/[a-zA-Z0-9-]+/members/[a-zA-Z0-9-]+$", // Remove specific member
|
||||
"^/api/v1/collections/[a-zA-Z0-9-]+/archive$", // Archive collection
|
||||
"^/api/v1/collections/[a-zA-Z0-9-]+/restore$", // Restore collection
|
||||
"^/api/v1/collections-by-parent/[a-zA-Z0-9-]+$", // Collections by parent
|
||||
|
||||
// Collection patterns (singular routes for files)
|
||||
"^/api/v1/collection/[a-zA-Z0-9-]+/files$", // Collection files (singular)
|
||||
|
||||
// File patterns (singular routes)
|
||||
"^/api/v1/file/[a-zA-Z0-9-]+$", // Individual file operations
|
||||
"^/api/v1/file/[a-zA-Z0-9-]+/data$", // File data
|
||||
"^/api/v1/file/[a-zA-Z0-9-]+/upload-url$", // File upload URL
|
||||
"^/api/v1/file/[a-zA-Z0-9-]+/download-url$", // File download URL
|
||||
"^/api/v1/file/[a-zA-Z0-9-]+/complete$", // Complete file upload
|
||||
"^/api/v1/file/[a-zA-Z0-9-]+/archive$", // Archive file
|
||||
"^/api/v1/file/[a-zA-Z0-9-]+/restore$", // Restore file
|
||||
|
||||
// Tag patterns
|
||||
"^/api/v1/tags/[a-zA-Z0-9-]+$", // Individual tag operations (GET, PUT, DELETE)
|
||||
"^/api/v1/tags/[a-zA-Z0-9-]+/assign$", // Assign tag to entity
|
||||
"^/api/v1/tags/[a-zA-Z0-9-]+/entities/[a-zA-Z0-9-]+$", // Unassign tag from entity
|
||||
"^/api/v1/tags/for/collection/[a-zA-Z0-9-]+$", // Get tags for collection
|
||||
"^/api/v1/tags/for/file/[a-zA-Z0-9-]+$", // Get tags for file
|
||||
"^/api/v1/tags/collections$", // List collections by tag
|
||||
"^/api/v1/tags/files$", // List files by tag
|
||||
}
|
||||
|
||||
// Precompile patterns
|
||||
patternRoutes = make([]protectedRoute, len(patterns))
|
||||
for i, pattern := range patterns {
|
||||
patternRoutes[i] = protectedRoute{
|
||||
pattern: pattern,
|
||||
regex: regexp.MustCompile(pattern),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isProtectedPath(logger *zap.Logger, path string) bool {
|
||||
// Check exact matches first (O(1) lookup)
|
||||
if exactPaths[path] {
|
||||
logger.Debug("✅ found via map - url is protected",
|
||||
zap.String("path", path))
|
||||
return true
|
||||
}
|
||||
|
||||
// Check patterns
|
||||
for _, route := range patternRoutes {
|
||||
if route.regex.MatchString(path) {
|
||||
logger.Debug("✅ found via regex - url is protected",
|
||||
zap.String("path", path))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug("❌ not found",
|
||||
zap.String("path", path))
|
||||
|
||||
return false
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue