vspd/internal/webapi/admin.go
jholdstock a7bb0cd9d7 webapi: Rename server to WebAPI.
Exporting this struct is a step towards breaking up the Start func into
a New func and Run func, enabling calling code to use:

    api := webapi.New()
    api.Run()

WebAPI is a more suitable name than server because it matches the
package name, and also it helps to distinguish WebAPI from the HTTP
server it uses internally (ie. webapi.server rather than server.server).
2023-09-16 07:54:24 +01:00

279 lines
8.1 KiB
Go

// Copyright (c) 2020-2023 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package webapi
import (
"encoding/json"
"net/http"
"github.com/decred/vspd/database"
"github.com/decred/vspd/rpc"
"github.com/gin-gonic/gin"
"github.com/gorilla/sessions"
)
// walletStatus describes the current status of a single voting wallet. This is
// used by the admin.html template, and also serialized to JSON for the
// /admin/status endpoint.
type walletStatus struct {
Connected bool `json:"connected"`
InfoError bool `json:"infoerror"`
DaemonConnected bool `json:"daemonconnected"`
VoteVersion uint32 `json:"voteversion"`
Unlocked bool `json:"unlocked"`
Voting bool `json:"voting"`
BestBlockError bool `json:"bestblockerror"`
BestBlockHeight int64 `json:"bestblockheight"`
}
// dcrdStatus describes the current status of the local instance of dcrd used by
// vspd. This is used by the admin.html template, and also serialized to JSON
// for the /admin/status endpoint.
type dcrdStatus struct {
Host string `json:"host"`
Connected bool `json:"connected"`
BestBlockError bool `json:"bestblockerror"`
BestBlockHeight uint32 `json:"bestblockheight"`
}
type searchResult struct {
Hash string
Found bool
Ticket database.Ticket
FeeTxDecoded string
AltSignAddrData *database.AltSignAddrData
VoteChanges map[uint32]database.VoteChangeRecord
MaxVoteChanges int
}
func (w *WebAPI) dcrdStatus(c *gin.Context) dcrdStatus {
hostname := c.MustGet(dcrdHostKey).(string)
status := dcrdStatus{Host: hostname}
dcrdClient := c.MustGet(dcrdKey).(*rpc.DcrdRPC)
dcrdErr := c.MustGet(dcrdErrorKey)
if dcrdErr != nil {
w.log.Errorf("Could not get dcrd client: %v", dcrdErr.(error))
return status
}
status.Connected = true
bestBlock, err := dcrdClient.GetBlockCount()
if err != nil {
w.log.Errorf("Could not get dcrd block count: %v", err)
status.BestBlockError = true
return status
}
status.BestBlockHeight = uint32(bestBlock)
return status
}
func (w *WebAPI) walletStatus(c *gin.Context) map[string]walletStatus {
walletClients := c.MustGet(walletsKey).([]*rpc.WalletRPC)
failedWalletClients := c.MustGet(failedWalletsKey).([]string)
status := make(map[string]walletStatus)
for _, v := range walletClients {
ws := walletStatus{Connected: true}
walletInfo, err := v.WalletInfo()
if err != nil {
w.log.Errorf("dcrwallet.WalletInfo error (wallet=%s): %v", v.String(), err)
ws.InfoError = true
} else {
ws.DaemonConnected = walletInfo.DaemonConnected
ws.VoteVersion = walletInfo.VoteVersion
ws.Unlocked = walletInfo.Unlocked
ws.Voting = walletInfo.Voting
}
height, err := v.GetBestBlockHeight()
if err != nil {
w.log.Errorf("dcrwallet.GetBestBlockHeight error (wallet=%s): %v", v.String(), err)
ws.BestBlockError = true
} else {
ws.BestBlockHeight = height
}
status[v.String()] = ws
}
for _, v := range failedWalletClients {
ws := walletStatus{Connected: false}
status[v] = ws
}
return status
}
// statusJSON is the handler for "GET /admin/status". It returns a JSON object
// describing the current status of voting wallets.
func (w *WebAPI) statusJSON(c *gin.Context) {
httpStatus := http.StatusOK
wallets := w.walletStatus(c)
// Respond with HTTP status 500 if any voting wallets have issues.
for _, wallet := range wallets {
if wallet.InfoError ||
wallet.BestBlockError ||
!wallet.Connected ||
!wallet.DaemonConnected ||
!wallet.Voting ||
!wallet.Unlocked {
httpStatus = http.StatusInternalServerError
break
}
}
dcrd := w.dcrdStatus(c)
// Respond with HTTP status 500 if dcrd has issues.
if !dcrd.Connected || dcrd.BestBlockError {
httpStatus = http.StatusInternalServerError
}
c.AbortWithStatusJSON(httpStatus, gin.H{
"wallets": wallets,
"dcrd": dcrd,
})
}
// adminPage is the handler for "GET /admin".
func (w *WebAPI) adminPage(c *gin.Context) {
c.HTML(http.StatusOK, "admin.html", gin.H{
"WebApiCache": w.cache.getData(),
"WebApiCfg": w.cfg,
"WalletStatus": w.walletStatus(c),
"DcrdStatus": w.dcrdStatus(c),
})
}
// 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) {
hash := c.PostForm("hash")
ticket, found, err := w.db.GetTicketByHash(hash)
if err != nil {
w.log.Errorf("db.GetTicketByHash error (ticketHash=%s): %v", hash, err)
c.String(http.StatusInternalServerError, "Error getting ticket from db")
return
}
voteChanges, err := w.db.GetVoteChanges(hash)
if err != nil {
w.log.Errorf("db.GetVoteChanges error (ticketHash=%s): %v", hash, err)
c.String(http.StatusInternalServerError, "Error getting vote changes from db")
return
}
altSignAddrData, err := w.db.AltSignAddrData(hash)
if err != nil {
w.log.Errorf("db.AltSignAddrData error (ticketHash=%s): %v", hash, err)
c.String(http.StatusInternalServerError, "Error getting alt sig from db")
return
}
// Decode the fee tx so it can be displayed human-readable. Fee tx hex may
// be null because it is removed from the DB if the tx is already mined and
// confirmed.
var feeTxDecoded string
if ticket.FeeTxHex != "" {
dcrdClient := c.MustGet(dcrdKey).(*rpc.DcrdRPC)
dcrdErr := c.MustGet(dcrdErrorKey)
if dcrdErr != nil {
w.log.Errorf("Could not get dcrd client: %v", dcrdErr.(error))
c.String(http.StatusInternalServerError, "Could not get dcrd client")
return
}
resp, err := dcrdClient.DecodeRawTransaction(ticket.FeeTxHex)
if err != nil {
w.log.Errorf("dcrd.DecodeRawTransaction error: %w", err)
c.String(http.StatusInternalServerError, "Error decoding fee transaction")
return
}
decoded, err := json.Marshal(resp)
if err != nil {
w.log.Errorf("Unmarshal fee tx error: %w", err)
c.String(http.StatusInternalServerError, "Error unmarshalling fee tx")
return
}
feeTxDecoded = string(decoded)
}
c.HTML(http.StatusOK, "admin.html", gin.H{
"SearchResult": searchResult{
Hash: hash,
Found: found,
Ticket: ticket,
FeeTxDecoded: feeTxDecoded,
AltSignAddrData: altSignAddrData,
VoteChanges: voteChanges,
MaxVoteChanges: w.cfg.MaxVoteChangeRecords,
},
"WebApiCache": w.cache.getData(),
"WebApiCfg": w.cfg,
"WalletStatus": w.walletStatus(c),
"DcrdStatus": w.dcrdStatus(c),
})
}
// 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) {
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(),
"WebApiCfg": w.cfg,
"FailedLoginMsg": "Incorrect password",
})
return
}
w.setAdminStatus(true, c)
}
// adminLogout is the handler for "POST /admin/logout". The current session will
// have its admin authentication removed.
func (w *WebAPI) adminLogout(c *gin.Context) {
w.setAdminStatus(nil, c)
}
// downloadDatabaseBackup is the handler for "GET /backup". A binary
// representation of the whole database is generated and returned to the client.
func (w *WebAPI) downloadDatabaseBackup(c *gin.Context) {
err := w.db.BackupDB(c.Writer)
if err != nil {
w.log.Errorf("Error backing up database: %v", err)
// Don't write any http body here because Content-Length has already
// been set in db.BackupDB. Status is enough to indicate an error.
c.Status(http.StatusInternalServerError)
}
}
// setAdminStatus stores the authentication status of the current session and
// redirects the client to GET /admin.
func (w *WebAPI) setAdminStatus(admin any, c *gin.Context) {
session := c.MustGet(sessionKey).(*sessions.Session)
session.Values["admin"] = admin
err := session.Save(c.Request, c.Writer)
if err != nil {
w.log.Errorf("Error saving session: %v", err)
c.String(http.StatusInternalServerError, "Error saving session")
return
}
c.Redirect(http.StatusFound, "/admin")
c.Abort()
}