diff --git a/cmd/dcrdata/internal/explorer/explorer.go b/cmd/dcrdata/internal/explorer/explorer.go index 0a6c5c9c1..f52ad11ef 100644 --- a/cmd/dcrdata/internal/explorer/explorer.go +++ b/cmd/dcrdata/internal/explorer/explorer.go @@ -8,8 +8,10 @@ package explorer import ( "context" + "crypto/rand" "fmt" "math" + "math/big" "net/http" "os" "os/signal" @@ -201,6 +203,8 @@ type pageData struct { BlockInfo *types.BlockInfo BlockchainInfo *chainjson.GetBlockChainInfoResult HomeInfo *types.HomeInfo + eTag string + lastModified time.Time } type explorerUI struct { @@ -359,6 +363,7 @@ func New(cfg *ExplorerConfig) *explorerUI { }, }, } + exp.resetETagAndLastModified() log.Infof("Mean Voting Blocks calculated: %d", exp.pageData.HomeInfo.Params.MeanVotingBlocks) @@ -447,6 +452,9 @@ func (exp *explorerUI) StoreMPData(_ *mempool.StakeData, _ []types.MempoolTx, in exp.invsMtx.Lock() exp.invs = inv exp.invsMtx.Unlock() + + exp.resetETagAndLastModified() + log.Debugf("Updated mempool details for the explorerUI.") } @@ -616,6 +624,8 @@ func (exp *explorerUI) Store(blockData *blockdata.BlockData, msgBlock *wire.MsgB }() } + exp.resetETagAndLastModified() + return nil } @@ -629,6 +639,21 @@ func (exp *explorerUI) ChartsUpdated() { exp.pageData.Unlock() } +// resetETagAndLastModified resets the eTag and last modified time to new +// values and is protected by the exp.pageData mutex. +func (exp *explorerUI) resetETagAndLastModified() { + exp.pageData.Lock() + exp.pageData.eTag = generateRandomString() + exp.pageData.lastModified = time.Now() + exp.pageData.Unlock() +} + +func (exp *explorerUI) eTagAndLastModified() (eTag string, lastModified time.Time) { + exp.pageData.RLock() + defer exp.pageData.RUnlock() + return exp.pageData.eTag, exp.pageData.lastModified +} + func (exp *explorerUI) updateDevFundBalance() { // yield processor to other goroutines runtime.Gosched() @@ -879,3 +904,16 @@ func indexPrice(index exchanges.CurrencyPair, indices map[string]map[exchanges.C } return price / nSources } + +const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" +const nLetters = len(letters) + +// generateRandomString creates a random alphanumeric string of length 16. +func generateRandomString() string { + bytes := make([]byte, 16) + for i := range bytes { + num, _ := rand.Int(rand.Reader, big.NewInt(int64(nLetters))) + bytes[i] = letters[num.Int64()] + } + return string(bytes) +} diff --git a/cmd/dcrdata/internal/explorer/explorermiddleware.go b/cmd/dcrdata/internal/explorer/explorermiddleware.go index 1ad9e4ad1..c663ce13c 100644 --- a/cmd/dcrdata/internal/explorer/explorermiddleware.go +++ b/cmd/dcrdata/internal/explorer/explorermiddleware.go @@ -189,6 +189,33 @@ func (exp *explorerUI) SyncStatusFileIntercept(next http.Handler) http.Handler { }) } +// ETagAndLastModifiedIntercept handles ETag and Last-Modified headers for caching purposes. +func (exp *explorerUI) ETagAndLastModifiedIntercept(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + eTag, lastModified := exp.eTagAndLastModified() + if match := r.Header.Get("If-None-Match"); match != "" { + if match == eTag { + w.WriteHeader(http.StatusNotModified) + return + } + } else if modifiedSince := r.Header.Get("If-Modified-Since"); modifiedSince != "" { + if t, err := time.Parse(http.TimeFormat, modifiedSince); err == nil { + if lastModified.Before(t.Add(1 * time.Second)) { + w.WriteHeader(http.StatusNotModified) + return + } + } + } + + // Set ETag and Last-Modified headers. + w.Header().Set("ETag", eTag) + w.Header().Set("Last-Modified", lastModified.Format(http.TimeFormat)) + w.Header().Set("Cache-Control", "private") + + next.ServeHTTP(w, r) + }) +} + func getBlockHashCtx(r *http.Request) string { hash, ok := r.Context().Value(ctxBlockHash).(string) if !ok { diff --git a/cmd/dcrdata/main.go b/cmd/dcrdata/main.go index 8bcf4e469..b7718b57b 100644 --- a/cmd/dcrdata/main.go +++ b/cmd/dcrdata/main.go @@ -761,23 +761,15 @@ func _main(ctx context.Context) error { r.Get("/rejects", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/disapproved", http.StatusPermanentRedirect) }) - r.Get("/disapproved", explore.DisapprovedBlocks) - r.Get("/mempool", explore.Mempool) - r.Get("/parameters", explore.ParametersPage) r.With(explore.BlockHashPathOrIndexCtx).Get("/block/{blockhash}", explore.Block) r.With(explorer.TransactionHashCtx).Get("/tx/{txid}", explore.TxPage) r.With(explorer.TransactionHashCtx, explorer.TransactionIoIndexCtx).Get("/tx/{txid}/{inout}/{inoutid}", explore.TxPage) r.With(explorer.AddressPathCtx).Get("/address/{address}", explore.AddressPage) r.With(explorer.AddressPathCtx).Get("/addresstable/{address}", explore.AddressTable) - r.Get("/treasury", explore.TreasuryPage) - r.Get("/treasurytable", explore.TreasuryTable) - r.Get("/agendas", explore.AgendasPage) - r.With(explorer.AgendaPathCtx).Get("/agenda/{agendaid}", explore.AgendaPage) r.Get("/proposals", explore.ProposalsPage) r.With(explorer.ProposalPathCtx).Get("/proposal/{proposaltoken}", explore.ProposalPage) r.Get("/decodetx", explore.DecodeTxPage) r.Get("/search", explore.Search) - r.Get("/charts", explore.Charts) r.Get("/ticketpool", explore.Ticketpool) r.Get("/market", explore.MarketPage) r.Get("/stats", func(w http.ResponseWriter, r *http.Request) { @@ -786,9 +778,23 @@ func _main(ctx context.Context) error { // MenuFormParser will typically redirect, but going to the homepage as a // fallback. r.With(explorer.MenuFormParser).Post("/set", explore.Home) - r.Get("/attack-cost", explore.AttackCost) r.Get("/verify-message", explore.VerifyMessagePage) r.With(mw.Tollbooth(limiter)).Post("/verify-message", explore.VerifyMessageHandler) + + // Pages that can be cached because they depend on block and/or mempool data cached by + // *explorer.explorerUI. This middleware sets ETag and Last-Modified headers that are + // reset if a new block or mempool change is detected. + withCache := r.With(explore.ETagAndLastModifiedIntercept) + withCache.Get("/", explore.Home) + withCache.Get("/disapproved", explore.DisapprovedBlocks) + withCache.Get("/mempool", explore.Mempool) + withCache.Get("/charts", explore.Charts) + withCache.Get("/treasury", explore.TreasuryPage) + withCache.Get("/treasurytable", explore.TreasuryTable) + withCache.Get("/parameters", explore.ParametersPage) + withCache.Get("/agendas", explore.AgendasPage) + withCache.With(explorer.AgendaPathCtx).Get("/agenda/{agendaid}", explore.AgendaPage) + withCache.Get("/attack-cost", explore.AttackCost) }) // Configure a page for the bare "/insight" path. This mounts the static