webapi: Abort requests if web cache not ready.
A new middleware aborts any requests which require the data cache if the cache is not yet initialized. An explicit error is returned so the admin will be immediately aware what has gone wrong. Previously, webpages were rendered and JSON responses were sent with zero values, and with no indication of anything being wrong. This was sometimes difficult to notice, and even when noticed the cause was not immediately apparent.
This commit is contained in:
parent
d1eddafb52
commit
99dc97d6a3
@ -144,8 +144,10 @@ func (w *WebAPI) statusJSON(c *gin.Context) {
|
|||||||
|
|
||||||
// adminPage is the handler for "GET /admin".
|
// adminPage is the handler for "GET /admin".
|
||||||
func (w *WebAPI) adminPage(c *gin.Context) {
|
func (w *WebAPI) adminPage(c *gin.Context) {
|
||||||
|
cacheData := c.MustGet(cacheKey).(cacheData)
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "admin.html", gin.H{
|
c.HTML(http.StatusOK, "admin.html", gin.H{
|
||||||
"WebApiCache": w.cache.getData(),
|
"WebApiCache": cacheData,
|
||||||
"WebApiCfg": w.cfg,
|
"WebApiCfg": w.cfg,
|
||||||
"WalletStatus": w.walletStatus(c),
|
"WalletStatus": w.walletStatus(c),
|
||||||
"DcrdStatus": w.dcrdStatus(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
|
// ticketSearch is the handler for "POST /admin/ticket". The hash param will be
|
||||||
// used to retrieve a ticket from the database.
|
// used to retrieve a ticket from the database.
|
||||||
func (w *WebAPI) ticketSearch(c *gin.Context) {
|
func (w *WebAPI) ticketSearch(c *gin.Context) {
|
||||||
|
cacheData := c.MustGet(cacheKey).(cacheData)
|
||||||
|
|
||||||
hash := c.PostForm("hash")
|
hash := c.PostForm("hash")
|
||||||
|
|
||||||
ticket, found, err := w.db.GetTicketByHash(hash)
|
ticket, found, err := w.db.GetTicketByHash(hash)
|
||||||
@ -218,7 +222,7 @@ func (w *WebAPI) ticketSearch(c *gin.Context) {
|
|||||||
VoteChanges: voteChanges,
|
VoteChanges: voteChanges,
|
||||||
MaxVoteChanges: w.cfg.MaxVoteChangeRecords,
|
MaxVoteChanges: w.cfg.MaxVoteChangeRecords,
|
||||||
},
|
},
|
||||||
"WebApiCache": w.cache.getData(),
|
"WebApiCache": cacheData,
|
||||||
"WebApiCfg": w.cfg,
|
"WebApiCfg": w.cfg,
|
||||||
"WalletStatus": w.walletStatus(c),
|
"WalletStatus": w.walletStatus(c),
|
||||||
"DcrdStatus": w.dcrdStatus(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,
|
// adminLogin is the handler for "POST /admin". If a valid password is provided,
|
||||||
// the current session will be authenticated as an admin.
|
// the current session will be authenticated as an admin.
|
||||||
func (w *WebAPI) adminLogin(c *gin.Context) {
|
func (w *WebAPI) adminLogin(c *gin.Context) {
|
||||||
|
cacheData := c.MustGet(cacheKey).(cacheData)
|
||||||
|
|
||||||
password := c.PostForm("password")
|
password := c.PostForm("password")
|
||||||
|
|
||||||
if password != w.cfg.AdminPass {
|
if password != w.cfg.AdminPass {
|
||||||
w.log.Warnf("Failed login attempt from %s", c.ClientIP())
|
w.log.Warnf("Failed login attempt from %s", c.ClientIP())
|
||||||
c.HTML(http.StatusUnauthorized, "login.html", gin.H{
|
c.HTML(http.StatusUnauthorized, "login.html", gin.H{
|
||||||
"WebApiCache": w.cache.getData(),
|
"WebApiCache": cacheData,
|
||||||
"WebApiCfg": w.cfg,
|
"WebApiCfg": w.cfg,
|
||||||
"FailedLoginMsg": "Incorrect password",
|
"FailedLoginMsg": "Incorrect password",
|
||||||
})
|
})
|
||||||
|
|||||||
@ -30,6 +30,10 @@ type cache struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type cacheData 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
|
UpdateTime string
|
||||||
PubKey string
|
PubKey string
|
||||||
DatabaseSize string
|
DatabaseSize string
|
||||||
@ -45,6 +49,13 @@ type cacheData struct {
|
|||||||
MissedProportion float32
|
MissedProportion float32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *cache) initialized() bool {
|
||||||
|
c.mtx.RLock()
|
||||||
|
defer c.mtx.RUnlock()
|
||||||
|
|
||||||
|
return c.data.Initialized
|
||||||
|
}
|
||||||
|
|
||||||
func (c *cache) getData() cacheData {
|
func (c *cache) getData() cacheData {
|
||||||
c.mtx.RLock()
|
c.mtx.RLock()
|
||||||
defer c.mtx.RUnlock()
|
defer c.mtx.RUnlock()
|
||||||
@ -106,6 +117,7 @@ func (c *cache) update() error {
|
|||||||
c.mtx.Lock()
|
c.mtx.Lock()
|
||||||
defer c.mtx.Unlock()
|
defer c.mtx.Unlock()
|
||||||
|
|
||||||
|
c.data.Initialized = true
|
||||||
c.data.UpdateTime = dateTime(time.Now().Unix())
|
c.data.UpdateTime = dateTime(time.Now().Unix())
|
||||||
c.data.DatabaseSize = humanize.Bytes(dbSize)
|
c.data.DatabaseSize = humanize.Bytes(dbSize)
|
||||||
c.data.Voting = voting
|
c.data.Voting = voting
|
||||||
|
|||||||
@ -11,8 +11,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (w *WebAPI) homepage(c *gin.Context) {
|
func (w *WebAPI) homepage(c *gin.Context) {
|
||||||
|
cacheData := c.MustGet(cacheKey).(cacheData)
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "homepage.html", gin.H{
|
c.HTML(http.StatusOK, "homepage.html", gin.H{
|
||||||
"WebApiCache": w.cache.getData(),
|
"WebApiCache": cacheData,
|
||||||
"WebApiCfg": w.cfg,
|
"WebApiCfg": w.cfg,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -93,15 +93,31 @@ 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() {
|
||||||
|
const str = "Cache is not yet initialized"
|
||||||
|
w.log.Errorf(str)
|
||||||
|
c.String(http.StatusInternalServerError, str)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Set(cacheKey, w.cache.getData())
|
||||||
|
}
|
||||||
|
|
||||||
// requireAdmin will only allow the request to proceed if the current session is
|
// requireAdmin will only allow the request to proceed if the current session is
|
||||||
// authenticated as an admin, otherwise it will render the login template.
|
// authenticated as an admin, otherwise it will render the login template.
|
||||||
func (w *WebAPI) requireAdmin(c *gin.Context) {
|
func (w *WebAPI) requireAdmin(c *gin.Context) {
|
||||||
|
cacheData := c.MustGet(cacheKey).(cacheData)
|
||||||
session := c.MustGet(sessionKey).(*sessions.Session)
|
session := c.MustGet(sessionKey).(*sessions.Session)
|
||||||
admin := session.Values["admin"]
|
admin := session.Values["admin"]
|
||||||
|
|
||||||
if admin == nil {
|
if admin == nil {
|
||||||
c.HTML(http.StatusUnauthorized, "login.html", gin.H{
|
c.HTML(http.StatusUnauthorized, "login.html", gin.H{
|
||||||
"WebApiCache": w.cache.getData(),
|
"WebApiCache": cacheData,
|
||||||
"WebApiCfg": w.cfg,
|
"WebApiCfg": w.cfg,
|
||||||
})
|
})
|
||||||
c.Abort()
|
c.Abort()
|
||||||
|
|||||||
@ -14,7 +14,8 @@ import (
|
|||||||
|
|
||||||
// vspInfo is the handler for "GET /api/v3/vspinfo".
|
// vspInfo is the handler for "GET /api/v3/vspinfo".
|
||||||
func (w *WebAPI) vspInfo(c *gin.Context) {
|
func (w *WebAPI) vspInfo(c *gin.Context) {
|
||||||
cachedStats := w.cache.getData()
|
cachedStats := c.MustGet(cacheKey).(cacheData)
|
||||||
|
|
||||||
w.sendJSONResponse(types.VspInfoResponse{
|
w.sendJSONResponse(types.VspInfoResponse{
|
||||||
APIVersions: []int64{3},
|
APIVersions: []int64{3},
|
||||||
Timestamp: time.Now().Unix(),
|
Timestamp: time.Now().Unix(),
|
||||||
|
|||||||
@ -57,6 +57,7 @@ const (
|
|||||||
dcrdKey = "DcrdClient"
|
dcrdKey = "DcrdClient"
|
||||||
dcrdHostKey = "DcrdHostname"
|
dcrdHostKey = "DcrdHostname"
|
||||||
dcrdErrorKey = "DcrdClientErr"
|
dcrdErrorKey = "DcrdClientErr"
|
||||||
|
cacheKey = "Cache"
|
||||||
walletsKey = "WalletClients"
|
walletsKey = "WalletClients"
|
||||||
failedWalletsKey = "FailedWalletClients"
|
failedWalletsKey = "FailedWalletClients"
|
||||||
requestBytesKey = "RequestBytes"
|
requestBytesKey = "RequestBytes"
|
||||||
@ -240,7 +241,7 @@ func (w *WebAPI) router(cookieSecret []byte, dcrd rpc.DcrdConnect, wallets rpc.W
|
|||||||
// API routes.
|
// API routes.
|
||||||
|
|
||||||
api := router.Group("/api/v3")
|
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("/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("/feeaddress", w.vspMustBeOpen, w.withDcrdClient(dcrd), w.broadcastTicket, w.vspAuth, w.feeAddress)
|
||||||
api.POST("/ticketstatus", w.withDcrdClient(dcrd), w.vspAuth, w.ticketStatus)
|
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.
|
// Website routes.
|
||||||
|
|
||||||
router.GET("", w.homepage)
|
router.GET("", w.requireWebCache, w.homepage)
|
||||||
|
|
||||||
login := router.Group("/admin").Use(
|
login := router.Group("/admin").Use(
|
||||||
w.withSession(cookieStore),
|
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.
|
// Limit login attempts to 3 per second.
|
||||||
loginRateLmiter := rateLimit(3, func(c *gin.Context) {
|
loginRateLmiter := rateLimit(3, func(c *gin.Context) {
|
||||||
|
cacheData := c.MustGet(cacheKey).(cacheData)
|
||||||
|
|
||||||
w.log.Warnf("Login rate limit exceeded by %s", c.ClientIP())
|
w.log.Warnf("Login rate limit exceeded by %s", c.ClientIP())
|
||||||
c.HTML(http.StatusTooManyRequests, "login.html", gin.H{
|
c.HTML(http.StatusTooManyRequests, "login.html", gin.H{
|
||||||
"WebApiCache": w.cache.getData(),
|
"WebApiCache": cacheData,
|
||||||
"WebApiCfg": w.cfg,
|
"WebApiCfg": w.cfg,
|
||||||
"FailedLoginMsg": "Rate limit exceeded",
|
"FailedLoginMsg": "Rate limit exceeded",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
login.POST("", loginRateLmiter, w.adminLogin)
|
login.POST("", w.requireWebCache, loginRateLmiter, w.adminLogin)
|
||||||
|
|
||||||
admin := router.Group("/admin").Use(
|
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.GET("", w.withDcrdClient(dcrd), w.adminPage)
|
||||||
admin.POST("/ticket", w.withDcrdClient(dcrd), w.ticketSearch)
|
admin.POST("/ticket", w.withDcrdClient(dcrd), w.ticketSearch)
|
||||||
admin.GET("/backup", w.downloadDatabaseBackup)
|
admin.GET("/backup", w.downloadDatabaseBackup)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user