diff --git a/internal/webapi/admin.go b/internal/webapi/admin.go index 6fe29a28..fbf25845 100644 --- a/internal/webapi/admin.go +++ b/internal/webapi/admin.go @@ -144,8 +144,10 @@ func (w *WebAPI) statusJSON(c *gin.Context) { // adminPage is the handler for "GET /admin". func (w *WebAPI) adminPage(c *gin.Context) { + cacheData := c.MustGet(cacheKey).(cacheData) + c.HTML(http.StatusOK, "admin.html", gin.H{ - "WebApiCache": w.cache.getData(), + "WebApiCache": cacheData, "WebApiCfg": w.cfg, "WalletStatus": w.walletStatus(c), "DcrdStatus": w.dcrdStatus(c), @@ -155,6 +157,8 @@ func (w *WebAPI) adminPage(c *gin.Context) { // ticketSearch is the handler for "POST /admin/ticket". The hash param will be // used to retrieve a ticket from the database. func (w *WebAPI) ticketSearch(c *gin.Context) { + cacheData := c.MustGet(cacheKey).(cacheData) + hash := c.PostForm("hash") ticket, found, err := w.db.GetTicketByHash(hash) @@ -218,7 +222,7 @@ func (w *WebAPI) ticketSearch(c *gin.Context) { VoteChanges: voteChanges, MaxVoteChanges: w.cfg.MaxVoteChangeRecords, }, - "WebApiCache": w.cache.getData(), + "WebApiCache": cacheData, "WebApiCfg": w.cfg, "WalletStatus": w.walletStatus(c), "DcrdStatus": w.dcrdStatus(c), @@ -228,12 +232,14 @@ func (w *WebAPI) ticketSearch(c *gin.Context) { // adminLogin is the handler for "POST /admin". If a valid password is provided, // the current session will be authenticated as an admin. func (w *WebAPI) adminLogin(c *gin.Context) { + cacheData := c.MustGet(cacheKey).(cacheData) + password := c.PostForm("password") if password != w.cfg.AdminPass { w.log.Warnf("Failed login attempt from %s", c.ClientIP()) c.HTML(http.StatusUnauthorized, "login.html", gin.H{ - "WebApiCache": w.cache.getData(), + "WebApiCache": cacheData, "WebApiCfg": w.cfg, "FailedLoginMsg": "Incorrect password", }) diff --git a/internal/webapi/cache.go b/internal/webapi/cache.go index 19dcd795..b36418ae 100644 --- a/internal/webapi/cache.go +++ b/internal/webapi/cache.go @@ -30,6 +30,10 @@ type cache struct { } type cacheData struct { + // Initialized is set true after all of the below values have been set for + // the first time. + Initialized bool + UpdateTime string PubKey string DatabaseSize string @@ -45,6 +49,13 @@ type cacheData struct { MissedProportion float32 } +func (c *cache) initialized() bool { + c.mtx.RLock() + defer c.mtx.RUnlock() + + return c.data.Initialized +} + func (c *cache) getData() cacheData { c.mtx.RLock() defer c.mtx.RUnlock() @@ -106,6 +117,7 @@ func (c *cache) update() error { c.mtx.Lock() defer c.mtx.Unlock() + c.data.Initialized = true c.data.UpdateTime = dateTime(time.Now().Unix()) c.data.DatabaseSize = humanize.Bytes(dbSize) c.data.Voting = voting diff --git a/internal/webapi/homepage.go b/internal/webapi/homepage.go index e8c26d0c..a7fdd3d2 100644 --- a/internal/webapi/homepage.go +++ b/internal/webapi/homepage.go @@ -11,8 +11,10 @@ import ( ) func (w *WebAPI) homepage(c *gin.Context) { + cacheData := c.MustGet(cacheKey).(cacheData) + c.HTML(http.StatusOK, "homepage.html", gin.H{ - "WebApiCache": w.cache.getData(), + "WebApiCache": cacheData, "WebApiCfg": w.cfg, }) } diff --git a/internal/webapi/middleware.go b/internal/webapi/middleware.go index 14b08bcc..a0b2e89d 100644 --- a/internal/webapi/middleware.go +++ b/internal/webapi/middleware.go @@ -93,15 +93,34 @@ func (w *WebAPI) withSession(store *sessions.CookieStore) gin.HandlerFunc { } } +// requireWebCache will only allow the request to proceed if the web API cache +// has been initialized with data, otherwise it will return a 500 Internal +// Server Error. +func (w *WebAPI) requireWebCache(c *gin.Context) { + if !w.cache.initialized() { + // Try to initialize it now. + err := w.cache.update() + if err != nil { + w.log.Errorf("Failed to initialize cache: %v", err) + c.String(http.StatusInternalServerError, "Cache is not initialized") + c.Abort() + return + } + } + + c.Set(cacheKey, w.cache.getData()) +} + // requireAdmin will only allow the request to proceed if the current session is // authenticated as an admin, otherwise it will render the login template. func (w *WebAPI) requireAdmin(c *gin.Context) { + cacheData := c.MustGet(cacheKey).(cacheData) session := c.MustGet(sessionKey).(*sessions.Session) admin := session.Values["admin"] if admin == nil { c.HTML(http.StatusUnauthorized, "login.html", gin.H{ - "WebApiCache": w.cache.getData(), + "WebApiCache": cacheData, "WebApiCfg": w.cfg, }) c.Abort() diff --git a/internal/webapi/vspinfo.go b/internal/webapi/vspinfo.go index 4f4568d9..6552a25d 100644 --- a/internal/webapi/vspinfo.go +++ b/internal/webapi/vspinfo.go @@ -14,7 +14,8 @@ import ( // vspInfo is the handler for "GET /api/v3/vspinfo". func (w *WebAPI) vspInfo(c *gin.Context) { - cachedStats := w.cache.getData() + cachedStats := c.MustGet(cacheKey).(cacheData) + w.sendJSONResponse(types.VspInfoResponse{ APIVersions: []int64{3}, Timestamp: time.Now().Unix(), diff --git a/internal/webapi/webapi.go b/internal/webapi/webapi.go index 94997594..a8f27d47 100644 --- a/internal/webapi/webapi.go +++ b/internal/webapi/webapi.go @@ -57,6 +57,7 @@ const ( dcrdKey = "DcrdClient" dcrdHostKey = "DcrdHostname" dcrdErrorKey = "DcrdClientErr" + cacheKey = "Cache" walletsKey = "WalletClients" failedWalletsKey = "FailedWalletClients" requestBytesKey = "RequestBytes" @@ -240,7 +241,7 @@ func (w *WebAPI) router(cookieSecret []byte, dcrd rpc.DcrdConnect, wallets rpc.W // API routes. api := router.Group("/api/v3") - api.GET("/vspinfo", w.vspInfo) + api.GET("/vspinfo", w.requireWebCache, w.vspInfo) api.POST("/setaltsignaddr", w.vspMustBeOpen, w.withDcrdClient(dcrd), w.broadcastTicket, w.vspAuth, w.setAltSignAddr) api.POST("/feeaddress", w.vspMustBeOpen, w.withDcrdClient(dcrd), w.broadcastTicket, w.vspAuth, w.feeAddress) api.POST("/ticketstatus", w.withDcrdClient(dcrd), w.vspAuth, w.ticketStatus) @@ -249,7 +250,7 @@ func (w *WebAPI) router(cookieSecret []byte, dcrd rpc.DcrdConnect, wallets rpc.W // Website routes. - router.GET("", w.homepage) + router.GET("", w.requireWebCache, w.homepage) login := router.Group("/admin").Use( w.withSession(cookieStore), @@ -257,18 +258,23 @@ func (w *WebAPI) router(cookieSecret []byte, dcrd rpc.DcrdConnect, wallets rpc.W // Limit login attempts to 3 per second. loginRateLmiter := rateLimit(3, func(c *gin.Context) { + cacheData := c.MustGet(cacheKey).(cacheData) + w.log.Warnf("Login rate limit exceeded by %s", c.ClientIP()) c.HTML(http.StatusTooManyRequests, "login.html", gin.H{ - "WebApiCache": w.cache.getData(), + "WebApiCache": cacheData, "WebApiCfg": w.cfg, "FailedLoginMsg": "Rate limit exceeded", }) }) - login.POST("", loginRateLmiter, w.adminLogin) + login.POST("", w.requireWebCache, loginRateLmiter, w.adminLogin) admin := router.Group("/admin").Use( - w.withWalletClients(wallets), w.withSession(cookieStore), w.requireAdmin, - ) + w.requireWebCache, + w.withWalletClients(wallets), + w.withSession(cookieStore), + w.requireAdmin) + admin.GET("", w.withDcrdClient(dcrd), w.adminPage) admin.POST("/ticket", w.withDcrdClient(dcrd), w.ticketSearch) admin.GET("/backup", w.downloadDatabaseBackup)