1- package traefik_queue_manager
1+ package queuemanager
22
33import (
44 "context"
5- "crypto/md5"
5+ "crypto/rand"
6+ "crypto/sha256"
67 "encoding/hex"
78 "fmt"
89 "html/template"
910 "log"
1011 "math"
1112 "net/http"
1213 "os"
14+ "strconv"
1315 "strings"
1416 "time"
1517
1618 "github.com/patrickmn/go-cache"
1719)
1820
19- // Config holds the plugin configuration
21+ // Config holds the plugin configuration.
2022type Config struct {
2123 Enabled bool `json:"enabled"` // Enable/disable the queue manager
2224 QueuePageFile string `json:"queuePageFile"` // Path to queue page HTML template
2325 SessionTime time.Duration `json:"sessionTime"` // How long a session is valid for
2426 PurgeTime time.Duration `json:"purgeTime"` // How often to purge expired sessions
2527 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
2830 UseCookies bool `json:"useCookies"` // Use cookies or IP+UserAgent hash
2931 CookieName string `json:"cookieName"` // Name of the cookie
3032 CookieMaxAge int `json:"cookieMaxAge"` // Max age of the cookie in seconds
@@ -33,16 +35,16 @@ type Config struct {
3335 Debug bool `json:"debug"` // Enable debug logging
3436}
3537
36- // CreateConfig creates the default plugin configuration
38+ // CreateConfig creates the default plugin configuration.
3739func CreateConfig () * Config {
3840 return & Config {
3941 Enabled : true ,
4042 QueuePageFile : "queue-page.html" ,
4143 SessionTime : 1 * time .Minute ,
4244 PurgeTime : 5 * time .Minute ,
4345 MaxEntries : 100 ,
44- HttpResponseCode : http .StatusTooManyRequests ,
45- HttpContentType : "text/html; charset=utf-8" ,
46+ HTTPResponseCode : http .StatusTooManyRequests ,
47+ HTTPContentType : "text/html; charset=utf-8" ,
4648 UseCookies : true ,
4749 CookieName : "queue-manager-id" ,
4850 CookieMaxAge : 3600 ,
@@ -52,15 +54,15 @@ func CreateConfig() *Config {
5254 }
5355}
5456
55- // Session represents a visitor session
57+ // Session represents a visitor session.
5658type Session struct {
5759 ID string `json:"id"` // Unique client identifier
5860 CreatedAt time.Time `json:"createdAt"` // When the session was created
5961 LastSeen time.Time `json:"lastSeen"` // When the client was last seen
6062 Position int `json:"position"` // Position in the queue
6163}
6264
63- // QueuePageData contains data to be passed to the HTML template
65+ // QueuePageData contains data to be passed to the HTML template.
6466type QueuePageData struct {
6567 Position int `json:"position"` // Position in queue
6668 QueueSize int `json:"queueSize"` // Total queue size
@@ -70,7 +72,7 @@ type QueuePageData struct {
7072 Message string `json:"message"` // Custom message
7173}
7274
73- // QueueManager is the middleware handler
75+ // QueueManager is the middleware handler.
7476type QueueManager struct {
7577 next http.Handler
7678 name string
@@ -81,7 +83,7 @@ type QueueManager struct {
8183 activeSessionIDs map [string ]bool
8284}
8385
84- // New creates a new queue manager middleware
86+ // New creates a new queue manager middleware.
8587func New (ctx context.Context , next http.Handler , config * Config , name string ) (http.Handler , error ) {
8688 // Validate configuration
8789 if config .MaxEntries <= 0 {
@@ -115,7 +117,7 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
115117 }, nil
116118}
117119
118- // ServeHTTP implements the http.Handler interface
120+ // ServeHTTP implements the http.Handler interface.
119121func (qm * QueueManager ) ServeHTTP (rw http.ResponseWriter , req * http.Request ) {
120122 // Skip if disabled
121123 if ! qm .config .Enabled {
@@ -136,9 +138,15 @@ func (qm *QueueManager) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
136138 if qm .activeSessionIDs [clientID ] {
137139 // Update last seen timestamp
138140 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+ }
142150 }
143151
144152 // Allow access
@@ -190,16 +198,22 @@ func (qm *QueueManager) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
190198
191199 // Update last seen timestamp
192200 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+ }
196210 }
197211
198212 // Serve queue page
199- qm .serveQueuePage (rw , req , position )
213+ qm .serveQueuePage (rw , position )
200214}
201215
202- // getClientID generates or retrieves a unique client identifier
216+ // getClientID generates or retrieves a unique client identifier.
203217func (qm * QueueManager ) getClientID (rw http.ResponseWriter , req * http.Request ) (string , bool ) {
204218 if qm .config .UseCookies {
205219 // Try to get existing cookie
@@ -220,36 +234,41 @@ func (qm *QueueManager) getClientID(rw http.ResponseWriter, req *http.Request) (
220234 SameSite : http .SameSiteLaxMode ,
221235 })
222236 return newID , true
223- } else {
224- // Use IP + UserAgent hash
225- return generateClientHash (req ), false
226237 }
238+
239+ // Use IP + UserAgent hash
240+ return generateClientHash (req ), false
227241}
228242
229- // generateUniqueID creates a unique identifier for a client
243+ // generateUniqueID creates a unique identifier for a client.
230244func 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+ }
240254 }
241255
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 ()
244261 hasher .Write (randBytes )
245262 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 ]
247266
248267 // Format: timestamp-ip-randomhash
249268 return fmt .Sprintf ("%d-%s-%s" , time .Now ().UnixNano (), clientIP , randHash )
250269}
251270
252- // generateClientHash creates a hash from client attributes
271+ // generateClientHash creates a hash from client attributes.
253272func generateClientHash (req * http.Request ) string {
254273 // Get client IP
255274 clientIP := getClientIP (req )
@@ -258,12 +277,12 @@ func generateClientHash(req *http.Request) string {
258277 userAgent := req .UserAgent ()
259278
260279 // Create hash
261- hasher := md5 .New ()
280+ hasher := sha256 .New ()
262281 hasher .Write ([]byte (clientIP + "|" + userAgent ))
263- return hex .EncodeToString (hasher .Sum (nil ))
282+ return hex .EncodeToString (hasher .Sum (nil ))[: 32 ]
264283}
265284
266- // getClientIP extracts the client's real IP address
285+ // getClientIP extracts the client's real IP address.
267286func getClientIP (req * http.Request ) string {
268287 // Check for X-Forwarded-For header
269288 if xff := req .Header .Get ("X-Forwarded-For" ); xff != "" {
@@ -289,8 +308,8 @@ func getClientIP(req *http.Request) string {
289308 return remoteAddr
290309}
291310
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 ) {
294313 // Calculate estimated wait time (rough estimate: 30 seconds per position)
295314 estimatedWaitTime := int (math .Ceil (float64 (position ) * 0.5 )) // in minutes
296315
@@ -310,26 +329,23 @@ func (qm *QueueManager) serveQueuePage(rw http.ResponseWriter, req *http.Request
310329 Message : "Please wait while we process your request." ,
311330 }
312331
313- // Check if we have a template file
332+ // Try to use the template file
314333 if fileExists (qm .config .QueuePageFile ) {
315- // Read the file content
316334 content , err := os .ReadFile (qm .config .QueuePageFile )
317335 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 {
321338 // 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 )
324341
325342 // 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 )
329345 }
330346 return
331347 } 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 )
333349 }
334350 } else if qm .config .Debug {
335351 log .Printf ("[Queue Manager] Error reading template file: %v" , err )
@@ -391,16 +407,16 @@ func (qm *QueueManager) serveQueuePage(rw http.ResponseWriter, req *http.Request
391407
392408 // Create and execute the fallback template
393409 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 {
398413 log .Printf ("[Queue Manager] Error executing fallback template: %v" , err )
399414 }
400415}
401416
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 () {
404420 // Check if any active sessions have expired
405421 for id := range qm .activeSessionIDs {
406422 if _ , found := qm .cache .Get (id ); ! found {
@@ -430,14 +446,16 @@ func (qm *QueueManager) cleanupExpiredSessions() {
430446 // Update positions in queue
431447 for i := range qm .queue {
432448 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+ }
436454 }
437455 }
438456}
439457
440- // Helper function to check if a file exists
458+ // fileExists checks if a file exists.
441459func fileExists (path string ) bool {
442460 _ , err := os .Stat (path )
443461 return err == nil
0 commit comments