1
- package traefik_queue_manager
1
+ package queuemanager
2
2
3
3
import (
4
4
"context"
5
- "crypto/md5"
5
+ "crypto/rand"
6
+ "crypto/sha256"
6
7
"encoding/hex"
7
8
"fmt"
8
9
"html/template"
9
10
"log"
10
11
"math"
11
12
"net/http"
12
13
"os"
14
+ "strconv"
13
15
"strings"
14
16
"time"
15
17
16
18
"github.com/patrickmn/go-cache"
17
19
)
18
20
19
- // Config holds the plugin configuration
21
+ // Config holds the plugin configuration.
20
22
type Config struct {
21
23
Enabled bool `json:"enabled"` // Enable/disable the queue manager
22
24
QueuePageFile string `json:"queuePageFile"` // Path to queue page HTML template
23
25
SessionTime time.Duration `json:"sessionTime"` // How long a session is valid for
24
26
PurgeTime time.Duration `json:"purgeTime"` // How often to purge expired sessions
25
27
MaxEntries int `json:"maxEntries"` // Maximum concurrent users
26
- HttpResponseCode int `json:"httpResponseCode"` // HTTP response code for queue page
27
- HttpContentType string `json:"httpContentType"` // Content type of queue page
28
+ HTTPResponseCode int `json:"httpResponseCode"` // HTTP response code for queue page
29
+ HTTPContentType string `json:"httpContentType"` // Content type of queue page
28
30
UseCookies bool `json:"useCookies"` // Use cookies or IP+UserAgent hash
29
31
CookieName string `json:"cookieName"` // Name of the cookie
30
32
CookieMaxAge int `json:"cookieMaxAge"` // Max age of the cookie in seconds
@@ -33,16 +35,16 @@ type Config struct {
33
35
Debug bool `json:"debug"` // Enable debug logging
34
36
}
35
37
36
- // CreateConfig creates the default plugin configuration
38
+ // CreateConfig creates the default plugin configuration.
37
39
func CreateConfig () * Config {
38
40
return & Config {
39
41
Enabled : true ,
40
42
QueuePageFile : "queue-page.html" ,
41
43
SessionTime : 1 * time .Minute ,
42
44
PurgeTime : 5 * time .Minute ,
43
45
MaxEntries : 100 ,
44
- HttpResponseCode : http .StatusTooManyRequests ,
45
- HttpContentType : "text/html; charset=utf-8" ,
46
+ HTTPResponseCode : http .StatusTooManyRequests ,
47
+ HTTPContentType : "text/html; charset=utf-8" ,
46
48
UseCookies : true ,
47
49
CookieName : "queue-manager-id" ,
48
50
CookieMaxAge : 3600 ,
@@ -52,15 +54,15 @@ func CreateConfig() *Config {
52
54
}
53
55
}
54
56
55
- // Session represents a visitor session
57
+ // Session represents a visitor session.
56
58
type Session struct {
57
59
ID string `json:"id"` // Unique client identifier
58
60
CreatedAt time.Time `json:"createdAt"` // When the session was created
59
61
LastSeen time.Time `json:"lastSeen"` // When the client was last seen
60
62
Position int `json:"position"` // Position in the queue
61
63
}
62
64
63
- // QueuePageData contains data to be passed to the HTML template
65
+ // QueuePageData contains data to be passed to the HTML template.
64
66
type QueuePageData struct {
65
67
Position int `json:"position"` // Position in queue
66
68
QueueSize int `json:"queueSize"` // Total queue size
@@ -70,7 +72,7 @@ type QueuePageData struct {
70
72
Message string `json:"message"` // Custom message
71
73
}
72
74
73
- // QueueManager is the middleware handler
75
+ // QueueManager is the middleware handler.
74
76
type QueueManager struct {
75
77
next http.Handler
76
78
name string
@@ -81,7 +83,7 @@ type QueueManager struct {
81
83
activeSessionIDs map [string ]bool
82
84
}
83
85
84
- // New creates a new queue manager middleware
86
+ // New creates a new queue manager middleware.
85
87
func New (ctx context.Context , next http.Handler , config * Config , name string ) (http.Handler , error ) {
86
88
// Validate configuration
87
89
if config .MaxEntries <= 0 {
@@ -115,7 +117,7 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
115
117
}, nil
116
118
}
117
119
118
- // ServeHTTP implements the http.Handler interface
120
+ // ServeHTTP implements the http.Handler interface.
119
121
func (qm * QueueManager ) ServeHTTP (rw http.ResponseWriter , req * http.Request ) {
120
122
// Skip if disabled
121
123
if ! qm .config .Enabled {
@@ -136,9 +138,15 @@ func (qm *QueueManager) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
136
138
if qm .activeSessionIDs [clientID ] {
137
139
// Update last seen timestamp
138
140
if session , found := qm .cache .Get (clientID ); found {
139
- sessionData := session .(Session )
140
- sessionData .LastSeen = time .Now ()
141
- qm .cache .Set (clientID , sessionData , cache .DefaultExpiration )
141
+ sessionData , ok := session .(Session )
142
+ if ! ok {
143
+ if qm .config .Debug {
144
+ log .Printf ("[Queue Manager] Error: Failed to convert session to Session type" )
145
+ }
146
+ } else {
147
+ sessionData .LastSeen = time .Now ()
148
+ qm .cache .Set (clientID , sessionData , cache .DefaultExpiration )
149
+ }
142
150
}
143
151
144
152
// Allow access
@@ -190,16 +198,22 @@ func (qm *QueueManager) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
190
198
191
199
// Update last seen timestamp
192
200
if session , found := qm .cache .Get (clientID ); found {
193
- sessionData := session .(Session )
194
- sessionData .LastSeen = time .Now ()
195
- qm .cache .Set (clientID , sessionData , cache .DefaultExpiration )
201
+ sessionData , ok := session .(Session )
202
+ if ! ok {
203
+ if qm .config .Debug {
204
+ log .Printf ("[Queue Manager] Error: Failed to convert session to Session type" )
205
+ }
206
+ } else {
207
+ sessionData .LastSeen = time .Now ()
208
+ qm .cache .Set (clientID , sessionData , cache .DefaultExpiration )
209
+ }
196
210
}
197
211
198
212
// Serve queue page
199
- qm .serveQueuePage (rw , req , position )
213
+ qm .serveQueuePage (rw , position )
200
214
}
201
215
202
- // getClientID generates or retrieves a unique client identifier
216
+ // getClientID generates or retrieves a unique client identifier.
203
217
func (qm * QueueManager ) getClientID (rw http.ResponseWriter , req * http.Request ) (string , bool ) {
204
218
if qm .config .UseCookies {
205
219
// Try to get existing cookie
@@ -220,36 +234,41 @@ func (qm *QueueManager) getClientID(rw http.ResponseWriter, req *http.Request) (
220
234
SameSite : http .SameSiteLaxMode ,
221
235
})
222
236
return newID , true
223
- } else {
224
- // Use IP + UserAgent hash
225
- return generateClientHash (req ), false
226
237
}
238
+
239
+ // Use IP + UserAgent hash
240
+ return generateClientHash (req ), false
227
241
}
228
242
229
- // generateUniqueID creates a unique identifier for a client
243
+ // generateUniqueID creates a unique identifier for a client.
230
244
func generateUniqueID (req * http.Request ) string {
231
- // Create unique ID based on current time, remote IP, and cryptographic randomness
232
- timestamp := time . Now (). UnixNano ( )
233
- clientIP := getClientIP ( req )
234
-
235
- // Add randomness to ensure uniqueness
236
- randBytes := make ([] byte , 8 )
237
- for i := range randBytes {
238
- randBytes [i ] = byte (timestamp % 256 )
239
- timestamp /= 256
245
+ // Create a buffer for true randomness
246
+ randBytes := make ([] byte , 16 )
247
+ _ , err := rand . Read ( randBytes )
248
+ if err != nil {
249
+ // If crypto/rand fails, use a fallback method
250
+ timestamp := time . Now (). UnixNano ( )
251
+ for i := range randBytes {
252
+ randBytes [i ] = byte (( timestamp + int64 ( i )) % 256 )
253
+ }
240
254
}
241
255
242
- // Create a hash from the random bytes
243
- hasher := md5 .New ()
256
+ // Add client IP to the randomness
257
+ clientIP := getClientIP (req )
258
+
259
+ // Create a hash of the random bytes + IP
260
+ hasher := sha256 .New ()
244
261
hasher .Write (randBytes )
245
262
hasher .Write ([]byte (clientIP ))
246
- randHash := hex .EncodeToString (hasher .Sum (nil ))[:12 ]
263
+ hasher .Write ([]byte (strconv .FormatInt (time .Now ().UnixNano (), 10 )))
264
+
265
+ randHash := hex .EncodeToString (hasher .Sum (nil ))[:16 ]
247
266
248
267
// Format: timestamp-ip-randomhash
249
268
return fmt .Sprintf ("%d-%s-%s" , time .Now ().UnixNano (), clientIP , randHash )
250
269
}
251
270
252
- // generateClientHash creates a hash from client attributes
271
+ // generateClientHash creates a hash from client attributes.
253
272
func generateClientHash (req * http.Request ) string {
254
273
// Get client IP
255
274
clientIP := getClientIP (req )
@@ -258,12 +277,12 @@ func generateClientHash(req *http.Request) string {
258
277
userAgent := req .UserAgent ()
259
278
260
279
// Create hash
261
- hasher := md5 .New ()
280
+ hasher := sha256 .New ()
262
281
hasher .Write ([]byte (clientIP + "|" + userAgent ))
263
- return hex .EncodeToString (hasher .Sum (nil ))
282
+ return hex .EncodeToString (hasher .Sum (nil ))[: 32 ]
264
283
}
265
284
266
- // getClientIP extracts the client's real IP address
285
+ // getClientIP extracts the client's real IP address.
267
286
func getClientIP (req * http.Request ) string {
268
287
// Check for X-Forwarded-For header
269
288
if xff := req .Header .Get ("X-Forwarded-For" ); xff != "" {
@@ -289,8 +308,8 @@ func getClientIP(req *http.Request) string {
289
308
return remoteAddr
290
309
}
291
310
292
- // serveQueuePage serves the queue page HTML
293
- func (qm * QueueManager ) serveQueuePage (rw http.ResponseWriter , req * http. Request , position int ) {
311
+ // serveQueuePage serves the queue page HTML.
312
+ func (qm * QueueManager ) serveQueuePage (rw http.ResponseWriter , position int ) {
294
313
// Calculate estimated wait time (rough estimate: 30 seconds per position)
295
314
estimatedWaitTime := int (math .Ceil (float64 (position ) * 0.5 )) // in minutes
296
315
@@ -310,26 +329,23 @@ func (qm *QueueManager) serveQueuePage(rw http.ResponseWriter, req *http.Request
310
329
Message : "Please wait while we process your request." ,
311
330
}
312
331
313
- // Check if we have a template file
332
+ // Try to use the template file
314
333
if fileExists (qm .config .QueuePageFile ) {
315
- // Read the file content
316
334
content , err := os .ReadFile (qm .config .QueuePageFile )
317
335
if err == nil {
318
- // Create a new template from the file content
319
- tmpl , err := template .New ("QueuePage" ).Delims ("[[" , "]]" ).Parse (string (content ))
320
- if err == nil {
336
+ queueTemplate , parseErr := template .New ("QueuePage" ).Delims ("[[" , "]]" ).Parse (string (content ))
337
+ if parseErr == nil {
321
338
// Set content type
322
- rw .Header ().Set ("Content-Type" , qm .config .HttpContentType )
323
- rw .WriteHeader (qm .config .HttpResponseCode )
339
+ rw .Header ().Set ("Content-Type" , qm .config .HTTPContentType )
340
+ rw .WriteHeader (qm .config .HTTPResponseCode )
324
341
325
342
// Execute template
326
- err = tmpl .Execute (rw , data )
327
- if err != nil && qm .config .Debug {
328
- log .Printf ("[Queue Manager] Error executing template: %v" , err )
343
+ if execErr := queueTemplate .Execute (rw , data ); execErr != nil && qm .config .Debug {
344
+ log .Printf ("[Queue Manager] Error executing template: %v" , execErr )
329
345
}
330
346
return
331
347
} else if qm .config .Debug {
332
- log .Printf ("[Queue Manager] Error parsing template: %v" , err )
348
+ log .Printf ("[Queue Manager] Error parsing template: %v" , parseErr )
333
349
}
334
350
} else if qm .config .Debug {
335
351
log .Printf ("[Queue Manager] Error reading template file: %v" , err )
@@ -391,16 +407,16 @@ func (qm *QueueManager) serveQueuePage(rw http.ResponseWriter, req *http.Request
391
407
392
408
// Create and execute the fallback template
393
409
tmpl , _ := template .New ("FallbackQueuePage" ).Delims ("[[" , "]]" ).Parse (fallbackTemplate )
394
- rw .Header ().Set ("Content-Type" , qm .config .HttpContentType )
395
- rw .WriteHeader (qm .config .HttpResponseCode )
396
- err := tmpl .Execute (rw , data )
397
- if err != nil && qm .config .Debug {
410
+ rw .Header ().Set ("Content-Type" , qm .config .HTTPContentType )
411
+ rw .WriteHeader (qm .config .HTTPResponseCode )
412
+ if err := tmpl .Execute (rw , data ); err != nil && qm .config .Debug {
398
413
log .Printf ("[Queue Manager] Error executing fallback template: %v" , err )
399
414
}
400
415
}
401
416
402
- // Periodically check and clean up expired sessions
403
- func (qm * QueueManager ) cleanupExpiredSessions () {
417
+ // CleanupExpiredSessions periodically checks and removes expired sessions.
418
+ // This should be called periodically, e.g., using a background goroutine.
419
+ func (qm * QueueManager ) CleanupExpiredSessions () {
404
420
// Check if any active sessions have expired
405
421
for id := range qm .activeSessionIDs {
406
422
if _ , found := qm .cache .Get (id ); ! found {
@@ -430,14 +446,16 @@ func (qm *QueueManager) cleanupExpiredSessions() {
430
446
// Update positions in queue
431
447
for i := range qm .queue {
432
448
if session , found := qm .cache .Get (qm .queue [i ].ID ); found {
433
- sessionData := session .(Session )
434
- sessionData .Position = i
435
- qm .cache .Set (qm .queue [i ].ID , sessionData , cache .DefaultExpiration )
449
+ sessionData , ok := session .(Session )
450
+ if ok {
451
+ sessionData .Position = i
452
+ qm .cache .Set (qm .queue [i ].ID , sessionData , cache .DefaultExpiration )
453
+ }
436
454
}
437
455
}
438
456
}
439
457
440
- // Helper function to check if a file exists
458
+ // fileExists checks if a file exists.
441
459
func fileExists (path string ) bool {
442
460
_ , err := os .Stat (path )
443
461
return err == nil
0 commit comments