// Copyright (c) 2020-2021 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 ( "bytes" "errors" "io" "net/http" "strings" "github.com/decred/vspd/rpc" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/gorilla/sessions" "github.com/jrick/wsrpc/v2" ) // withSession middleware adds a gorilla session to the request context for // downstream handlers to make use of. Sessions are used by admin pages to // maintain authentication status. func withSession(store *sessions.CookieStore) gin.HandlerFunc { return func(c *gin.Context) { session, err := store.Get(c.Request, "vspd-session") if err != nil { // "value is not valid" occurs if the cookie secret changes. This is // common during development (eg. when using the test harness) but // it should not occur in production. if strings.Contains(err.Error(), "securecookie: the value is not valid") { log.Warn("Cookie secret has changed. Generating new session.") // Persist the newly generated session. err = store.Save(c.Request, c.Writer, session) if err != nil { log.Errorf("Error saving session: %v", err) c.String(http.StatusInternalServerError, "Error saving session") c.Abort() return } } else { log.Errorf("Session error: %v", err) c.String(http.StatusInternalServerError, "Error getting session") c.Abort() return } } c.Set(sessionKey, session) } } // 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 requireAdmin() gin.HandlerFunc { return func(c *gin.Context) { session := c.MustGet(sessionKey).(*sessions.Session) admin := session.Values["admin"] if admin == nil { c.HTML(http.StatusUnauthorized, "login.html", gin.H{ "WebApiCache": getCache(), "WebApiCfg": cfg, }) c.Abort() return } } } // withDcrdClient middleware adds a dcrd client to the request context for // downstream handlers to make use of. func withDcrdClient(dcrd rpc.DcrdConnect) gin.HandlerFunc { return func(c *gin.Context) { client, hostname, err := dcrd.Client(c, cfg.NetParams) // Don't handle the error here, add it to the context and let downstream // handlers decide what to do with it. c.Set(dcrdKey, client) c.Set(dcrdHostKey, hostname) c.Set(dcrdErrorKey, err) } } // withWalletClients middleware attempts to add voting wallet clients to the // request context for downstream handlers to make use of. Downstream handlers // must handle the case where no wallet clients are connected. func withWalletClients(wallets rpc.WalletConnect) gin.HandlerFunc { return func(c *gin.Context) { clients, failedConnections := wallets.Clients(c, cfg.NetParams) if len(clients) == 0 { log.Error("Could not connect to any wallets") } else if len(failedConnections) > 0 { log.Errorf("Failed to connect to %d wallet(s), proceeding with only %d", len(failedConnections), len(clients)) } c.Set(walletsKey, clients) c.Set(failedWalletsKey, failedConnections) } } // drainAndReplaceBody will read and return the body of the provided request. It // replaces the request reader with an identical one so it can be used again. func drainAndReplaceBody(req *http.Request) ([]byte, error) { reqBytes, err := io.ReadAll(req.Body) if err != nil { return nil, err } req.Body.Close() req.Body = io.NopCloser(bytes.NewBuffer(reqBytes)) return reqBytes, nil } // broadcastTicket will ensure that the local dcrd instance is aware of the // provided ticket. // Ticket hash, ticket hex, and parent hex are parsed from the request body and // validated. They are broadcast to the network using SendRawTransaction if dcrd // is not aware of them. func broadcastTicket() gin.HandlerFunc { return func(c *gin.Context) { const funcName = "broadcastTicket" // Read request bytes. reqBytes, err := drainAndReplaceBody(c.Request) if err != nil { log.Warnf("%s: Error reading request (clientIP=%s): %v", funcName, c.ClientIP(), err) sendErrorWithMsg(err.Error(), errBadRequest, c) return } // Parse request to ensure ticket hash/hex and parent hex are included. var request struct { TicketHex string `json:"tickethex" binding:"required"` TicketHash string `json:"tickethash" binding:"required"` ParentHex string `json:"parenthex" binding:"required"` } if err := binding.JSON.BindBody(reqBytes, &request); err != nil { log.Warnf("%s: Bad request (clientIP=%s): %v", funcName, c.ClientIP(), err) sendErrorWithMsg(err.Error(), errBadRequest, c) return } // Ensure the provided ticket hex is a valid ticket. msgTx, err := decodeTransaction(request.TicketHex) if err != nil { log.Errorf("%s: Failed to decode ticket hex (ticketHash=%s): %v", funcName, request.TicketHash, err) sendErrorWithMsg("cannot decode ticket hex", errBadRequest, c) return } err = isValidTicket(msgTx) if err != nil { log.Warnf("%s: Invalid ticket (clientIP=%s, ticketHash=%s): %v", funcName, c.ClientIP(), request.TicketHash, err) sendError(errInvalidTicket, c) return } // Ensure hex matches hash. if msgTx.TxHash().String() != request.TicketHash { log.Warnf("%s: Ticket hex/hash mismatch (clientIP=%s, ticketHash=%s)", funcName, c.ClientIP(), request.TicketHash) sendErrorWithMsg("ticket hex does not match hash", errBadRequest, c) return } // Ensure the provided parent hex is a valid tx. parentTx, err := decodeTransaction(request.ParentHex) if err != nil { log.Errorf("%s: Failed to decode parent hex (ticketHash=%s): %v", funcName, request.TicketHash, err) sendErrorWithMsg("cannot decode parent hex", errBadRequest, c) return } parentHash := parentTx.TxHash() // Check if local dcrd already knows the parent tx. dcrdClient := c.MustGet(dcrdKey).(*rpc.DcrdRPC) dcrdErr := c.MustGet(dcrdErrorKey) if dcrdErr != nil { log.Errorf("%s: could not get dcrd client: %v", funcName, dcrdErr.(error)) sendError(errInternalError, c) return } _, err = dcrdClient.GetRawTransaction(parentHash.String()) var e *wsrpc.Error if err == nil { // No error means dcrd already knows the parent tx, nothing to do. } else if errors.As(err, &e) && e.Code == rpc.ErrNoTxInfo { // ErrNoTxInfo means local dcrd is not aware of the parent. We have // the hex, so we can broadcast it here. // Before broadcasting, check that the provided parent hex is // actually the parent of the ticket. var found bool for _, txIn := range msgTx.TxIn { if !txIn.PreviousOutPoint.Hash.IsEqual(&parentHash) { continue } found = true break } if !found { log.Errorf("%s: Invalid ticket parent (ticketHash=%s)", funcName, request.TicketHash) sendErrorWithMsg("invalid ticket parent", errBadRequest, c) return } log.Debugf("%s: Broadcasting parent tx %s (ticketHash=%s)", funcName, parentHash, request.TicketHash) err = dcrdClient.SendRawTransaction(request.ParentHex) if err != nil { log.Errorf("%s: dcrd.SendRawTransaction for parent tx failed (ticketHash=%s): %v", funcName, request.TicketHash, err) sendError(errCannotBroadcastTicket, c) return } } else { log.Errorf("%s: dcrd.GetRawTransaction for ticket parent failed (ticketHash=%s): %v", funcName, request.TicketHash, err) sendError(errInternalError, c) return } // Check if local dcrd already knows the ticket. _, err = dcrdClient.GetRawTransaction(request.TicketHash) if err == nil { // No error means dcrd already knows the ticket, we are done here. return } // ErrNoTxInfo means local dcrd is not aware of the ticket. We have the // hex, so we can broadcast it here. if errors.As(err, &e) && e.Code == rpc.ErrNoTxInfo { log.Debugf("%s: Broadcasting ticket (ticketHash=%s)", funcName, request.TicketHash) err = dcrdClient.SendRawTransaction(request.TicketHex) if err != nil { log.Errorf("%s: dcrd.SendRawTransaction for ticket failed (ticketHash=%s): %v", funcName, request.TicketHash, err) sendError(errCannotBroadcastTicket, c) return } } else { log.Errorf("%s: dcrd.GetRawTransaction for ticket failed (ticketHash=%s): %v", funcName, request.TicketHash, err) sendError(errInternalError, c) return } } } // vspAuth middleware reads the request body and extracts the ticket hash. The // commitment address for the ticket is retrieved from the database if it is // known, or it is retrieved from the chain if not. // The middleware errors out if the VSP-Client-Signature header of the request // does not contain the request body signed with the commitment address. // Ticket information is added to the request context for downstream handlers to // use. func vspAuth() gin.HandlerFunc { return func(c *gin.Context) { const funcName = "vspAuth" // Read request bytes. reqBytes, err := drainAndReplaceBody(c.Request) if err != nil { log.Warnf("%s: Error reading request (clientIP=%s): %v", funcName, c.ClientIP(), err) sendErrorWithMsg(err.Error(), errBadRequest, c) return } // Add request bytes to request context for downstream handlers to reuse. // Necessary because the request body reader can only be used once. c.Set(requestBytesKey, reqBytes) // Parse request and ensure there is a ticket hash included. var request struct { TicketHash string `json:"tickethash" binding:"required"` } if err := binding.JSON.BindBody(reqBytes, &request); err != nil { log.Warnf("%s: Bad request (clientIP=%s): %v", funcName, c.ClientIP(), err) sendErrorWithMsg(err.Error(), errBadRequest, c) return } hash := request.TicketHash // Before hitting the db or any RPC, ensure this is a valid ticket hash. err = validateTicketHash(hash) if err != nil { log.Errorf("%s: Bad request (clientIP=%s): %v", funcName, c.ClientIP(), err) sendErrorWithMsg("invalid ticket hash", errBadRequest, c) return } // Check if this ticket already appears in the database. ticket, ticketFound, err := db.GetTicketByHash(hash) if err != nil { log.Errorf("%s: db.GetTicketByHash error (ticketHash=%s): %v", funcName, hash, err) sendError(errInternalError, c) return } // If the ticket was found in the database, we already know its // commitment address. Otherwise we need to get it from the chain. var commitmentAddress string dcrdClient := c.MustGet(dcrdKey).(*rpc.DcrdRPC) dcrdErr := c.MustGet(dcrdErrorKey) if dcrdErr != nil { log.Errorf("%s: could not get dcrd client: %v", funcName, dcrdErr.(error)) sendError(errInternalError, c) return } if ticketFound { commitmentAddress = ticket.CommitmentAddress } else { commitmentAddress, err = getCommitmentAddress(hash, dcrdClient) if err != nil { var apiErr *apiError if errors.Is(err, apiErr) { sendError(errInvalidTicket, c) } else { sendError(errInternalError, c) } log.Errorf("%s: (clientIP: %s, ticketHash: %s): %v", funcName, c.ClientIP(), hash, err) return } } // Ensure a signature is provided. signature := c.GetHeader("VSP-Client-Signature") if signature == "" { log.Warnf("%s: Bad request (clientIP=%s): %v", funcName, c.ClientIP(), err) sendErrorWithMsg("no VSP-Client-Signature header", errBadRequest, c) return } // Validate request signature to ensure ticket ownership. err = validateSignature(hash, commitmentAddress, signature, string(reqBytes)) if err != nil { log.Errorf("%s: Bad signature (clientIP=%s, ticketHash=%s): %v", funcName, err) sendError(errBadSignature, c) return } // Add ticket information to context so downstream handlers don't need // to access the db for it. c.Set(ticketKey, ticket) c.Set(knownTicketKey, ticketFound) c.Set(commitmentAddressKey, commitmentAddress) } }