@@ -69,6 +69,7 @@ type CacheConfig struct {
6969 StaleExpiry time.Duration
7070 EnableProfile bool
7171 ProfilePort string
72+ SecretKey string
7273}
7374
7475type CacheEntry struct {
@@ -132,6 +133,7 @@ func loadConfig() *CacheConfig {
132133 StaleExpiry : time .Duration (getEnvInt ("STALE_TTL" , 432000 )) * time .Second , // 5 days = 432000 seconds
133134 EnableProfile : getEnvBool ("ENABLE_PROFILE" , true ),
134135 ProfilePort : getEnv ("PROFILE_PORT" , "6060" ),
136+ SecretKey : getEnv ("SECRET_KEY" , "changeme" ),
135137 }
136138 })
137139 return cachedConfig
@@ -160,12 +162,17 @@ func main() {
160162 // Register HTTP handler
161163 http .HandleFunc ("/" , handleRequest )
162164
165+ // Register cache listing endpoint
166+ http .HandleFunc ("/cache/list" , handleSecuredCacheList )
167+
163168 // Log startup information
164169 infoLog ("FPC Server starting:\n " )
165170 infoLog ("- Port: %s\n " , port )
166171 infoLog ("- Backend: %s://%s\n " , map [bool ]string {true : "https" , false : "http" }[config .UseHTTPS ], config .Host )
167172 infoLog ("- Redis: %s:%s (DB: %d)\n " , config .RedisHost , config .RedisPort , config .RedisDB )
168173 infoLog ("- Cache: %v (TTL: %.0fs)\n " , config .UseCache , config .CacheTTL .Seconds ())
174+ infoLog ("- Cache List URL: http://localhost:%s/cache/list (Secret Key Required)\n " , port )
175+ infoLog ("- Cache List JSON: http://localhost:%s/cache/list?format=json\n " , port )
169176
170177
171178 fmt .Println (`
@@ -658,3 +665,103 @@ func getEnvInt(key string, defaultValue int) int {
658665 }
659666 return defaultValue
660667}
668+
669+ // First, define a common struct type to use in both places
670+ type CacheKeyInfo struct {
671+ Key string `json:"key"`
672+ Size int `json:"size"`
673+ ExpiredAt string `json:"expired_at,omitempty"`
674+ IsStale bool `json:"is_stale"`
675+ }
676+
677+ // Update the handleSecuredCacheList function
678+ func handleSecuredCacheList (w http.ResponseWriter , r * http.Request ) {
679+ config := loadConfig ()
680+
681+ // Check secret key from header or query parameter
682+ secretKey := r .Header .Get ("X-Secret-Key" )
683+ if secretKey == "" {
684+ // as GET query parameter
685+ secretKey = r .URL .Query ().Get ("key" )
686+ }
687+
688+ if secretKey != config .SecretKey {
689+ w .WriteHeader (http .StatusUnauthorized )
690+ errorLog ("Unauthorized cache list access attempt\n " )
691+ return
692+ }
693+
694+ format := r .URL .Query ().Get ("format" )
695+ var cacheKeys []CacheKeyInfo
696+
697+ // Get items from local cache
698+ for k , item := range localCache .Items () {
699+ if entry , ok := item .Object .(CacheEntry ); ok {
700+ cacheKeys = append (cacheKeys , CacheKeyInfo {
701+ Key : k ,
702+ Size : len (entry .Content ),
703+ ExpiredAt : time .Unix (0 , item .Expiration ).Format (time .RFC3339 ),
704+ IsStale : entry .Expired ,
705+ })
706+ }
707+ }
708+
709+ switch format {
710+ case "json" :
711+ w .Header ().Set ("Content-Type" , "application/json" )
712+ json .NewEncoder (w ).Encode (cacheKeys )
713+ default :
714+ w .Header ().Set ("Content-Type" , "text/html" )
715+ serveHTMLCacheList (w , cacheKeys )
716+ }
717+ }
718+
719+ func serveHTMLCacheList (w http.ResponseWriter , keys []CacheKeyInfo ) {
720+ tmpl := `<!DOCTYPE html>
721+ <html>
722+ <head>
723+ <title>FPC Cache Keys</title>
724+ <style>
725+ body { font-family: Arial, sans-serif; padding: 20px; }
726+ table { border-collapse: collapse; width: 100%; margin-top: 20px; }
727+ th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
728+ tr:nth-child(even) { background-color: #f9f9f9; }
729+ th { background-color: #4CAF50; color: white; }
730+ .stale { color: #ff9800; }
731+ .active { color: #4CAF50; }
732+ .count { font-size: 1.2em; margin-bottom: 20px; }
733+ </style>
734+ </head>
735+ <body>
736+ <h1>FPC Cache Keys</h1>
737+ <div class="count">Total Keys: %d</div>
738+ <table>
739+ <tr>
740+ <th>Key</th>
741+ <th>Size</th>
742+ <th>Expires</th>
743+ <th>Status</th>
744+ </tr>
745+ %s
746+ </table>
747+ </body>
748+ </html>`
749+
750+ var rows string
751+ for _ , k := range keys {
752+ status := "active"
753+ statusClass := "active"
754+ if k .IsStale {
755+ status = "stale"
756+ statusClass = "stale"
757+ }
758+ rows += fmt .Sprintf ("<tr><td>%s</td><td>%d</td><td>%s</td><td class='%s'>%s</td></tr>" ,
759+ k .Key ,
760+ k .Size ,
761+ k .ExpiredAt ,
762+ statusClass ,
763+ status )
764+ }
765+
766+ fmt .Fprintf (w , tmpl , len (keys ), rows )
767+ }
0 commit comments