package server import ( "encoding/base64" "log/slog" "strings" "time" "github.com/gofiber/fiber/v2" "github.com/tg123/go-htpasswd" ) /* Middleware for larc server: Basic Auth, logging, repo access control. */ // AuthMiddleware creates Basic Auth middleware using htpasswd file func AuthMiddleware(htpasswdPath, realm string) fiber.Handler { auth, err := htpasswd.New(htpasswdPath, htpasswd.DefaultSystems, nil) if err != nil { slog.Error("failed to load htpasswd", "path", htpasswdPath, "error", err) return func(c *fiber.Ctx) error { return c.Status(fiber.StatusInternalServerError).SendString("Auth configuration error") } } return func(c *fiber.Ctx) error { /* parse Authorization header */ authHeader := c.Get("Authorization") if authHeader == "" { return unauthorized(c, realm) } if !strings.HasPrefix(authHeader, "Basic ") { return unauthorized(c, realm) } decoded, err := base64.StdEncoding.DecodeString(authHeader[6:]) if err != nil { return unauthorized(c, realm) } parts := strings.SplitN(string(decoded), ":", 2) if len(parts) != 2 { return unauthorized(c, realm) } username, password := parts[0], parts[1] /* validate credentials */ if !auth.Match(username, password) { slog.Warn("auth failed", "username", username, "ip", c.IP()) return unauthorized(c, realm) } /* store username in context */ c.Locals("username", username) return c.Next() } } // OptionalAuthMiddleware tries to authenticate but allows anonymous access func OptionalAuthMiddleware(htpasswdPath string) fiber.Handler { auth, err := htpasswd.New(htpasswdPath, htpasswd.DefaultSystems, nil) if err != nil { slog.Warn("htpasswd not available, anonymous only", "error", err) return func(c *fiber.Ctx) error { c.Locals("username", "") return c.Next() } } return func(c *fiber.Ctx) error { authHeader := c.Get("Authorization") if authHeader == "" || !strings.HasPrefix(authHeader, "Basic ") { c.Locals("username", "") return c.Next() } decoded, err := base64.StdEncoding.DecodeString(authHeader[6:]) if err != nil { c.Locals("username", "") return c.Next() } parts := strings.SplitN(string(decoded), ":", 2) if len(parts) != 2 { c.Locals("username", "") return c.Next() } username, password := parts[0], parts[1] if auth.Match(username, password) { c.Locals("username", username) } else { c.Locals("username", "") } return c.Next() } } // RepoAccessMiddleware checks if user has access to repository func RepoAccessMiddleware(cfg *Config, writeAccess bool) fiber.Handler { return func(c *fiber.Ctx) error { repoName := c.Params("repo") if repoName == "" { return c.Status(fiber.StatusBadRequest).SendString("Repository name required") } repoCfg := cfg.GetRepoConfig(repoName) if repoCfg == nil { return c.Status(fiber.StatusNotFound).SendString("Repository not found") } username, _ := c.Locals("username").(string) if !repoCfg.IsUserAllowed(username, writeAccess) { if username == "" { /* anonymous user - request auth */ return unauthorized(c, cfg.Auth.Realm) } slog.Warn("access denied", "repo", repoName, "user", username, "write", writeAccess) return c.Status(fiber.StatusForbidden).SendString("Access denied") } /* store repo config in context */ c.Locals("repo_config", repoCfg) return c.Next() } } // LoggingMiddleware logs all requests func LoggingMiddleware() fiber.Handler { return func(c *fiber.Ctx) error { start := time.Now() err := c.Next() duration := time.Since(start) username, _ := c.Locals("username").(string) if username == "" { username = "anonymous" } slog.Info("request", "method", c.Method(), "path", c.Path(), "status", c.Response().StatusCode(), "duration_ms", duration.Milliseconds(), "ip", c.IP(), "user", username, ) return err } } func unauthorized(c *fiber.Ctx, realm string) error { c.Set("WWW-Authenticate", `Basic realm="`+realm+`"`) return c.Status(fiber.StatusUnauthorized).SendString("Unauthorized") }