diff --git a/docs/api.md b/docs/api.md index 1747b0d..53399b9 100644 --- a/docs/api.md +++ b/docs/api.md @@ -68,7 +68,8 @@ for the specified ticket. ```json { "timestamp":1590509066, - "tickethash":"484a68f7148e55d05f0b64a29fe7b148572cb5272d1ce2438cf15466d347f4f4" + "tickethash":"1b9f5dc3b4872c47f66b148b0633647458123d72a0f0623a90890cc51a668737", + "tickethex":"0100000001a8...bfa6e4bf9c5ec1" } ``` diff --git a/webapi/middleware.go b/webapi/middleware.go index 616a1a9..e2f8086 100644 --- a/webapi/middleware.go +++ b/webapi/middleware.go @@ -2,6 +2,7 @@ package webapi import ( "bytes" + "errors" "io/ioutil" "net/http" "strings" @@ -12,12 +13,18 @@ import ( "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/gorilla/sessions" + "github.com/jrick/wsrpc/v2" ) type ticketHashRequest struct { TicketHash string `json:"tickethash" binding:"required"` } +type ticketRequest struct { + TicketHex string `json:"tickethex" binding:"required"` + TicketHash string `json:"tickethash" binding:"required"` +} + // 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. @@ -101,6 +108,81 @@ func withWalletClients(wallets rpc.WalletConnect) gin.HandlerFunc { } } +// ensureTicketBroadcast will parse ticket hash and ticket hex from the request +// body, and ensure the local dcrd instance can retrieve information about that +// ticket. If no info can be found, the ticket hex will be broadcast. +func ensureTicketBroadcast() gin.HandlerFunc { + return func(c *gin.Context) { + // Read request bytes and then replace the request reader for + // downstream handlers to use. + reqBytes, err := ioutil.ReadAll(c.Request.Body) + if err != nil { + log.Warnf("Error reading request from %s: %v", c.ClientIP(), err) + sendErrorWithMsg(err.Error(), errBadRequest, c) + return + } + c.Request.Body.Close() + c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(reqBytes)) + + // Parse request and ensure ticket hash and hex are included. + var request ticketRequest + if err := binding.JSON.BindBody(reqBytes, &request); err != nil { + log.Warnf("Bad request from %s: %v", c.ClientIP(), err) + sendErrorWithMsg(err.Error(), errBadRequest, c) + return + } + + // Ensure the provided hex is a valid ticket. + msgTx, err := decodeTransaction(request.TicketHex) + if err != nil { + log.Warnf("decodeTransaction error: %v", err) + sendErrorWithMsg("cannot decode ticket hex", errBadRequest, c) + return + } + + err = isValidTicket(msgTx) + if err != nil { + log.Warnf("Invalid ticket from %s: %v", c.ClientIP(), err) + sendError(errInvalidTicket, c) + return + } + + // Ensure hex matches hash. + if msgTx.TxHash().String() != request.TicketHash { + log.Warnf("Ticket hex/hash mismatch from %s", c.ClientIP()) + sendErrorWithMsg("ticket hex does not match hash", errBadRequest, c) + return + } + + dcrdClient := c.MustGet("DcrdClient").(*rpc.DcrdRPC) + + // Use GetRawTransaction to check if local dcrd already knows this + // ticket. + _, err = dcrdClient.GetRawTransaction(request.TicketHash) + if err == nil { + // No error means dcrd 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. + var e *wsrpc.Error + if errors.As(err, &e) && e.Code == rpc.ErrNoTxInfo { + log.Debugf("Broadcasting ticket with hash %s", request.TicketHash) + err = dcrdClient.SendRawTransaction(request.TicketHex) + if err != nil { + log.Errorf("SendRawTransaction error: %v", err) + sendError(errInternalError, c) + return + } + } else { + log.Errorf("GetRawTransaction error: %v", 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. diff --git a/webapi/types.go b/webapi/types.go index 4e6e480..e5b0bc1 100644 --- a/webapi/types.go +++ b/webapi/types.go @@ -11,6 +11,7 @@ type vspInfoResponse struct { type FeeAddressRequest struct { Timestamp int64 `json:"timestamp" binding:"required"` TicketHash string `json:"tickethash" binding:"required"` + TicketHex string `json:"tickethex" binding:"required"` } type feeAddressResponse struct { diff --git a/webapi/webapi.go b/webapi/webapi.go index 8e3d71a..7f45f1c 100644 --- a/webapi/webapi.go +++ b/webapi/webapi.go @@ -183,21 +183,22 @@ func router(debugMode bool, cookieSecret []byte, dcrd rpc.DcrdConnect, wallets r // Serve static web resources router.Static("/public", "webapi/public/") - // These routes have no extra middleware. They can be accessed by anybody. - router.GET("", homepage) - router.GET("/api/vspinfo", vspInfo) - - // These API routes access dcrd and they need authentication. - feeOnly := router.Group("/api").Use( - withDcrdClient(dcrd), vspAuth(), - ) - feeOnly.POST("/feeaddress", feeAddress) - feeOnly.GET("/ticketstatus", ticketStatus) - feeOnly.POST("/payfee", payFee) - // Create a cookie store for persisting admin session information. cookieStore := sessions.NewCookieStore(cookieSecret) + // API routes. + + api := router.Group("/api") + api.GET("/vspinfo", vspInfo) + api.POST("/feeaddress", withDcrdClient(dcrd), ensureTicketBroadcast(), vspAuth(), feeAddress) + api.GET("/ticketstatus", withDcrdClient(dcrd), vspAuth(), ticketStatus) + api.POST("/payfee", withDcrdClient(dcrd), vspAuth(), payFee) + api.POST("/setvotechoices", withDcrdClient(dcrd), withWalletClients(wallets), vspAuth(), setVoteChoices) + + // Website routes. + + router.GET("", homepage) + login := router.Group("/admin").Use( withSession(cookieStore), ) @@ -211,13 +212,6 @@ func router(debugMode bool, cookieSecret []byte, dcrd rpc.DcrdConnect, wallets r admin.GET("/backup", downloadDatabaseBackup) admin.POST("/logout", adminLogout) - // These API routes access dcrd and the voting wallets, and they need - // authentication. - both := router.Group("/api").Use( - withDcrdClient(dcrd), withWalletClients(wallets), vspAuth(), - ) - both.POST("/setvotechoices", setVoteChoices) - return router }