From e8bb3d518939b9fea30052b8f5dbc9d6de53b9c7 Mon Sep 17 00:00:00 2001 From: jholdstock Date: Sat, 30 May 2026 10:00:57 +0800 Subject: [PATCH 1/5] Require go 1.25. --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 05550c5b..5f914e75 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/decred/vspd -go 1.24.0 +go 1.25.0 require ( decred.org/dcrwallet/v5 v5.0.1 From d69f2cf211ee3a4b98ce21265dbd2181ae7eb9ed Mon Sep 17 00:00:00 2001 From: jholdstock Date: Sat, 30 May 2026 10:01:14 +0800 Subject: [PATCH 2/5] Remove unneeded crypto/rand error handling. --- database/database.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/database/database.go b/database/database.go index f7409ad7..8c91de27 100644 --- a/database/database.go +++ b/database/database.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2024 The Decred developers +// Copyright (c) 2020-2026 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -126,11 +126,10 @@ func CreateNew(dbFile, feeXPub string) error { } // Generate a secret key for initializing the cookie store. + // Since go 1.24, crypto/rand.Read will never return an error. secret := make([]byte, 32) - _, err = rand.Read(secret) - if err != nil { - return err - } + _, _ = rand.Read(secret) + err = vspBkt.Put(cookieSecretK, secret) if err != nil { return err From f154716ed8aefb420cb1339295ce8c28881dff12 Mon Sep 17 00:00:00 2001 From: jholdstock Date: Sat, 30 May 2026 10:05:08 +0800 Subject: [PATCH 3/5] Use WaitGroup.Go. As documented in the sync package, using WaitGroup.Go is now recommended over using .Add and .Done. --- cmd/vspd/main.go | 20 +++++++------------- internal/webapi/webapi.go | 19 ++++++------------- 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/cmd/vspd/main.go b/cmd/vspd/main.go index 796a9bd9..49375765 100644 --- a/cmd/vspd/main.go +++ b/cmd/vspd/main.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2024 The Decred developers +// Copyright (c) 2020-2026 The Decred developers // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. @@ -158,27 +158,21 @@ func run() int { var wg sync.WaitGroup // Start the webapi server. - wg.Add(1) - go func() { + wg.Go(func() { api.Run(ctx) - wg.Done() - }() + }) // Start vspd. vspd := vspd.New(network, log, db, dcrd, wallets, blockNotifChan) - wg.Add(1) - go func() { + wg.Go(func() { vspd.Run(ctx) - wg.Done() - }() + }) // Periodically write a database backup file. - wg.Add(1) - go func() { + wg.Go(func() { for { select { case <-ctx.Done(): - wg.Done() return case <-time.After(cfg.BackupInterval): err := db.WriteHotBackupFile() @@ -187,7 +181,7 @@ func run() int { } } } - }() + }) // Wait for shutdown tasks to complete before running deferred tasks and // returning. diff --git a/internal/webapi/webapi.go b/internal/webapi/webapi.go index f7a1fefb..4856f2ac 100644 --- a/internal/webapi/webapi.go +++ b/internal/webapi/webapi.go @@ -147,8 +147,7 @@ func (w *WebAPI) Run(ctx context.Context) { var wg sync.WaitGroup // Add the graceful shutdown to the waitgroup. - wg.Add(1) - go func() { + wg.Go(func() { // Wait until context is canceled before shutting down the server. <-ctx.Done() @@ -159,13 +158,10 @@ func (w *WebAPI) Run(ctx context.Context) { cancel() w.log.Debug("Webserver stopped") - - wg.Done() - }() + }) // Start webserver. - wg.Add(1) - go func() { + wg.Go(func() { w.log.Infof("Listening on %s", w.listener.Addr()) err := w.server.Serve(w.listener) // ErrServerClosed is expected from a graceful server shutdown, it can @@ -173,12 +169,10 @@ func (w *WebAPI) Run(ctx context.Context) { if err != nil && !errors.Is(err, http.ErrServerClosed) { w.log.Errorf("Unexpected webserver error: %v", err) } - wg.Done() - }() + }) // Periodically update cached VSP stats. - wg.Add(1) - go func() { + wg.Go(func() { refresh := 1 * time.Minute if w.cfg.Debug { refresh = 1 * time.Second @@ -186,7 +180,6 @@ func (w *WebAPI) Run(ctx context.Context) { for { select { case <-ctx.Done(): - wg.Done() return case <-time.After(refresh): err := w.cache.update() @@ -195,7 +188,7 @@ func (w *WebAPI) Run(ctx context.Context) { } } } - }() + }) wg.Wait() } From 37de394dd51e6b25a6ecb75dcb4882c5dfa9a4b4 Mon Sep 17 00:00:00 2001 From: jholdstock Date: Sat, 30 May 2026 10:05:20 +0800 Subject: [PATCH 4/5] Update decred deps. --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 5f914e75..76bf0eb2 100644 --- a/go.mod +++ b/go.mod @@ -3,18 +3,18 @@ module github.com/decred/vspd go 1.25.0 require ( - decred.org/dcrwallet/v5 v5.0.1 + decred.org/dcrwallet/v5 v5.1.0 github.com/decred/dcrd/blockchain/stake/v5 v5.0.2 - github.com/decred/dcrd/blockchain/standalone/v2 v2.2.2 + github.com/decred/dcrd/blockchain/standalone/v2 v2.3.0 github.com/decred/dcrd/chaincfg/chainhash v1.0.5 github.com/decred/dcrd/chaincfg/v3 v3.3.0 - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 github.com/decred/dcrd/dcrutil/v4 v4.0.3 github.com/decred/dcrd/gcs/v4 v4.1.1 github.com/decred/dcrd/hdkeychain/v3 v3.1.3 github.com/decred/dcrd/rpc/jsonrpc/types/v4 v4.4.0 github.com/decred/dcrd/txscript/v4 v4.1.2 - github.com/decred/dcrd/wire v1.7.1 + github.com/decred/dcrd/wire v1.7.5 github.com/decred/slog v1.2.0 github.com/decred/vspd/client/v4 v4.0.2 github.com/decred/vspd/types/v3 v3.0.0 diff --git a/go.sum b/go.sum index b7be2250..905c4d21 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -decred.org/dcrwallet/v5 v5.0.1 h1:0n4HyMyjdsxiwDMyPx4ANRqadgVaNoiyqMU7T/pe7fs= -decred.org/dcrwallet/v5 v5.0.1/go.mod h1:SIFVCQX7qSGccZRG7sbm4NyA49WzXIXuXFll/NItSNo= +decred.org/dcrwallet/v5 v5.1.0 h1:bgd2D/X6OY/+oJoX6bE4rKAV/1JxKd8sETfdAMduO5c= +decred.org/dcrwallet/v5 v5.1.0/go.mod h1:ey7FaK4F6dmyHa3gmOC1XUShtI5jr5/zsYLdqyyVThc= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= @@ -17,8 +17,8 @@ github.com/decred/base58 v1.0.6 h1:NXndBcO+ubGZORV3EulvqeBcMuQM7doqVGa7pBhMOs4= github.com/decred/base58 v1.0.6/go.mod h1:KR7Oh9njDPXTagD4P67KJZwroL8jT653u8CffkYqhcQ= github.com/decred/dcrd/blockchain/stake/v5 v5.0.2 h1:5SZwlt0oJRuj64MqSFyQItSvfnd6hEgkkx4LySK9Zxo= github.com/decred/dcrd/blockchain/stake/v5 v5.0.2/go.mod h1:h6Xw2ECYewEb27i8yVsjOKktVIIu0TTS6EKhqfxfnbA= -github.com/decred/dcrd/blockchain/standalone/v2 v2.2.2 h1:mQA1cEzMS6EHbgV50wb2VFx4VQjNE8+n/B5BKvp5HWk= -github.com/decred/dcrd/blockchain/standalone/v2 v2.2.2/go.mod h1:lkBXrIA42dLYG4nDKAf08RlAeO4c4FC5YeDh8if91Hc= +github.com/decred/dcrd/blockchain/standalone/v2 v2.3.0 h1:AbHhFQE0paPmhRumRkoeRUTrWh0Gb0409exls+yU3VM= +github.com/decred/dcrd/blockchain/standalone/v2 v2.3.0/go.mod h1:9r5PlToh/WNrP3UJ4mG7Hq1U2WQ6I0uu9qMCpsRw22s= github.com/decred/dcrd/chaincfg/chainhash v1.0.5 h1:GwzXLsZoemdDxFdtj3GdW25Z+NXdN6nD3OjVtA+UwiE= github.com/decred/dcrd/chaincfg/chainhash v1.0.5/go.mod h1:vCqZMGtKbyxJkdcVzP3Ryc9IvaspVUb7hO6t+rjxmcs= github.com/decred/dcrd/chaincfg/v3 v3.3.0 h1:Plu5nS4ctf2V+8PMyannHRtcgexjY1EnFLTt3zfgGI8= @@ -35,8 +35,8 @@ github.com/decred/dcrd/dcrec v1.0.1 h1:gDzlndw0zYxM5BlaV17d7ZJV6vhRe9njPBFeg4Db2 github.com/decred/dcrd/dcrec v1.0.1/go.mod h1:CO+EJd8eHFb8WHa84C7ZBkXsNUIywaTHb+UAuI5uo6o= github.com/decred/dcrd/dcrec/edwards/v2 v2.0.4 h1:xmmdtnGxF/Od2doiP56zBv5a3LgJ3PA6mlx3Luf622I= github.com/decred/dcrd/dcrec/edwards/v2 v2.0.4/go.mod h1:07Ke2V+uJkG72M1Eiek8CF6NUB3XPlZ38cIit57R0UU= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/decred/dcrd/dcrjson/v4 v4.2.0 h1:VZOitxS5/J1Gr8V/rUcVnvbtTvijxrVy7i2A3nu9wto= github.com/decred/dcrd/dcrjson/v4 v4.2.0/go.mod h1:1YuURV3cVmko3lmBlkKc6Y2iHwHJJXATdiLULBS9D+Q= github.com/decred/dcrd/dcrutil/v4 v4.0.3 h1:uUgSBB4ZFHeKQFrdUgKv3PvVJ3YpBpFeXMgXZsa0790= @@ -49,8 +49,8 @@ github.com/decred/dcrd/rpc/jsonrpc/types/v4 v4.4.0 h1:BBVaYemabsFsaqNVlCmacoZlSs github.com/decred/dcrd/rpc/jsonrpc/types/v4 v4.4.0/go.mod h1:w4C6hZ7ywpc8/YNkiPAknCaqKofF68cRhUiTglEIc7s= github.com/decred/dcrd/txscript/v4 v4.1.2 h1:1EP7ZmBDl2LBeAMTEygxY8rVNN3+lkGqrsb4u64x+II= github.com/decred/dcrd/txscript/v4 v4.1.2/go.mod h1:r5/8qfCnl6TFrE369gggUayVIryM1oC7BLoRfa27Ckw= -github.com/decred/dcrd/wire v1.7.1 h1:kDuHBiY1Qv9rBxYKgC2RgyPy7IOA2WRf00jqHwpr16I= -github.com/decred/dcrd/wire v1.7.1/go.mod h1:eP9XRsMloy+phlntkTAaAm611JgLv8NqY1YJoRxkNKU= +github.com/decred/dcrd/wire v1.7.5 h1:fRaaB5CrwYWGI3YVv50XHm54lsU1TB40WnnIJ4W6aGM= +github.com/decred/dcrd/wire v1.7.5/go.mod h1:NZK8QD5W2ObX6p+Q0TUzYNpQtk4Ov3pBIvc6ZUK88FU= github.com/decred/slog v1.2.0 h1:soHAxV52B54Di3WtKLfPum9OFfWqwtf/ygf9njdfnPM= github.com/decred/slog v1.2.0/go.mod h1:kVXlGnt6DHy2fV5OjSeuvCJ0OmlmTF6LFpEPMu/fOY0= github.com/decred/vspd/client/v4 v4.0.2 h1:yvQaJFy3UdQMcRzjAyvuKPpr2G2Rn6Bbha+HH4TwItQ= From bfac71fa8863dc7cb776b0e8dad3e0ea98f1fdd4 Mon Sep 17 00:00:00 2001 From: jholdstock Date: Sat, 30 May 2026 13:23:21 +0800 Subject: [PATCH 5/5] Add CSRF middleware to web server. CSRF was not previously implemented in vspd because it would have required a bit of bodging: - Imported from gorilla/csrf with some kind of adapter to make it work for gin. - Imported from a gin specific package (eg. utrack/gin-csrf) which would pull in a bunch of new dependencies. - Implemented from scratch. None of the actions in the vspd API were sensitive enough to be worth this extra cruft, however this PR uses the implementation of CSRF protection which was added to stdlib in go 1.25, meaning it is free of the above compromises. --- internal/webapi/middleware.go | 20 ++++++++++++++++++++ internal/webapi/webapi.go | 2 ++ 2 files changed, 22 insertions(+) diff --git a/internal/webapi/middleware.go b/internal/webapi/middleware.go index 6a4915e8..f0b3957a 100644 --- a/internal/webapi/middleware.go +++ b/internal/webapi/middleware.go @@ -111,6 +111,26 @@ func (w *WebAPI) requireWebCache(c *gin.Context) { c.Set(cacheKey, w.cache.getData()) } +// csrf provides protection against Cross-Site Request Forgery (CSRF) attacks. +func (w *WebAPI) csrf() gin.HandlerFunc { + // The protection offered by http.CrossOriginProtection is not as robust as + // traditional token based solutions, however it is good enough for the vspd + // website which has no particularly sensitive form actions. + // + // Alex Edwards's blog has a good overview of the limitations: + // https://www.alexedwards.net/blog/preventing-csrf-in-go + csrf := http.NewCrossOriginProtection() + return func(c *gin.Context) { + err := csrf.Check(c.Request) + if err != nil { + w.log.Warnf("CSRF failure (clientIP=%s): %v", c.ClientIP(), err) + c.String(http.StatusBadRequest, "CSRF failure") + c.Abort() + return + } + } +} + // 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 (w *WebAPI) requireAdmin(c *gin.Context) { diff --git a/internal/webapi/webapi.go b/internal/webapi/webapi.go index 4856f2ac..5817bda8 100644 --- a/internal/webapi/webapi.go +++ b/internal/webapi/webapi.go @@ -232,6 +232,8 @@ func (w *WebAPI) router(cookieSecret []byte, dcrd rpc.DcrdConnect, wallets rpc.W router.Use(gin.Logger()) } + router.Use(w.csrf()) + // Serve static web resources router.Static("/public", "internal/webapi/public/")