diff --git a/database/database.go b/database/database.go index 4ea8ef9..62f4431 100644 --- a/database/database.go +++ b/database/database.go @@ -23,7 +23,8 @@ import ( // VspDatabase wraps an instance of bolt.DB and provides VSP specific // convenience functions. type VspDatabase struct { - db *bolt.DB + db *bolt.DB + maxVoteChangeRecords int ticketsMtx sync.RWMutex } @@ -35,6 +36,8 @@ var ( vspBktK = []byte("vspbkt") // ticketbkt stores all tickets known by this VSP. ticketBktK = []byte("ticketbkt") + // votechangebkt stores records of web requests which update vote choices. + voteChangeBktK = []byte("votechangebkt") // version is the current database version. versionK = []byte("version") // feeXPub is the extended public key used for collecting VSP fees. @@ -150,6 +153,12 @@ func CreateNew(dbFile, feeXPub string) error { return fmt.Errorf("failed to create %s bucket: %v", string(ticketBktK), err) } + // Create vote change bucket. + _, err = vspBkt.CreateBucket(voteChangeBktK) + if err != nil { + return fmt.Errorf("failed to create %s bucket: %v", string(voteChangeBktK), err) + } + return nil }) @@ -164,7 +173,7 @@ func CreateNew(dbFile, feeXPub string) error { // Open initializes and returns an open database. An error is returned if no // database file is found at the provided path. -func Open(ctx context.Context, shutdownWg *sync.WaitGroup, dbFile string, backupInterval time.Duration) (*VspDatabase, error) { +func Open(ctx context.Context, shutdownWg *sync.WaitGroup, dbFile string, backupInterval time.Duration, maxVoteChangeRecords int) (*VspDatabase, error) { // Error if db file does not exist. This is needed because bolt.Open will // silently create a new empty database if the file does not exist. A new @@ -200,7 +209,7 @@ func Open(ctx context.Context, shutdownWg *sync.WaitGroup, dbFile string, backup } }() - return &VspDatabase{db: db}, nil + return &VspDatabase{db: db, maxVoteChangeRecords: maxVoteChangeRecords}, nil } // Close will close the database and then make a copy of the database to the diff --git a/database/database_test.go b/database/database_test.go index 7d30e76..556009e 100644 --- a/database/database_test.go +++ b/database/database_test.go @@ -13,9 +13,10 @@ import ( ) var ( - testDb = "test.db" - backupDb = "test.db-backup" - db *VspDatabase + testDb = "test.db" + backupDb = "test.db-backup" + db *VspDatabase + maxVoteChangeRecords = 3 ) // TestDatabase runs all database tests. @@ -26,13 +27,14 @@ func TestDatabase(t *testing.T) { // All sub-tests to run. tests := map[string]func(*testing.T){ - "testInsertNewTicket": testInsertNewTicket, - "testGetTicketByHash": testGetTicketByHash, - "testUpdateTicket": testUpdateTicket, - "testTicketFeeExpired": testTicketFeeExpired, - "testFilterTickets": testFilterTickets, - "testAddressIndex": testAddressIndex, - "testDeleteTicket": testDeleteTicket, + "testInsertNewTicket": testInsertNewTicket, + "testGetTicketByHash": testGetTicketByHash, + "testUpdateTicket": testUpdateTicket, + "testTicketFeeExpired": testTicketFeeExpired, + "testFilterTickets": testFilterTickets, + "testAddressIndex": testAddressIndex, + "testDeleteTicket": testDeleteTicket, + "testVoteChangeRecords": testVoteChangeRecords, } for testName, test := range tests { @@ -44,7 +46,7 @@ func TestDatabase(t *testing.T) { if err != nil { t.Fatalf("error creating test database: %v", err) } - db, err = Open(ctx, &wg, testDb, time.Hour) + db, err = Open(ctx, &wg, testDb, time.Hour, maxVoteChangeRecords) if err != nil { t.Fatalf("error opening test database: %v", err) } diff --git a/database/ticket.go b/database/ticket.go index 403bdea..dd64aa7 100644 --- a/database/ticket.go +++ b/database/ticket.go @@ -139,6 +139,7 @@ func (vdb *VspDatabase) DeleteTicket(ticket Ticket) error { return nil }) } + func (vdb *VspDatabase) UpdateTicket(ticket Ticket) error { defer vdb.ticketsMtx.Unlock() vdb.ticketsMtx.Lock() diff --git a/database/votechange.go b/database/votechange.go new file mode 100644 index 0000000..a9f9415 --- /dev/null +++ b/database/votechange.go @@ -0,0 +1,131 @@ +// Copyright (c) 2020 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package database + +import ( + "encoding/binary" + "encoding/json" + "fmt" + "math" + + bolt "go.etcd.io/bbolt" +) + +// VoteChangeRecord is serialized to json and stored in bbolt db. The json keys +// are deliberately kept short because they are duplicated many times in the db. +type VoteChangeRecord struct { + Request string `json:"req"` + RequestSignature string `json:"reqs"` + Response string `json:"rsp"` + ResponseSignature string `json:"rsps"` +} + +// SaveVoteChange will insert the provided vote change record into the database, +// and if this breaches the maximum amount of allowed records, delete the oldest +// one which is currently stored. +func (vdb *VspDatabase) SaveVoteChange(ticketHash string, record VoteChangeRecord) error { + + return vdb.db.Update(func(tx *bolt.Tx) error { + // Create or get a bucket for this ticket. + bkt, err := tx.Bucket(vspBktK).Bucket(voteChangeBktK). + CreateBucketIfNotExists([]byte(ticketHash)) + if err != nil { + return fmt.Errorf("failed to create vote change bucket (ticketHash=%s): %v", + ticketHash, err) + } + + // Serialize record for storage in the database. + recordBytes, err := json.Marshal(record) + if err != nil { + return fmt.Errorf("could not marshal vote change record: %v", err) + } + + // Records are stored using a serially increasing integer as the key. + + // Loop through the bucket to count the records, as well as finding the + // most recent and the oldest record. + var count int + highest := uint32(0) + lowest := uint32(math.MaxUint32) + err = bkt.ForEach(func(k, v []byte) error { + count++ + key := binary.LittleEndian.Uint32(k) + if key > highest { + highest = key + } + if key < lowest { + lowest = key + } + return nil + }) + if err != nil { + return fmt.Errorf("error iterating over vote change bucket: %v", err) + } + + // If bucket is at (or over) the limit of max allowed records, remove + // the oldest one. + if count >= vdb.maxVoteChangeRecords { + keyBytes := make([]byte, 4) + binary.LittleEndian.PutUint32(keyBytes, lowest) + err = bkt.Delete(keyBytes) + if err != nil { + return fmt.Errorf("failed to delete old vote change record: %v", err) + } + } + + // Insert record with index 0 if the bucket is currently empty, + // otherwise use most recent + 1. + var newKey uint32 + if count > 0 { + newKey = highest + 1 + } + + keyBytes := make([]byte, 4) + binary.LittleEndian.PutUint32(keyBytes, newKey) + + // Insert record. + err = bkt.Put(keyBytes, recordBytes) + if err != nil { + return fmt.Errorf("could not store vote change record: %v", err) + } + + return nil + }) +} + +// GetVoteChanges retrieves all of the stored vote change records for the +// provided ticket hash. +func (vdb *VspDatabase) GetVoteChanges(ticketHash string) (map[uint32]VoteChangeRecord, error) { + + records := make(map[uint32]VoteChangeRecord) + + err := vdb.db.View(func(tx *bolt.Tx) error { + bkt := tx.Bucket(vspBktK).Bucket(voteChangeBktK). + Bucket([]byte(ticketHash)) + + if bkt == nil { + return nil + } + + err := bkt.ForEach(func(k, v []byte) error { + var record VoteChangeRecord + err := json.Unmarshal(v, &record) + if err != nil { + return fmt.Errorf("could not unmarshal vote change record: %v", err) + } + + records[binary.LittleEndian.Uint32(k)] = record + + return nil + }) + if err != nil { + return fmt.Errorf("error iterating over vote change bucket: %v", err) + } + + return nil + }) + + return records, err +} diff --git a/database/votechange_test.go b/database/votechange_test.go new file mode 100644 index 0000000..3dd11f8 --- /dev/null +++ b/database/votechange_test.go @@ -0,0 +1,66 @@ +// Copyright (c) 2020 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package database + +import ( + "testing" +) + +func exampleRecord() VoteChangeRecord { + return VoteChangeRecord{ + Request: "Request", + RequestSignature: "RequestSignature", + Response: "Response", + ResponseSignature: "ResponseSignature", + } +} + +func testVoteChangeRecords(t *testing.T) { + hash := "MyHash" + record := exampleRecord() + + // Insert a record into the database. + err := db.SaveVoteChange(hash, record) + if err != nil { + t.Fatalf("error storing vote change record in database: %v", err) + } + + // Retrieve record and check values. + retrieved, err := db.GetVoteChanges(hash) + if err != nil { + t.Fatalf("error retrieving vote change records: %v", err) + } + + if len(retrieved) != 1 || + retrieved[0].Request != record.Request || + retrieved[0].RequestSignature != record.RequestSignature || + retrieved[0].Response != record.Response || + retrieved[0].ResponseSignature != record.ResponseSignature { + t.Fatal("retrieved record didnt match expected") + } + + // Insert some more records, giving us one greater than the limit. + for i := 0; i < maxVoteChangeRecords; i++ { + err = db.SaveVoteChange(hash, record) + if err != nil { + t.Fatalf("error storing vote change record in database: %v", err) + } + } + + // Retrieve records. + retrieved, err = db.GetVoteChanges(hash) + if err != nil { + t.Fatalf("error retrieving vote change records: %v", err) + } + + // Oldest record should have been deleted. + if len(retrieved) != maxVoteChangeRecords { + t.Fatalf("vote change record limit breached") + } + + if _, ok := retrieved[0]; ok { + t.Fatalf("oldest vote change record should have been deleted") + } +} diff --git a/vspd.go b/vspd.go index ec4d30d..b667c3c 100644 --- a/vspd.go +++ b/vspd.go @@ -19,6 +19,12 @@ import ( "github.com/decred/vspd/webapi" ) +// maxVoteChangeRecords defines how many vote change records will be stored for +// each ticket. The limit is in place to mitigate DoS attacks on server storage +// space. When storing a new record breaches this limit, the oldest record in +// the database is deleted. +const maxVoteChangeRecords = 10 + func main() { // Create a context that is cancelled when a shutdown request is received // through an interrupt signal. @@ -59,7 +65,7 @@ func run(ctx context.Context) error { defer log.Info("Shutdown complete") // Open database. - db, err := database.Open(ctx, &shutdownWg, cfg.dbPath, cfg.BackupInterval) + db, err := database.Open(ctx, &shutdownWg, cfg.dbPath, cfg.BackupInterval, maxVoteChangeRecords) if err != nil { log.Errorf("Database error: %v", err) requestShutdown() @@ -79,13 +85,14 @@ func run(ctx context.Context) error { // Create and start webapi server. apiCfg := webapi.Config{ - VSPFee: cfg.VSPFee, - NetParams: cfg.netParams.Params, - SupportEmail: cfg.SupportEmail, - VspClosed: cfg.VspClosed, - AdminPass: cfg.AdminPass, - Debug: cfg.WebServerDebug, - Designation: cfg.Designation, + VSPFee: cfg.VSPFee, + NetParams: cfg.netParams.Params, + SupportEmail: cfg.SupportEmail, + VspClosed: cfg.VspClosed, + AdminPass: cfg.AdminPass, + Debug: cfg.WebServerDebug, + Designation: cfg.Designation, + MaxVoteChangeRecords: maxVoteChangeRecords, } err = webapi.Start(ctx, shutdownRequestChannel, &shutdownWg, cfg.Listen, db, dcrd, wallets, apiCfg) diff --git a/webapi/admin.go b/webapi/admin.go index 06c90ef..9418e83 100644 --- a/webapi/admin.go +++ b/webapi/admin.go @@ -100,16 +100,25 @@ func ticketSearch(c *gin.Context) { ticket, found, err := db.GetTicketByHash(hash) if err != nil { - log.Errorf("db.GetTicketByHash error: %v", err) + log.Errorf("db.GetTicketByHash error (ticketHash=%s): %v", hash, err) c.String(http.StatusInternalServerError, "Error getting ticket from db") return } + voteChanges, err := db.GetVoteChanges(hash) + if err != nil { + log.Errorf("db.GetVoteChanges error (ticketHash=%s): %v", hash, err) + c.String(http.StatusInternalServerError, "Error getting vote changes from db") + return + } + c.HTML(http.StatusOK, "admin.html", gin.H{ "SearchResult": gin.H{ - "Hash": hash, - "Found": found, - "Ticket": ticket, + "Hash": hash, + "Found": found, + "Ticket": ticket, + "VoteChanges": voteChanges, + "MaxVoteChanges": cfg.MaxVoteChangeRecords, }, "VspStats": getVSPStats(), "WalletStatus": walletStatus(c), diff --git a/webapi/getfeeaddress.go b/webapi/getfeeaddress.go index 934b221..d75588e 100644 --- a/webapi/getfeeaddress.go +++ b/webapi/getfeeaddress.go @@ -13,6 +13,7 @@ import ( "github.com/decred/vspd/database" "github.com/decred/vspd/rpc" "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" ) // addrMtx protects getNewFeeAddress. @@ -71,6 +72,7 @@ func feeAddress(c *gin.Context) { knownTicket := c.MustGet("KnownTicket").(bool) commitmentAddress := c.MustGet("CommitmentAddress").(string) dcrdClient := c.MustGet("DcrdClient").(*rpc.DcrdRPC) + reqBytes := c.MustGet("RequestBytes").([]byte) if cfg.VspClosed { sendError(errVspClosed, c) @@ -78,7 +80,7 @@ func feeAddress(c *gin.Context) { } var request feeAddressRequest - if err := c.ShouldBindJSON(&request); err != nil { + 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 diff --git a/webapi/middleware.go b/webapi/middleware.go index 1a773e0..ad0fbfd 100644 --- a/webapi/middleware.go +++ b/webapi/middleware.go @@ -203,16 +203,17 @@ func vspAuth() gin.HandlerFunc { return func(c *gin.Context) { const funcName = "vspAuth" - // Read request bytes and then replace the request reader for - // downstream handlers to use. + // Read request bytes. reqBytes, err := ioutil.ReadAll(c.Request.Body) if err != nil { log.Warnf("%s: Error reading request (clientIP=%s): %v", funcName, c.ClientIP(), err) sendErrorWithMsg(err.Error(), errBadRequest, c) return } - c.Request.Body.Close() - c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(reqBytes)) + + // Add request bytes to request context for downstream handlers to reuse. + // Necessary because the request body reader can only be used once. + c.Set("RequestBytes", reqBytes) // Parse request and ensure there is a ticket hash included. var request ticketHashRequest diff --git a/webapi/payfee.go b/webapi/payfee.go index 41a6a28..d2f8e9e 100644 --- a/webapi/payfee.go +++ b/webapi/payfee.go @@ -14,6 +14,7 @@ import ( "github.com/decred/vspd/database" "github.com/decred/vspd/rpc" "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" ) // payFee is the handler for "POST /api/v3/payfee". @@ -24,6 +25,7 @@ func payFee(c *gin.Context) { ticket := c.MustGet("Ticket").(database.Ticket) knownTicket := c.MustGet("KnownTicket").(bool) dcrdClient := c.MustGet("DcrdClient").(*rpc.DcrdRPC) + reqBytes := c.MustGet("RequestBytes").([]byte) if cfg.VspClosed { sendError(errVspClosed, c) @@ -37,7 +39,7 @@ func payFee(c *gin.Context) { } var request payFeeRequest - if err := c.ShouldBindJSON(&request); err != nil { + 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 @@ -257,8 +259,22 @@ findAddress: funcName, ticket.Hash, ticket.FeeTxHash) } - sendJSONResponse(payFeeResponse{ + // Send success response to client. + resp, respSig := sendJSONResponse(payFeeResponse{ Timestamp: time.Now().Unix(), Request: request, }, c) + + // Store a record of the vote choice change. + err = db.SaveVoteChange( + ticket.Hash, + database.VoteChangeRecord{ + Request: string(reqBytes), + RequestSignature: c.GetHeader("VSP-Client-Signature"), + Response: resp, + ResponseSignature: respSig, + }) + if err != nil { + log.Errorf("%s: Failed to store vote change record (ticketHash=%s): %v", err) + } } diff --git a/webapi/public/css/vspd.css b/webapi/public/css/vspd.css index 17340a3..b20055f 100644 --- a/webapi/public/css/vspd.css +++ b/webapi/public/css/vspd.css @@ -132,6 +132,7 @@ td.status-bad{ text-align: right; } .ticket-table td { + font-size: 14px; text-align: left; } diff --git a/webapi/setvotechoices.go b/webapi/setvotechoices.go index 7ce73b7..e2d7fbd 100644 --- a/webapi/setvotechoices.go +++ b/webapi/setvotechoices.go @@ -5,11 +5,13 @@ package webapi import ( + "fmt" "time" "github.com/decred/vspd/database" "github.com/decred/vspd/rpc" "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" ) // setVoteChoices is the handler for "POST /api/v3/setvotechoices". @@ -20,6 +22,7 @@ func setVoteChoices(c *gin.Context) { ticket := c.MustGet("Ticket").(database.Ticket) knownTicket := c.MustGet("KnownTicket").(bool) walletClients := c.MustGet("WalletClients").([]*rpc.WalletRPC) + reqBytes := c.MustGet("RequestBytes").([]byte) // If we cannot set the vote choices on at least one voting wallet right // now, don't update the database, just return an error. @@ -41,8 +44,17 @@ func setVoteChoices(c *gin.Context) { return } + // Only allow vote choices to be updated for live/immature tickets. + if ticket.Outcome != "" { + log.Warnf("%s: Ticket not eligible to vote (clientIP=%s, ticketHash=%s)", + funcName, c.ClientIP(), ticket.Hash) + sendErrorWithMsg(fmt.Sprintf("ticket not eligible to vote (status=%s)", ticket.Outcome), + errTicketCannotVote, c) + return + } + var request setVoteChoicesRequest - if err := c.ShouldBindJSON(&request); err != nil { + 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 @@ -88,10 +100,22 @@ func setVoteChoices(c *gin.Context) { // TODO: DB - error if given timestamp is older than any previous requests - // TODO: DB - store setvotechoices receipt in log - - sendJSONResponse(setVoteChoicesResponse{ + // Send success response to client. + resp, respSig := sendJSONResponse(setVoteChoicesResponse{ Timestamp: time.Now().Unix(), Request: request, }, c) + + // Store a record of the vote choice change. + err = db.SaveVoteChange( + ticket.Hash, + database.VoteChangeRecord{ + Request: string(reqBytes), + RequestSignature: c.GetHeader("VSP-Client-Signature"), + Response: resp, + ResponseSignature: respSig, + }) + if err != nil { + log.Errorf("%s: Failed to store vote change record (ticketHash=%s): %v", err) + } } diff --git a/webapi/templates/admin.html b/webapi/templates/admin.html index 87d7dee..0a9c6a4 100644 --- a/webapi/templates/admin.html +++ b/webapi/templates/admin.html @@ -86,73 +86,109 @@
| Hash | -{{ .Hash }} | -
|---|---|
| Commitment Address | -{{ .CommitmentAddress }} | -
| Fee Address Index | -{{ .FeeAddressIndex }} | -
| Fee Address | -{{ .FeeAddress }} | -
| Fee Amount | -{{ .FeeAmount }} atoms | -
| Fee Expiration | -{{ .FeeExpiration }} | -
| Confirmed | -{{ .Confirmed }} | -
| Vote Choices | -
- {{ range $key, $value := .VoteChoices }}
- {{ $key }}: {{ $value }} - {{ end }} - |
-
| Voting WIF | -{{ .VotingWIF }} | -
| Fee Tx | -{{ .FeeTxHex }} | -
| Fee Tx Hash | -{{ .FeeTxHash }} | -
| Fee Tx Status | -{{ .FeeTxStatus }} | -
| Ticket Outcome | -{{ .Outcome }} | -
| Hash | +{{ .Ticket.Hash }} | +||||||||
|---|---|---|---|---|---|---|---|---|---|
| Commitment Address | +{{ .Ticket.CommitmentAddress }} | +||||||||
| Fee Address Index | +{{ .Ticket.FeeAddressIndex }} | +||||||||
| Fee Address | +{{ .Ticket.FeeAddress }} | +||||||||
| Fee Amount | +{{ .Ticket.FeeAmount }} atoms | +||||||||
| Fee Expiration | +{{ .Ticket.FeeExpiration }} | +||||||||
| Confirmed | +{{ .Ticket.Confirmed }} | +||||||||
| Current Vote Choices | +
+ {{ range $key, $value := .Ticket.VoteChoices }}
+ {{ $key }}: {{ $value }} + {{ end }} + |
+ ||||||||
|
+ Vote Choice Changes + ({{ .MaxVoteChanges }} most recent) + |
+
+ {{ range $key, $value := .VoteChanges }}
+
+
+ {{end}}
+ + {{ if eq $key 0}} + Initial choices + {{ else }} + Change {{ $key }} + {{ end }} ++
|
+ ||||||||
| Voting WIF | +{{ .Ticket.VotingWIF }} | +||||||||
| Fee Tx | +{{ .Ticket.FeeTxHex }} | +||||||||
| Fee Tx Hash | +{{ .Ticket.FeeTxHash }} | +||||||||
| Fee Tx Status | +{{ .Ticket.FeeTxStatus }} | +||||||||
| Ticket Outcome | +{{ .Ticket.Outcome }} | +
No ticket found with hash {{ .Hash }}
{{ end }} diff --git a/webapi/ticketstatus.go b/webapi/ticketstatus.go index 0f25b94..3f61504 100644 --- a/webapi/ticketstatus.go +++ b/webapi/ticketstatus.go @@ -9,6 +9,7 @@ import ( "github.com/decred/vspd/database" "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" ) // ticketStatus is the handler for "POST /api/v3/ticketstatus". @@ -18,6 +19,7 @@ func ticketStatus(c *gin.Context) { // Get values which have been added to context by middleware. ticket := c.MustGet("Ticket").(database.Ticket) knownTicket := c.MustGet("KnownTicket").(bool) + reqBytes := c.MustGet("RequestBytes").([]byte) if !knownTicket { log.Warnf("%s: Unknown ticket (clientIP=%s)", funcName, c.ClientIP()) @@ -26,7 +28,7 @@ func ticketStatus(c *gin.Context) { } var request ticketStatusRequest - if err := c.ShouldBindJSON(&request); err != nil { + 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 diff --git a/webapi/webapi.go b/webapi/webapi.go index 3ea2d05..92c53e7 100644 --- a/webapi/webapi.go +++ b/webapi/webapi.go @@ -23,14 +23,15 @@ import ( ) type Config struct { - VSPFee float64 - NetParams *chaincfg.Params - FeeAccountName string - SupportEmail string - VspClosed bool - AdminPass string - Debug bool - Designation string + VSPFee float64 + NetParams *chaincfg.Params + FeeAccountName string + SupportEmail string + VspClosed bool + AdminPass string + Debug bool + Designation string + MaxVoteChangeRecords int } const ( @@ -227,28 +228,34 @@ func router(debugMode bool, cookieSecret []byte, dcrd rpc.DcrdConnect, wallets r return router } -func sendJSONResponse(resp interface{}, c *gin.Context) { +// sendJSONResponse serializes the provided response, signs it, and sends the +// response to the client with a 200 OK status. Returns the seralized response +// and the signature. +func sendJSONResponse(resp interface{}, c *gin.Context) (string, string) { dec, err := json.Marshal(resp) if err != nil { log.Errorf("JSON marshal error: %v", err) sendError(errInternalError, c) - return + return "", "" } sig := ed25519.Sign(signPrivKey, dec) - c.Writer.Header().Set("VSP-Server-Signature", base64.StdEncoding.EncodeToString(sig)) + sigStr := base64.StdEncoding.EncodeToString(sig) + c.Writer.Header().Set("VSP-Server-Signature", sigStr) c.AbortWithStatusJSON(http.StatusOK, resp) + + return string(dec), sigStr } -// sendError returns an error response to the client using the default error +// sendError sends 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 +// sendErrorWithMsg sends an error response to the client using the provided // error message. func sendErrorWithMsg(msg string, e apiError, c *gin.Context) { status := e.httpStatus()