@@ -6,10 +6,13 @@ package webapi
6
6
7
7
import (
8
8
"bytes"
9
+ "encoding/base64"
9
10
"errors"
11
+ "fmt"
10
12
"io"
11
13
"net/http"
12
14
"strings"
15
+ "time"
13
16
14
17
"github.com/decred/vspd/rpc"
15
18
"github.com/gin-gonic/gin"
@@ -18,6 +21,10 @@ import (
18
21
"github.com/jrick/wsrpc/v2"
19
22
)
20
23
24
+ // TicketSearchMessageFmt is the format for the message to be signed
25
+ // in order to search for a ticket using the vspd frontend.
26
+ const TicketSearchMessageFmt = "I want to check vspd ticket status for ticket %s at vsp with pubkey %s on window %d."
27
+
21
28
// withSession middleware adds a gorilla session to the request context for
22
29
// downstream handlers to make use of. Sessions are used by admin pages to
23
30
// maintain authentication status.
@@ -349,3 +356,111 @@ func (s *Server) vspAuth(c *gin.Context) {
349
356
c .Set (knownTicketKey , ticketFound )
350
357
c .Set (commitmentAddressKey , commitmentAddress )
351
358
}
359
+
360
+ // ticketSearchAuth middleware reads the request form body and extracts the
361
+ // ticket hash and signature from the base64 string provided. The commitment
362
+ // address for the ticket is retrieved from the database if it is known, or it
363
+ // is retrieved from the chain if not. The middleware errors out if required
364
+ // information is not provided or the signature does not contain a message
365
+ // signed with the commitment address. Ticket information is added to the
366
+ // request context for downstream handlers to use.
367
+ func (s * Server ) ticketSearchAuth (c * gin.Context ) {
368
+ funcName := "ticketSearchAuth"
369
+
370
+ encodedString := c .PostForm ("encoded" )
371
+
372
+ // Get information added to context.
373
+ dcrdClient := c .MustGet (dcrdKey ).(* rpc.DcrdRPC )
374
+ dcrdErr := c .MustGet (dcrdErrorKey )
375
+ if dcrdErr != nil {
376
+ s .log .Errorf ("%s: Could not get dcrd client: %v" , funcName , dcrdErr .(error ))
377
+ c .Set (errorKey , errInternalError )
378
+ return
379
+ }
380
+
381
+ currentBlockHeader , err := dcrdClient .GetBestBlockHeader ()
382
+ if err != nil {
383
+ s .log .Errorf ("%s: Error getting best block header : %v" , funcName , err )
384
+ c .Set (errorKey , errInternalError )
385
+ // Average blocks per day for the current network.
386
+ blocksPerDay := (24 * time .Hour ) / s .cfg .NetParams .TargetTimePerBlock
387
+ blockWindow := int (currentBlockHeader .Height ) / int (blocksPerDay )
388
+
389
+ decodedByte , err := base64 .StdEncoding .DecodeString (encodedString )
390
+ if err != nil {
391
+ s .log .Errorf ("%s: Decoding form data error : %v" , funcName , err )
392
+ c .Set (errorKey , errBadRequest )
393
+ return
394
+ }
395
+
396
+ data := strings .Split (string (decodedByte ), ":" )
397
+ if len (data ) != 2 {
398
+ c .Set (errorKey , errBadRequest )
399
+ return
400
+ }
401
+
402
+ ticketHash := data [0 ]
403
+ signature := data [1 ]
404
+ vspPublicKey := s .cache .data .PubKey
405
+ messageSigned := fmt .Sprintf (TicketSearchMessageFmt , ticketHash , vspPublicKey , blockWindow )
406
+
407
+ // Before hitting the db or any RPC, ensure this is a valid ticket hash.
408
+ err = validateTicketHash (ticketHash )
409
+ if err != nil {
410
+ s .log .Errorf ("%s: Invalid ticket (clientIP=%s): %v" , funcName , c .ClientIP (), err )
411
+ c .Set (errorKey , errInvalidTicket )
412
+ return
413
+ }
414
+
415
+ // Check if this ticket already appears in the database.
416
+ ticket , ticketFound , err := s .db .GetTicketByHash (ticketHash )
417
+ if err != nil {
418
+ s .log .Errorf ("%s: db.GetTicketByHash error (ticketHash=%s): %v" , funcName , ticketHash , err )
419
+ c .Set (errorKey , errInternalError )
420
+ return
421
+ }
422
+
423
+ if ! ticketFound {
424
+ s .log .Warnf ("%s: Unknown ticket (clientIP=%s)" , funcName , c .ClientIP ())
425
+ c .Set (errorKey , errUnknownTicket )
426
+ return
427
+ }
428
+
429
+ // If the ticket was found in the database, we already know its
430
+ // commitment address. Otherwise we need to get it from the chain.
431
+ var commitmentAddress string
432
+ if ticketFound {
433
+ commitmentAddress = ticket .CommitmentAddress
434
+ } else {
435
+ commitmentAddress , err = getCommitmentAddress (ticketHash , dcrdClient , s .cfg .NetParams )
436
+ if err != nil {
437
+ s .log .Errorf ("%s: Failed to get commitment address (clientIP=%s, ticketHash=%s): %v" ,
438
+ funcName , c .ClientIP (), ticketHash , err )
439
+
440
+ var apiErr * apiError
441
+ if errors .Is (err , apiErr ) {
442
+ c .Set (errorKey , errInvalidTicket )
443
+ } else {
444
+ c .Set (errorKey , errInternalError )
445
+ }
446
+
447
+ return
448
+ }
449
+ }
450
+
451
+ // Validate request signature to ensure ticket ownership.
452
+ err = validateSignature (ticketHash , commitmentAddress , signature , messageSigned , s .db , s .cfg .NetParams )
453
+ if err != nil {
454
+ s .log .Errorf ("%s: Couldn't validate signature (clientIP=%s, ticketHash=%s): %v" ,
455
+ funcName , c .ClientIP (), ticketHash , err )
456
+ c .Set (errorKey , errBadSignature )
457
+ return
458
+ }
459
+
460
+ // Add ticket information to context so downstream handlers don't need
461
+ // to access the db for it.
462
+ c .Set (ticketKey , ticket )
463
+ c .Set (knownTicketKey , ticketFound )
464
+ c .Set (errorKey , nil )
465
+ }
466
+ }
0 commit comments