@@ -152,74 +152,13 @@ func (h *Handler) SetPostAuthHook(hook coreauth.PostAuthHook) {
152152// All requests (local and remote) require a valid management key.
153153// Additionally, remote access requires allow-remote-management=true.
154154func (h * Handler ) Middleware () gin.HandlerFunc {
155- const maxFailures = 5
156- const banDuration = 30 * time .Minute
157-
158155 return func (c * gin.Context ) {
159156 c .Header ("X-CPA-VERSION" , buildinfo .Version )
160157 c .Header ("X-CPA-COMMIT" , buildinfo .Commit )
161158 c .Header ("X-CPA-BUILD-DATE" , buildinfo .BuildDate )
162159
163160 clientIP := c .ClientIP ()
164161 localClient := clientIP == "127.0.0.1" || clientIP == "::1"
165- cfg := h .cfg
166- var (
167- allowRemote bool
168- secretHash string
169- )
170- if cfg != nil {
171- allowRemote = cfg .RemoteManagement .AllowRemote
172- secretHash = cfg .RemoteManagement .SecretKey
173- }
174- if h .allowRemoteOverride {
175- allowRemote = true
176- }
177- envSecret := h .envSecret
178-
179- fail := func () {}
180- if ! localClient {
181- h .attemptsMu .Lock ()
182- ai := h .failedAttempts [clientIP ]
183- if ai != nil {
184- if ! ai .blockedUntil .IsZero () {
185- if time .Now ().Before (ai .blockedUntil ) {
186- remaining := time .Until (ai .blockedUntil ).Round (time .Second )
187- h .attemptsMu .Unlock ()
188- c .AbortWithStatusJSON (http .StatusForbidden , gin.H {"error" : fmt .Sprintf ("IP banned due to too many failed attempts. Try again in %s" , remaining )})
189- return
190- }
191- // Ban expired, reset state
192- ai .blockedUntil = time.Time {}
193- ai .count = 0
194- }
195- }
196- h .attemptsMu .Unlock ()
197-
198- if ! allowRemote {
199- c .AbortWithStatusJSON (http .StatusForbidden , gin.H {"error" : "remote management disabled" })
200- return
201- }
202-
203- fail = func () {
204- h .attemptsMu .Lock ()
205- aip := h .failedAttempts [clientIP ]
206- if aip == nil {
207- aip = & attemptInfo {}
208- h .failedAttempts [clientIP ] = aip
209- }
210- aip .count ++
211- aip .lastActivity = time .Now ()
212- if aip .count >= maxFailures {
213- aip .blockedUntil = time .Now ().Add (banDuration )
214- aip .count = 0
215- }
216- h .attemptsMu .Unlock ()
217- }
218- }
219- if secretHash == "" && envSecret == "" {
220- c .AbortWithStatusJSON (http .StatusForbidden , gin.H {"error" : "remote management key not set" })
221- return
222- }
223162
224163 // Accept either Authorization: Bearer <key> or X-Management-Key
225164 var provided string
@@ -235,55 +174,114 @@ func (h *Handler) Middleware() gin.HandlerFunc {
235174 provided = c .GetHeader ("X-Management-Key" )
236175 }
237176
238- if provided == "" {
239- if ! localClient {
240- fail ()
241- }
242- c .AbortWithStatusJSON (http .StatusUnauthorized , gin.H {"error" : "missing management key" })
177+ allowed , statusCode , errMsg := h .AuthenticateManagementKey (clientIP , localClient , provided )
178+ if ! allowed {
179+ c .AbortWithStatusJSON (statusCode , gin.H {"error" : errMsg })
243180 return
244181 }
182+ c .Next ()
183+ }
184+ }
245185
246- if localClient {
247- if lp := h .localPassword ; lp != "" {
248- if subtle .ConstantTimeCompare ([]byte (provided ), []byte (lp )) == 1 {
249- c .Next ()
250- return
251- }
252- }
186+ // AuthenticateManagementKey verifies the provided management key for the given client.
187+ // It mirrors the behaviour of Middleware() so non-HTTP callers can reuse the same logic.
188+ func (h * Handler ) AuthenticateManagementKey (clientIP string , localClient bool , provided string ) (bool , int , string ) {
189+ const maxFailures = 5
190+ const banDuration = 30 * time .Minute
191+
192+ if h == nil {
193+ return false , http .StatusForbidden , "remote management disabled"
194+ }
195+
196+ cfg := h .cfg
197+ var (
198+ allowRemote bool
199+ secretHash string
200+ )
201+ if cfg != nil {
202+ allowRemote = cfg .RemoteManagement .AllowRemote
203+ secretHash = cfg .RemoteManagement .SecretKey
204+ }
205+ if h .allowRemoteOverride {
206+ allowRemote = true
207+ }
208+ envSecret := h .envSecret
209+
210+ now := time .Now ()
211+ h .attemptsMu .Lock ()
212+ ai := h .failedAttempts [clientIP ]
213+ if ai != nil && ! ai .blockedUntil .IsZero () {
214+ if now .Before (ai .blockedUntil ) {
215+ remaining := ai .blockedUntil .Sub (now ).Round (time .Second )
216+ h .attemptsMu .Unlock ()
217+ return false , http .StatusForbidden , fmt .Sprintf ("IP banned due to too many failed attempts. Try again in %s" , remaining )
253218 }
219+ // Ban expired, reset state
220+ ai .blockedUntil = time.Time {}
221+ ai .count = 0
222+ }
223+ h .attemptsMu .Unlock ()
254224
255- if envSecret != "" && subtle .ConstantTimeCompare ([]byte (provided ), []byte (envSecret )) == 1 {
256- if ! localClient {
257- h .attemptsMu .Lock ()
258- if ai := h .failedAttempts [clientIP ]; ai != nil {
259- ai .count = 0
260- ai .blockedUntil = time.Time {}
261- }
262- h .attemptsMu .Unlock ()
263- }
264- c .Next ()
265- return
225+ if ! localClient && ! allowRemote {
226+ return false , http .StatusForbidden , "remote management disabled"
227+ }
228+
229+ fail := func () {
230+ h .attemptsMu .Lock ()
231+ aip := h .failedAttempts [clientIP ]
232+ if aip == nil {
233+ aip = & attemptInfo {}
234+ h .failedAttempts [clientIP ] = aip
266235 }
236+ aip .count ++
237+ aip .lastActivity = time .Now ()
238+ if aip .count >= maxFailures {
239+ aip .blockedUntil = time .Now ().Add (banDuration )
240+ aip .count = 0
241+ }
242+ h .attemptsMu .Unlock ()
243+ }
267244
268- if secretHash == "" || bcrypt .CompareHashAndPassword ([]byte (secretHash ), []byte (provided )) != nil {
269- if ! localClient {
270- fail ()
271- }
272- c .AbortWithStatusJSON (http .StatusUnauthorized , gin.H {"error" : "invalid management key" })
273- return
245+ reset := func () {
246+ h .attemptsMu .Lock ()
247+ if ai := h .failedAttempts [clientIP ]; ai != nil {
248+ ai .count = 0
249+ ai .blockedUntil = time.Time {}
274250 }
251+ h .attemptsMu .Unlock ()
252+ }
253+
254+ if secretHash == "" && envSecret == "" {
255+ return false , http .StatusForbidden , "remote management key not set"
256+ }
275257
276- if ! localClient {
277- h .attemptsMu .Lock ()
278- if ai := h .failedAttempts [clientIP ]; ai != nil {
279- ai .count = 0
280- ai .blockedUntil = time.Time {}
258+ if provided == "" {
259+ fail ()
260+ return false , http .StatusUnauthorized , "missing management key"
261+ }
262+
263+ if localClient {
264+ if lp := h .localPassword ; lp != "" {
265+ if subtle .ConstantTimeCompare ([]byte (provided ), []byte (lp )) == 1 {
266+ reset ()
267+ return true , 0 , ""
281268 }
282- h .attemptsMu .Unlock ()
283269 }
270+ }
284271
285- c .Next ()
272+ if envSecret != "" && subtle .ConstantTimeCompare ([]byte (provided ), []byte (envSecret )) == 1 {
273+ reset ()
274+ return true , 0 , ""
275+ }
276+
277+ if secretHash == "" || bcrypt .CompareHashAndPassword ([]byte (secretHash ), []byte (provided )) != nil {
278+ fail ()
279+ return false , http .StatusUnauthorized , "invalid management key"
286280 }
281+
282+ reset ()
283+
284+ return true , 0 , ""
287285}
288286
289287// persist saves the current in-memory config to disk.
0 commit comments