diff --git a/docs/api.md b/docs/api.md index bfaa2cb..424954c 100644 --- a/docs/api.md +++ b/docs/api.md @@ -4,8 +4,16 @@ - Success responses use HTTP status 200 and a JSON encoded body. -- Error responses use either HTTP status 500 or 400, and a JSON encoded error - in the body, e.g. `{"error":"Description"}`. +- Error responses use HTTP status 500 to indicate a server error or 400 to + indiciate a client error, and will include a JSON body describing the error. + For example: + + ```json + {"code": 9, "message":"invalid vote choices"} + ``` + + A full list of error codes can be looked up in + [webapi/errors.go](../webapi/errors.go) - Requests which reference specific tickets need to be properly signed as described in [two-way-accountability.md](./two-way-accountability.md). diff --git a/webapi/errors.go b/webapi/errors.go new file mode 100644 index 0000000..4ab3d20 --- /dev/null +++ b/webapi/errors.go @@ -0,0 +1,84 @@ +package webapi + +import "net/http" + +type apiError int + +const ( + errBadRequest apiError = iota + errInternalError + errVspClosed + errFeeAlreadyReceived + errInvalidFeeTx + errFeeTooSmall + errUnknownTicket + errTicketCannotVote + errFeeExpired + errInvalidVoteChoices + errBadSignature + errInvalidPrivKey +) + +// httpStatus maps application error codes to HTTP status codes. +func (e apiError) httpStatus() int { + switch e { + case errBadRequest: + return http.StatusBadRequest + case errInternalError: + return http.StatusInternalServerError + case errVspClosed: + return http.StatusBadRequest + case errFeeAlreadyReceived: + return http.StatusBadRequest + case errInvalidFeeTx: + return http.StatusBadRequest + case errFeeTooSmall: + return http.StatusBadRequest + case errUnknownTicket: + return http.StatusBadRequest + case errTicketCannotVote: + return http.StatusBadRequest + case errFeeExpired: + return http.StatusBadRequest + case errInvalidVoteChoices: + return http.StatusBadRequest + case errBadSignature: + return http.StatusBadRequest + case errInvalidPrivKey: + return http.StatusBadRequest + default: + return http.StatusInternalServerError + } +} + +// defaultMessage returns a descriptive error string for a given error code. +func (e apiError) defaultMessage() string { + switch e { + case errBadRequest: + return "bad request" + case errInternalError: + return "internal error" + case errVspClosed: + return "vsp is closed" + case errFeeAlreadyReceived: + return "fee tx already received" + case errInvalidFeeTx: + return "invalid fee transaction" + case errFeeTooSmall: + return "fee too small" + case errUnknownTicket: + return "unknown ticket" + case errTicketCannotVote: + return "ticket not eligible to vote" + case errFeeExpired: + return "fee has expired" + case errInvalidVoteChoices: + return "invalid vote choices" + case errBadSignature: + return "bad request signature" + case errInvalidPrivKey: + return "invalid private key" + default: + return "unknown error" + } +} diff --git a/webapi/getfeeaddress.go b/webapi/getfeeaddress.go index eb28924..ed2ed35 100644 --- a/webapi/getfeeaddress.go +++ b/webapi/getfeeaddress.go @@ -1,7 +1,6 @@ package webapi import ( - "net/http" "sync" "time" @@ -70,14 +69,14 @@ func feeAddress(c *gin.Context) { dcrdClient := c.MustGet("DcrdClient").(*rpc.DcrdRPC) if cfg.VspClosed { - sendErrorResponse("pool is not accepting new tickets", http.StatusBadRequest, c) + sendError(errVspClosed, c) return } var feeAddressRequest FeeAddressRequest if err := binding.JSON.BindBody(rawRequest, &feeAddressRequest); err != nil { log.Warnf("Bad feeaddress request from %s: %v", c.ClientIP(), err) - sendErrorResponse(err.Error(), http.StatusBadRequest, c) + sendErrorWithMsg(err.Error(), errBadRequest, c) return } @@ -86,7 +85,7 @@ func feeAddress(c *gin.Context) { // Respond early if we already have the fee tx for this ticket. if ticket.FeeTxHex != "" { log.Warnf("Fee tx already received from %s: ticketHash=%s", c.ClientIP(), ticket.Hash) - sendErrorResponse("fee tx already received", http.StatusBadRequest, c) + sendError(errFeeAlreadyReceived, c) return } @@ -94,7 +93,7 @@ func feeAddress(c *gin.Context) { rawTicket, err := dcrdClient.GetRawTransaction(ticketHash) if err != nil { log.Errorf("Could not retrieve tx %s for %s: %v", ticketHash, c.ClientIP(), err) - sendErrorResponse(err.Error(), http.StatusInternalServerError, c) + sendError(errInternalError, c) return } @@ -102,12 +101,12 @@ func feeAddress(c *gin.Context) { canVote, err := dcrdClient.CanTicketVote(rawTicket, ticketHash, cfg.NetParams) if err != nil { log.Errorf("canTicketVote error: %v", err) - sendErrorResponse("error validating ticket", http.StatusInternalServerError, c) + sendError(errInternalError, c) return } if !canVote { log.Warnf("Unvotable ticket %s from %s", ticketHash, c.ClientIP()) - sendErrorResponse("ticket not eligible to vote", http.StatusBadRequest, c) + sendError(errTicketCannotVote, c) return } @@ -120,7 +119,7 @@ func feeAddress(c *gin.Context) { newFee, err := getCurrentFee(dcrdClient) if err != nil { log.Errorf("getCurrentFee error: %v", err) - sendErrorResponse("fee error", http.StatusInternalServerError, c) + sendError(errInternalError, c) return } ticket.FeeExpiration = now.Add(feeAddressExpiration).Unix() @@ -129,7 +128,7 @@ func feeAddress(c *gin.Context) { err = db.UpdateTicket(ticket) if err != nil { log.Errorf("UpdateTicket error: %v", err) - sendErrorResponse("database error", http.StatusInternalServerError, c) + sendError(errInternalError, c) return } log.Debugf("Expired fee updated for ticket: newFeeAmt=%f, ticketHash=%s", @@ -152,7 +151,7 @@ func feeAddress(c *gin.Context) { fee, err := getCurrentFee(dcrdClient) if err != nil { log.Errorf("getCurrentFee error: %v", err) - sendErrorResponse("fee error", http.StatusInternalServerError, c) + sendError(errInternalError, c) return } @@ -180,7 +179,7 @@ func feeAddress(c *gin.Context) { err = db.InsertNewTicket(dbTicket) if err != nil { log.Errorf("InsertTicket error: %v", err) - sendErrorResponse("database error", http.StatusInternalServerError, c) + sendError(errInternalError, c) return } diff --git a/webapi/middleware.go b/webapi/middleware.go index 6564799..bf64c0e 100644 --- a/webapi/middleware.go +++ b/webapi/middleware.go @@ -54,7 +54,7 @@ func withDcrdClient() gin.HandlerFunc { client, err := dcrd.Client(c, cfg.NetParams) if err != nil { log.Error(err) - sendErrorResponse("dcrd RPC error", http.StatusInternalServerError, c) + sendError(errInternalError, c) return } @@ -69,7 +69,7 @@ func withWalletClients() gin.HandlerFunc { clients, failedConnections := wallets.Clients(c, cfg.NetParams) if len(clients) == 0 { log.Error("Could not connect to any wallets") - sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) + sendError(errInternalError, c) return } if failedConnections > 0 { @@ -93,7 +93,7 @@ func vspAuth() gin.HandlerFunc { reqBytes, err := c.GetRawData() if err != nil { log.Warnf("Error reading request from %s: %v", c.ClientIP(), err) - sendErrorResponse(err.Error(), http.StatusBadRequest, c) + sendErrorWithMsg(err.Error(), errBadRequest, c) return } @@ -104,7 +104,7 @@ func vspAuth() gin.HandlerFunc { var request ticketHashRequest if err := binding.JSON.BindBody(reqBytes, &request); err != nil { log.Warnf("Bad request from %s: %v", c.ClientIP(), err) - sendErrorResponse(err.Error(), http.StatusBadRequest, c) + sendErrorWithMsg(err.Error(), errBadRequest, c) return } hash := request.TicketHash @@ -113,7 +113,7 @@ func vspAuth() gin.HandlerFunc { ticket, ticketFound, err := db.GetTicketByHash(hash) if err != nil { log.Errorf("GetTicketByHash error: %v", err) - sendErrorResponse("database error", http.StatusInternalServerError, c) + sendError(errInternalError, c) return } @@ -127,7 +127,7 @@ func vspAuth() gin.HandlerFunc { commitmentAddress, err = dcrdClient.GetTicketCommitmentAddress(hash, cfg.NetParams) if err != nil { log.Errorf("GetTicketCommitmentAddress error: %v", err) - sendErrorResponse(err.Error(), http.StatusInternalServerError, c) + sendError(errInternalError, c) return } } @@ -136,7 +136,7 @@ func vspAuth() gin.HandlerFunc { err = validateSignature(reqBytes, commitmentAddress, c) if err != nil { log.Warnf("Bad signature from %s: %v", c.ClientIP(), err) - sendErrorResponse("bad signature", http.StatusBadRequest, c) + sendError(errBadSignature, c) return } diff --git a/webapi/payfee.go b/webapi/payfee.go index 1438c79..1e5e38e 100644 --- a/webapi/payfee.go +++ b/webapi/payfee.go @@ -2,7 +2,6 @@ package webapi import ( "encoding/hex" - "net/http" "time" "github.com/decred/dcrd/dcrec" @@ -24,23 +23,28 @@ func payFee(c *gin.Context) { knownTicket := c.MustGet("KnownTicket").(bool) dcrdClient := c.MustGet("DcrdClient").(*rpc.DcrdRPC) + if cfg.VspClosed { + sendError(errVspClosed, c) + return + } + if !knownTicket { - log.Warnf("Invalid ticket from %s", c.ClientIP()) - sendErrorResponse("invalid ticket", http.StatusBadRequest, c) + log.Warnf("Unknown ticket from %s", c.ClientIP()) + sendError(errUnknownTicket, c) return } var payFeeRequest PayFeeRequest if err := binding.JSON.BindBody(rawRequest, &payFeeRequest); err != nil { log.Warnf("Bad payfee request from %s: %v", c.ClientIP(), err) - sendErrorResponse(err.Error(), http.StatusBadRequest, c) + sendErrorWithMsg(err.Error(), errBadRequest, c) return } // Respond early if we already have the fee tx for this ticket. if ticket.FeeTxHex != "" { log.Warnf("Fee tx already received from %s: ticketHash=%s", c.ClientIP(), ticket.Hash) - sendErrorResponse("fee tx already received", http.StatusBadRequest, c) + sendError(errFeeAlreadyReceived, c) return } @@ -48,7 +52,7 @@ func payFee(c *gin.Context) { rawTicket, err := dcrdClient.GetRawTransaction(ticket.Hash) if err != nil { log.Errorf("Could not retrieve tx %s for %s: %v", ticket.Hash, c.ClientIP(), err) - sendErrorResponse(err.Error(), http.StatusInternalServerError, c) + sendError(errInternalError, c) return } @@ -56,19 +60,19 @@ func payFee(c *gin.Context) { canVote, err := dcrdClient.CanTicketVote(rawTicket, ticket.Hash, cfg.NetParams) if err != nil { log.Errorf("canTicketVote error: %v", err) - sendErrorResponse("error validating ticket", http.StatusInternalServerError, c) + sendError(errInternalError, c) return } if !canVote { log.Warnf("Unvotable ticket %s from %s", ticket.Hash, c.ClientIP()) - sendErrorResponse("ticket not eligible to vote", http.StatusBadRequest, c) + sendError(errTicketCannotVote, c) return } // Respond early if the fee for this ticket is expired. if ticket.FeeExpired() { log.Warnf("Expired payfee request from %s", c.ClientIP()) - sendErrorResponse("fee has expired", http.StatusBadRequest, c) + sendError(errFeeExpired, c) return } @@ -77,7 +81,7 @@ func payFee(c *gin.Context) { votingWIF, err := dcrutil.DecodeWIF(votingKey, cfg.NetParams.PrivateKeyID) if err != nil { log.Warnf("Failed to decode WIF: %v", err) - sendErrorResponse("error decoding WIF", http.StatusBadRequest, c) + sendError(errInvalidPrivKey, c) return } @@ -86,7 +90,7 @@ func payFee(c *gin.Context) { err = isValidVoteChoices(cfg.NetParams, currentVoteVersion(cfg.NetParams), voteChoices) if err != nil { log.Warnf("Invalid votechoices from %s: %v", c.ClientIP(), err) - sendErrorResponse(err.Error(), http.StatusBadRequest, c) + sendErrorWithMsg(err.Error(), errInvalidVoteChoices, c) return } @@ -94,14 +98,14 @@ func payFee(c *gin.Context) { feeTxBytes, err := hex.DecodeString(payFeeRequest.FeeTx) if err != nil { log.Warnf("Failed to decode tx: %v", err) - sendErrorResponse("failed to decode transaction", http.StatusBadRequest, c) + sendError(errInvalidFeeTx, c) return } feeTx := wire.NewMsgTx() if err = feeTx.FromBytes(feeTxBytes); err != nil { log.Warnf("Failed to deserialize tx: %v", err) - sendErrorResponse("unable to deserialize transaction", http.StatusBadRequest, c) + sendError(errInvalidFeeTx, c) return } @@ -113,14 +117,14 @@ func payFee(c *gin.Context) { findAddress: for _, txOut := range feeTx.TxOut { if txOut.Version != scriptVersion { - sendErrorResponse("invalid script version", http.StatusBadRequest, c) + sendErrorWithMsg("invalid script version", errInvalidFeeTx, c) return } _, addresses, _, err := txscript.ExtractPkScriptAddrs(scriptVersion, txOut.PkScript, cfg.NetParams) if err != nil { log.Errorf("Extract PK error: %v", err) - sendErrorResponse("extract PK error", http.StatusBadRequest, c) + sendError(errInternalError, c) return } for _, addr := range addresses { @@ -133,7 +137,7 @@ findAddress: if feePaid == 0 { log.Warnf("FeeTx for ticket %s did not include any payments for address %s", ticket.Hash, ticket.FeeAddress) - sendErrorResponse("feetx did not include any payments for fee address", http.StatusBadRequest, c) + sendErrorWithMsg("feetx did not include any payments for fee address", errInvalidFeeTx, c) return } @@ -141,7 +145,7 @@ findAddress: dcrec.STEcdsaSecp256k1) if err != nil { log.Errorf("NewAddressPubKeyHash: %v", err) - sendErrorResponse("failed to deserialize voting wif", http.StatusInternalServerError, c) + sendError(errInvalidPrivKey, c) return } @@ -149,13 +153,13 @@ findAddress: ticketBytes, err := hex.DecodeString(rawTicket.Hex) if err != nil { log.Warnf("Failed to decode tx: %v", err) - sendErrorResponse("failed to decode transaction", http.StatusBadRequest, c) + sendError(errInternalError, c) return } ticketTx := wire.NewMsgTx() if err = ticketTx.FromBytes(ticketBytes); err != nil { log.Errorf("Failed to deserialize tx: %v", err) - sendErrorResponse("unable to deserialize transaction", http.StatusInternalServerError, c) + sendError(errInternalError, c) return } @@ -163,12 +167,12 @@ findAddress: _, votingAddr, _, err := txscript.ExtractPkScriptAddrs(scriptVersion, ticketTx.TxOut[0].PkScript, cfg.NetParams) if err != nil { log.Errorf("ExtractPK error: %v", err) - sendErrorResponse("extract PK error", http.StatusInternalServerError, c) + sendError(errInternalError, c) return } if len(votingAddr) == 0 { log.Error("No voting address found for ticket %s", ticket.Hash) - sendErrorResponse("no voting address found", http.StatusInternalServerError, c) + sendError(errInternalError, c) return } @@ -176,21 +180,21 @@ findAddress: if votingAddr[0].Address() != wifAddr.Address() { log.Warnf("Voting address does not match provided private key: "+ "votingAddr=%+v, wifAddr=%+v", votingAddr[0], wifAddr) - sendErrorResponse("voting address does not match provided private key", - http.StatusBadRequest, c) + sendErrorWithMsg("voting address does not match provided private key", + errInvalidPrivKey, c) return } minFee, err := dcrutil.NewAmount(ticket.FeeAmount) if err != nil { log.Errorf("dcrutil.NewAmount: %v", err) - sendErrorResponse("fee error", http.StatusInternalServerError, c) + sendError(errInternalError, c) return } if feePaid < minFee { log.Warnf("Fee too small from %s: was %v, expected %v", c.ClientIP(), feePaid, minFee) - sendErrorResponse("fee too small", http.StatusInternalServerError, c) + sendError(errFeeTooSmall, c) return } @@ -205,7 +209,7 @@ findAddress: err = db.UpdateTicket(ticket) if err != nil { log.Errorf("InsertTicket failed: %v", err) - sendErrorResponse("database error", http.StatusInternalServerError, c) + sendError(errInternalError, c) return } @@ -216,7 +220,7 @@ findAddress: feeTxHash, err := dcrdClient.SendRawTransaction(payFeeRequest.FeeTx) if err != nil { log.Errorf("SendRawTransaction failed: %v", err) - sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) + sendError(errInternalError, c) return } ticket.FeeTxHash = feeTxHash @@ -224,7 +228,7 @@ findAddress: err = db.UpdateTicket(ticket) if err != nil { log.Errorf("InsertTicket failed: %v", err) - sendErrorResponse("database error", http.StatusInternalServerError, c) + sendError(errInternalError, c) return } diff --git a/webapi/setvotechoices.go b/webapi/setvotechoices.go index 4374e16..053275e 100644 --- a/webapi/setvotechoices.go +++ b/webapi/setvotechoices.go @@ -1,7 +1,6 @@ package webapi import ( - "net/http" "time" "github.com/decred/vspd/database" @@ -20,8 +19,8 @@ func setVoteChoices(c *gin.Context) { walletClients := c.MustGet("WalletClients").([]*rpc.WalletRPC) if !knownTicket { - log.Warnf("Invalid ticket from %s", c.ClientIP()) - sendErrorResponse("invalid ticket", http.StatusBadRequest, c) + log.Warnf("Unknown ticket from %s", c.ClientIP()) + sendError(errUnknownTicket, c) return } @@ -30,7 +29,7 @@ func setVoteChoices(c *gin.Context) { var setVoteChoicesRequest SetVoteChoicesRequest if err := binding.JSON.BindBody(rawRequest, &setVoteChoicesRequest); err != nil { log.Warnf("Bad setvotechoices request from %s: %v", c.ClientIP(), err) - sendErrorResponse(err.Error(), http.StatusBadRequest, c) + sendErrorWithMsg(err.Error(), errBadRequest, c) return } @@ -38,7 +37,7 @@ func setVoteChoices(c *gin.Context) { err := isValidVoteChoices(cfg.NetParams, currentVoteVersion(cfg.NetParams), voteChoices) if err != nil { log.Warnf("Invalid votechoices from %s: %v", c.ClientIP(), err) - sendErrorResponse(err.Error(), http.StatusBadRequest, c) + sendErrorWithMsg(err.Error(), errInvalidVoteChoices, c) return } @@ -48,7 +47,7 @@ func setVoteChoices(c *gin.Context) { err = db.UpdateTicket(ticket) if err != nil { log.Errorf("UpdateTicket error: %v", err) - sendErrorResponse("database error", http.StatusInternalServerError, c) + sendError(errInternalError, c) return } diff --git a/webapi/ticketstatus.go b/webapi/ticketstatus.go index b802952..9dd0f17 100644 --- a/webapi/ticketstatus.go +++ b/webapi/ticketstatus.go @@ -1,7 +1,6 @@ package webapi import ( - "net/http" "time" "github.com/decred/vspd/database" @@ -18,15 +17,15 @@ func ticketStatus(c *gin.Context) { knownTicket := c.MustGet("KnownTicket").(bool) if !knownTicket { - log.Warnf("Invalid ticket from %s", c.ClientIP()) - sendErrorResponse("invalid ticket", http.StatusBadRequest, c) + log.Warnf("Unknown ticket from %s", c.ClientIP()) + sendError(errUnknownTicket, c) return } var ticketStatusRequest TicketStatusRequest if err := binding.JSON.BindBody(rawRequest, &ticketStatusRequest); err != nil { log.Warnf("Bad ticketstatus request from %s: %v", c.ClientIP(), err) - sendErrorResponse(err.Error(), http.StatusBadRequest, c) + sendErrorWithMsg(err.Error(), errBadRequest, c) return } diff --git a/webapi/webapi.go b/webapi/webapi.go index ee92eaa..404aa30 100644 --- a/webapi/webapi.go +++ b/webapi/webapi.go @@ -221,7 +221,7 @@ func sendJSONResponse(resp interface{}, c *gin.Context) { dec, err := json.Marshal(resp) if err != nil { log.Errorf("JSON marshal error: %v", err) - sendErrorResponse("failed to marshal json", http.StatusInternalServerError, c) + sendError(errInternalError, c) return } @@ -231,8 +231,22 @@ func sendJSONResponse(resp interface{}, c *gin.Context) { c.AbortWithStatusJSON(http.StatusOK, resp) } -func sendErrorResponse(errMsg string, code int, c *gin.Context) { - resp := gin.H{"error": errMsg} +// sendError returns an error response to the client using the default error +// message. +func sendError(e apiError, c *gin.Context) { + msg := e.defaultMessage() + sendErrorWithMsg(msg, e, c) +} + +// sendErrorWithMsg returns an error response to the client using the provided +// error message. +func sendErrorWithMsg(msg string, e apiError, c *gin.Context) { + status := e.httpStatus() + + resp := gin.H{ + "code": int(e), + "message": msg, + } // Try to sign the error response. If it fails, send it without a signature. dec, err := json.Marshal(resp) @@ -243,5 +257,5 @@ func sendErrorResponse(errMsg string, code int, c *gin.Context) { c.Writer.Header().Set("VSP-Server-Signature", hex.EncodeToString(sig)) } - c.AbortWithStatusJSON(code, resp) + c.AbortWithStatusJSON(status, resp) }