Inital build
Some checks failed
Automated Container Build / build-and-push (push) Failing after 12s

This commit is contained in:
Elijah 2026-05-22 12:29:43 -07:00
parent fa2be029a2
commit 724d70e58b
3339 changed files with 1075535 additions and 0 deletions

View file

@ -0,0 +1,88 @@
package middleware
import (
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
)
// AuthMiddleware validates JWT tokens from the Authorization header.
func AuthMiddleware(jwtSecret string) fiber.Handler {
return func(c *fiber.Ctx) error {
authHeader := c.Get("Authorization")
tokenString := ""
if authHeader != "" {
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) == 2 && strings.ToLower(parts[0]) == "bearer" {
tokenString = parts[1]
}
}
// Fallback to query parameter for img/video tags
if tokenString == "" {
tokenString = c.Query("token")
}
if tokenString == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "missing or invalid authorization",
})
}
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fiber.NewError(fiber.StatusUnauthorized, "unexpected signing method")
}
return []byte(jwtSecret), nil
})
if err != nil || !token.Valid {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "invalid or expired token",
})
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "invalid token claims",
})
}
c.Locals("userID", claims["sub"])
c.Locals("username", claims["username"])
return c.Next()
}
}
// GenerateAccessToken creates a short-lived JWT access token.
func GenerateAccessToken(userID int, username, secret string, expirySecs int) (string, error) {
claims := jwt.MapClaims{
"sub": userID,
"username": username,
"exp": time.Now().Add(time.Duration(expirySecs) * time.Second).Unix(),
"iat": time.Now().Unix(),
"type": "access",
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(secret))
}
// GenerateRefreshToken creates a longer-lived refresh token.
func GenerateRefreshToken(userID int, username, secret string) (string, error) {
claims := jwt.MapClaims{
"sub": userID,
"username": username,
"exp": time.Now().Add(7 * 24 * time.Hour).Unix(), // 7 days
"iat": time.Now().Unix(),
"type": "refresh",
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(secret))
}

View file

@ -0,0 +1,104 @@
package middleware
import (
"sync"
"time"
"github.com/gofiber/fiber/v2"
)
type loginAttempt struct {
Count int
LockedAt time.Time
}
// BruteForceGuard tracks login attempts per IP and enforces exponential backoff.
type BruteForceGuard struct {
mu sync.Mutex
attempts map[string]*loginAttempt
maxAttempts int
lockoutSecs int
}
func NewBruteForceGuard(maxAttempts, lockoutSecs int) *BruteForceGuard {
guard := &BruteForceGuard{
attempts: make(map[string]*loginAttempt),
maxAttempts: maxAttempts,
lockoutSecs: lockoutSecs,
}
// Clean up stale entries every 10 minutes
go func() {
ticker := time.NewTicker(10 * time.Minute)
defer ticker.Stop()
for range ticker.C {
guard.cleanup()
}
}()
return guard
}
// Check returns a Fiber handler that blocks requests from locked-out IPs.
func (g *BruteForceGuard) Check() fiber.Handler {
return func(c *fiber.Ctx) error {
ip := c.IP()
g.mu.Lock()
attempt, exists := g.attempts[ip]
g.mu.Unlock()
if exists && attempt.Count >= g.maxAttempts {
lockoutDuration := time.Duration(g.lockoutSecs) * time.Second
if time.Since(attempt.LockedAt) < lockoutDuration {
remaining := lockoutDuration - time.Since(attempt.LockedAt)
return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{
"error": "too many login attempts, account temporarily locked",
"retry_after": int(remaining.Seconds()),
})
}
// Lockout expired, reset
g.mu.Lock()
delete(g.attempts, ip)
g.mu.Unlock()
}
return c.Next()
}
}
// RecordFailure increments the failure count for an IP.
func (g *BruteForceGuard) RecordFailure(ip string) {
g.mu.Lock()
defer g.mu.Unlock()
attempt, exists := g.attempts[ip]
if !exists {
g.attempts[ip] = &loginAttempt{Count: 1, LockedAt: time.Now()}
return
}
attempt.Count++
if attempt.Count >= g.maxAttempts {
attempt.LockedAt = time.Now()
}
}
// RecordSuccess resets the failure count for an IP after a successful login.
func (g *BruteForceGuard) RecordSuccess(ip string) {
g.mu.Lock()
defer g.mu.Unlock()
delete(g.attempts, ip)
}
func (g *BruteForceGuard) cleanup() {
g.mu.Lock()
defer g.mu.Unlock()
cutoff := time.Duration(g.lockoutSecs*2) * time.Second
for ip, attempt := range g.attempts {
if time.Since(attempt.LockedAt) > cutoff {
delete(g.attempts, ip)
}
}
}

View file

@ -0,0 +1,68 @@
package middleware
import (
"fmt"
"path/filepath"
"strings"
"github.com/gofiber/fiber/v2"
)
// PathJail middleware ensures all file path parameters are confined within
// the storage root. Any path that resolves outside the jail is rejected
// immediately, preventing directory traversal attacks.
func PathJail(storageRoot string) fiber.Handler {
// Resolve the absolute jail root once at startup
absRoot, err := filepath.Abs(storageRoot)
if err != nil {
panic(fmt.Sprintf("pathjail: invalid storage root: %v", err))
}
// Ensure it ends with separator for prefix matching
absRootSlash := absRoot + string(filepath.Separator)
return func(c *fiber.Ctx) error {
// Extract the path parameter from the request.
// Handlers attach the relative file path as "filepath" param or query.
rawPath := c.Params("*")
if rawPath == "" {
rawPath = c.Query("path", "")
}
// If no path param, assume root path '.'
if rawPath == "" {
rawPath = "."
}
// Clean and resolve the path
cleaned := filepath.Clean(rawPath)
// Reject obvious traversal attempts before even joining
if strings.Contains(cleaned, "..") {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"error": "path traversal detected",
})
}
// Build the full absolute path within the jail
fullPath := filepath.Join(absRoot, cleaned)
absPath, err := filepath.Abs(fullPath)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "invalid path",
})
}
// Final check: resolved path must be within the jail root
if absPath != absRoot && !strings.HasPrefix(absPath, absRootSlash) {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"error": "access denied: path outside storage boundary",
})
}
// Store the validated absolute path for downstream handlers
c.Locals("resolvedPath", absPath)
c.Locals("relativePath", cleaned)
return c.Next()
}
}