larc r15

161 lines ยท 4.0 KB Raw
1 package server
2
3 import (
4 "encoding/base64"
5 "log/slog"
6 "strings"
7 "time"
8
9 "github.com/gofiber/fiber/v2"
10 "github.com/tg123/go-htpasswd"
11 )
12
13 /* Middleware for larc server: Basic Auth, logging, repo access control. */
14
15 // AuthMiddleware creates Basic Auth middleware using htpasswd file
16 func AuthMiddleware(htpasswdPath, realm string) fiber.Handler {
17 auth, err := htpasswd.New(htpasswdPath, htpasswd.DefaultSystems, nil)
18 if err != nil {
19 slog.Error("failed to load htpasswd", "path", htpasswdPath, "error", err)
20 return func(c *fiber.Ctx) error {
21 return c.Status(fiber.StatusInternalServerError).SendString("Auth configuration error")
22 }
23 }
24
25 return func(c *fiber.Ctx) error {
26 /* parse Authorization header */
27 authHeader := c.Get("Authorization")
28 if authHeader == "" {
29 return unauthorized(c, realm)
30 }
31
32 if !strings.HasPrefix(authHeader, "Basic ") {
33 return unauthorized(c, realm)
34 }
35
36 decoded, err := base64.StdEncoding.DecodeString(authHeader[6:])
37 if err != nil {
38 return unauthorized(c, realm)
39 }
40
41 parts := strings.SplitN(string(decoded), ":", 2)
42 if len(parts) != 2 {
43 return unauthorized(c, realm)
44 }
45
46 username, password := parts[0], parts[1]
47
48 /* validate credentials */
49 if !auth.Match(username, password) {
50 slog.Warn("auth failed", "username", username, "ip", c.IP())
51 return unauthorized(c, realm)
52 }
53
54 /* store username in context */
55 c.Locals("username", username)
56 return c.Next()
57 }
58 }
59
60 // OptionalAuthMiddleware tries to authenticate but allows anonymous access
61 func OptionalAuthMiddleware(htpasswdPath string) fiber.Handler {
62 auth, err := htpasswd.New(htpasswdPath, htpasswd.DefaultSystems, nil)
63 if err != nil {
64 slog.Warn("htpasswd not available, anonymous only", "error", err)
65 return func(c *fiber.Ctx) error {
66 c.Locals("username", "")
67 return c.Next()
68 }
69 }
70
71 return func(c *fiber.Ctx) error {
72 authHeader := c.Get("Authorization")
73 if authHeader == "" || !strings.HasPrefix(authHeader, "Basic ") {
74 c.Locals("username", "")
75 return c.Next()
76 }
77
78 decoded, err := base64.StdEncoding.DecodeString(authHeader[6:])
79 if err != nil {
80 c.Locals("username", "")
81 return c.Next()
82 }
83
84 parts := strings.SplitN(string(decoded), ":", 2)
85 if len(parts) != 2 {
86 c.Locals("username", "")
87 return c.Next()
88 }
89
90 username, password := parts[0], parts[1]
91 if auth.Match(username, password) {
92 c.Locals("username", username)
93 } else {
94 c.Locals("username", "")
95 }
96
97 return c.Next()
98 }
99 }
100
101 // RepoAccessMiddleware checks if user has access to repository
102 func RepoAccessMiddleware(cfg *Config, writeAccess bool) fiber.Handler {
103 return func(c *fiber.Ctx) error {
104 repoName := c.Params("repo")
105 if repoName == "" {
106 return c.Status(fiber.StatusBadRequest).SendString("Repository name required")
107 }
108
109 repoCfg := cfg.GetRepoConfig(repoName)
110 if repoCfg == nil {
111 return c.Status(fiber.StatusNotFound).SendString("Repository not found")
112 }
113
114 username, _ := c.Locals("username").(string)
115
116 if !repoCfg.IsUserAllowed(username, writeAccess) {
117 if username == "" {
118 /* anonymous user - request auth */
119 return unauthorized(c, cfg.Auth.Realm)
120 }
121 slog.Warn("access denied", "repo", repoName, "user", username, "write", writeAccess)
122 return c.Status(fiber.StatusForbidden).SendString("Access denied")
123 }
124
125 /* store repo config in context */
126 c.Locals("repo_config", repoCfg)
127 return c.Next()
128 }
129 }
130
131 // LoggingMiddleware logs all requests
132 func LoggingMiddleware() fiber.Handler {
133 return func(c *fiber.Ctx) error {
134 start := time.Now()
135
136 err := c.Next()
137
138 duration := time.Since(start)
139 username, _ := c.Locals("username").(string)
140 if username == "" {
141 username = "anonymous"
142 }
143
144 slog.Info("request",
145 "method", c.Method(),
146 "path", c.Path(),
147 "status", c.Response().StatusCode(),
148 "duration_ms", duration.Milliseconds(),
149 "ip", c.IP(),
150 "user", username,
151 )
152
153 return err
154 }
155 }
156
157 func unauthorized(c *fiber.Ctx, realm string) error {
158 c.Set("WWW-Authenticate", `Basic realm="`+realm+`"`)
159 return c.Status(fiber.StatusUnauthorized).SendString("Unauthorized")
160 }
161