diff --git a/config.go b/config.go index cda45310..190e8fdd 100644 --- a/config.go +++ b/config.go @@ -52,6 +52,7 @@ type config struct { SupportEmail string `long:"supportemail" ini-name:"supportemail" description:"Email address for users in need of support."` BackupInterval time.Duration `long:"backupinterval" ini-name:"backupinterval" description:"Time period between automatic database backups. Valid time units are {s,m,h}. Minimum 30 seconds."` VspClosed bool `long:"vspclosed" ini-name:"vspclosed" description:"Closed prevents the VSP from accepting new tickets."` + AdminPass string `long:"adminpass" ini-name:"adminpass" description:"Password for accessing admin page."` // The following flags should be set on CLI only, not via config file. FeeXPub string `long:"feexpub" no-ini:"true" description:"Cold wallet xpub used for collecting fees. Should be provided once to initialize a vspd database."` @@ -271,6 +272,11 @@ func loadConfig() (*config, error) { return nil, errors.New("the supportemail option is not set") } + // Ensure the administrator password is set. + if cfg.AdminPass == "" { + return nil, errors.New("the adminpass option is not set") + } + // Ensure the dcrd RPC username is set. if cfg.DcrdUser == "" { return nil, errors.New("the dcrduser option is not set") diff --git a/database/database.go b/database/database.go index 77e103d4..9e3913e8 100644 --- a/database/database.go +++ b/database/database.go @@ -30,6 +30,8 @@ var ( versionK = []byte("version") // feeXPub is the extended public key used for collecting VSP fees. feeXPubK = []byte("feeXPub") + // cookieSecret is the secret key for initializing the cookie store. + cookieSecretK = []byte("cookieSecret") // privatekey is the private key. privateKeyK = []byte("privatekey") // lastaddressindex is the index of the last address used for fees. @@ -103,6 +105,18 @@ func CreateNew(dbFile, feeXPub string) error { return err } + // Generate a secret key for initializing the cookie store. + log.Info("Generating cookie secret") + secret := make([]byte, 32) + _, err = rand.Read(secret) + if err != nil { + return err + } + err = vspBkt.Put(cookieSecretK, secret) + if err != nil { + return err + } + log.Info("Storing extended public key") // Store fee xpub err = vspBkt.Put(feeXPubK, []byte(feeXPub)) @@ -228,3 +242,16 @@ func (vdb *VspDatabase) GetFeeXPub() (string, error) { return feeXPub, err } + +func (vdb *VspDatabase) GetCookieSecret() ([]byte, error) { + var cookieSecret []byte + err := vdb.db.View(func(tx *bolt.Tx) error { + vspBkt := tx.Bucket(vspBktK) + + cookieSecret = vspBkt.Get(cookieSecretK) + + return nil + }) + + return cookieSecret, err +} diff --git a/database/database_test.go b/database/database_test.go index eeabf235..367d8d22 100644 --- a/database/database_test.go +++ b/database/database_test.go @@ -56,4 +56,4 @@ func TestDatabase(t *testing.T) { } // TODO: Add tests for CountTickets, GetUnconfirmedTickets, GetPendingFees, -// GetUnconfirmedFees. +// GetUnconfirmedFees, GetAllTickets. diff --git a/database/ticket.go b/database/ticket.go index 985d9e6d..9307bdbb 100644 --- a/database/ticket.go +++ b/database/ticket.go @@ -227,3 +227,24 @@ func (vdb *VspDatabase) GetUnconfirmedFees() ([]Ticket, error) { return tickets, err } + +func (vdb *VspDatabase) GetAllTickets() ([]Ticket, error) { + var tickets []Ticket + err := vdb.db.View(func(tx *bolt.Tx) error { + ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK) + + return ticketBkt.ForEach(func(k, v []byte) error { + var ticket Ticket + err := json.Unmarshal(v, &ticket) + if err != nil { + return fmt.Errorf("could not unmarshal ticket: %v", err) + } + + tickets = append(tickets, ticket) + + return nil + }) + }) + + return tickets, err +} diff --git a/go.mod b/go.mod index 58e0b45e..d40c5d15 100644 --- a/go.mod +++ b/go.mod @@ -4,16 +4,17 @@ go 1.13 require ( decred.org/dcrwallet v1.2.3-0.20200519180100-f1aa4c354e05 - github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200528191943-a2771b8ce693 - github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200528191943-a2771b8ce693 + github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200607041702-62fa0661bd57 + github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200607041702-62fa0661bd57 github.com/decred/dcrd/dcrec v1.0.0 - github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200528191943-a2771b8ce693 - github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200528191943-a2771b8ce693 + github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200607041702-62fa0661bd57 + github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200607041702-62fa0661bd57 github.com/decred/dcrd/rpc/jsonrpc/types/v2 v2.0.1-0.20200527025017-6fc98347d984 - github.com/decred/dcrd/txscript/v3 v3.0.0-20200528191943-a2771b8ce693 + github.com/decred/dcrd/txscript/v3 v3.0.0-20200607041702-62fa0661bd57 github.com/decred/dcrd/wire v1.3.0 github.com/decred/slog v1.0.0 github.com/gin-gonic/gin v1.6.3 + github.com/gorilla/sessions v1.2.0 github.com/jessevdk/go-flags v1.4.0 github.com/jrick/bitset v1.0.0 github.com/jrick/logrotate v1.0.0 diff --git a/go.sum b/go.sum index cf2e1a7f..8d4dd28a 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,8 @@ github.com/decred/dcrd/addrmgr v1.1.0/go.mod h1:exghL+0+QeVvO4MXezWJ1C2tcpBn3ngf github.com/decred/dcrd/blockchain/stake/v2 v2.0.2/go.mod h1:o2TT/l/YFdrt15waUdlZ3g90zfSwlA0WgQqHV9UGJF4= github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:aDL94kcVJfaaJP+acWUJrlK7g7xEOqTSiFe6bSN3yRQ= github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:4zE60yDWlfCDtmqnyP5o1k1K0oyhNn3Tvqo6F93/+RU= -github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200528191943-a2771b8ce693 h1:2Wbj27h04yq4l8KSqCJkZQnmWiKfUI4kNYaUms5oSnY= -github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200528191943-a2771b8ce693/go.mod h1:4zE60yDWlfCDtmqnyP5o1k1K0oyhNn3Tvqo6F93/+RU= +github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200607041702-62fa0661bd57 h1:4/glIgVrylnAAYlpfgBmxg8dX9DHWJu/tlBagcNiXz4= +github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200607041702-62fa0661bd57/go.mod h1:4zE60yDWlfCDtmqnyP5o1k1K0oyhNn3Tvqo6F93/+RU= github.com/decred/dcrd/blockchain/standalone v1.1.0 h1:yclvVGEY09Gf8A4GSAo+NCtL1dW2TYJ4OKp4+g0ICI0= github.com/decred/dcrd/blockchain/standalone v1.1.0/go.mod h1:6K8ZgzlWM1Kz2TwXbrtiAvfvIwfAmlzrtpA7CVPCUPE= github.com/decred/dcrd/blockchain/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:R9rIXU8kEJVC9Z4LAlh9bo9hiT3a+ihys3mCrz4PVao= @@ -34,8 +34,8 @@ github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200215015031-3283587e6add/go.mod h1: github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200215023918-6247af01d5e3/go.mod h1:v4oyBPQ/ZstYCV7+B0y6HogFByW76xTjr+72fOm66Y8= github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:v4oyBPQ/ZstYCV7+B0y6HogFByW76xTjr+72fOm66Y8= github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:v4oyBPQ/ZstYCV7+B0y6HogFByW76xTjr+72fOm66Y8= -github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200528191943-a2771b8ce693 h1:zTb6LJaZpYAWItJbo/XY8howiwCceEfWzHZbI06PRNI= -github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200528191943-a2771b8ce693/go.mod h1:OHbKBa6UZZOXCU1Y8f9Ta3O+GShto7nB1O0nuEutKq4= +github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200607041702-62fa0661bd57 h1:agpmzwuPv/iaePlk/2MxXPU6Zz14qy2LY1kWmRCBdAQ= +github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200607041702-62fa0661bd57/go.mod h1:OHbKBa6UZZOXCU1Y8f9Ta3O+GShto7nB1O0nuEutKq4= github.com/decred/dcrd/connmgr/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:mvIMJsrOEngogmVrq+tdbPIZchHVgGnVBZeNwj1cW6E= github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= @@ -61,14 +61,14 @@ github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200215015031-3283587e6add/go.mod h1:C github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200215023918-6247af01d5e3/go.mod h1:48ZLpNNrRIYfqYxmvzMgOZrnTZUU3aTJveWtamCkOxo= github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:jFxEd2LWDLvrWlrIiyx9ZGTQjvoFHZ0OVfBdyIX7jSw= github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:/CDBC1SOXKrmihavgXviaTr6eVZSAWKQqEbRmacDxgg= -github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200528191943-a2771b8ce693 h1:6Sz3pijZe2cyY8y1hhTmeQDz6Bg+KheIA5Hkks1n8QY= -github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200528191943-a2771b8ce693/go.mod h1:85NtF/fmqL2UDf0/gLhTHG/m/0HQHwG+erQKkwWW27A= +github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200607041702-62fa0661bd57 h1://D7EuW5XY8Me8uOHH2PH9APsdUQ0cm3KIyXXIcMYGc= +github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200607041702-62fa0661bd57/go.mod h1:85NtF/fmqL2UDf0/gLhTHG/m/0HQHwG+erQKkwWW27A= github.com/decred/dcrd/gcs/v2 v2.0.0/go.mod h1:3XjKcrtvB+r2ezhIsyNCLk6dRnXRJVyYmsd1P3SkU3o= github.com/decred/dcrd/gcs/v2 v2.0.2-0.20200312171759-0a8cc56a776e h1:tBOk2P8F9JyRUSp0iRTs4nYEBro1FKBDIbg/UualLWw= github.com/decred/dcrd/gcs/v2 v2.0.2-0.20200312171759-0a8cc56a776e/go.mod h1:JJGd1m0DrFgV4J2J8HKNB9YVkM06ewQHT6iINis39Z4= github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200421213827-b60c60ffe98b/go.mod h1:qKN0WzeSEEZ4fUBsTwKzOPkLP7GqSM6jBUm5Auq9mrM= -github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200528191943-a2771b8ce693 h1:8MKtyq4+zxRC/SEtJuF4+qB+Svd5vvHZeG9kbHxg9wY= -github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200528191943-a2771b8ce693/go.mod h1:qKN0WzeSEEZ4fUBsTwKzOPkLP7GqSM6jBUm5Auq9mrM= +github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200607041702-62fa0661bd57 h1:fGahtE/RfIBYlguw7tG11g1RBMBqqaZKVWFYlccYbX4= +github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200607041702-62fa0661bd57/go.mod h1:qKN0WzeSEEZ4fUBsTwKzOPkLP7GqSM6jBUm5Auq9mrM= github.com/decred/dcrd/rpc/jsonrpc/types/v2 v2.0.0/go.mod h1:c5S+PtQWNIA2aUakgrLhrlopkMadcOv51dWhCEdo49c= github.com/decred/dcrd/rpc/jsonrpc/types/v2 v2.0.1-0.20200527025017-6fc98347d984 h1:xfdiilBsDinOLbglqzHH98fOO1iBtVrpSHMVpNK/2lg= github.com/decred/dcrd/rpc/jsonrpc/types/v2 v2.0.1-0.20200527025017-6fc98347d984/go.mod h1:c5S+PtQWNIA2aUakgrLhrlopkMadcOv51dWhCEdo49c= @@ -76,8 +76,8 @@ github.com/decred/dcrd/txscript/v2 v2.1.0/go.mod h1:XaJAVrZU4NWRx4UEzTiDAs86op1m github.com/decred/dcrd/txscript/v3 v3.0.0-20200215023918-6247af01d5e3/go.mod h1:ATMA8K0SOo+M9Wdbr6dMnAd8qICJi6pXjGLlKsJc99E= github.com/decred/dcrd/txscript/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:KsDS7McU1yFaCYR9LCIwk6YnE15YN3wJUDxhKdFqlsc= github.com/decred/dcrd/txscript/v3 v3.0.0-20200421213827-b60c60ffe98b/go.mod h1:vrm3R/AesmA9slTf0rFcwhD0SduAJAWxocyaWVi8dM0= -github.com/decred/dcrd/txscript/v3 v3.0.0-20200528191943-a2771b8ce693 h1:8QJvuZnGXKs1x2VftugOOApMyARYH4tnu4n4o+JASZo= -github.com/decred/dcrd/txscript/v3 v3.0.0-20200528191943-a2771b8ce693/go.mod h1:vrm3R/AesmA9slTf0rFcwhD0SduAJAWxocyaWVi8dM0= +github.com/decred/dcrd/txscript/v3 v3.0.0-20200607041702-62fa0661bd57 h1:UaIGTrn1bpdZVgULdkwxZhULw5T5Jm3+OPJDw4+/p5c= +github.com/decred/dcrd/txscript/v3 v3.0.0-20200607041702-62fa0661bd57/go.mod h1:vrm3R/AesmA9slTf0rFcwhD0SduAJAWxocyaWVi8dM0= github.com/decred/dcrd/wire v1.3.0 h1:X76I2/a8esUmxXmFpJpAvXEi014IA4twgwcOBeIS8lE= github.com/decred/dcrd/wire v1.3.0/go.mod h1:fnKGlUY2IBuqnpxx5dYRU5Oiq392OBqAuVjRVSkIoXM= github.com/decred/go-socks v1.1.0/go.mod h1:sDhHqkZH0X4JjSa02oYOGhcGHYp12FsY1jQ/meV8md0= @@ -105,6 +105,10 @@ github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ= +github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= diff --git a/harness.sh b/harness.sh index a4743dbf..b2f478df 100755 --- a/harness.sh +++ b/harness.sh @@ -125,6 +125,7 @@ webserverdebug = false supportemail = example@test.com backupinterval = 3m0s vspclosed = false +adminpass=12345 EOF tmux new-window -t $TMUX_SESSION -n "vspd" diff --git a/main.go b/main.go index 849c580c..d6d84c9a 100644 --- a/main.go +++ b/main.go @@ -88,6 +88,7 @@ func run(ctx context.Context) error { NetParams: cfg.netParams.Params, SupportEmail: cfg.SupportEmail, VspClosed: cfg.VspClosed, + AdminPass: cfg.AdminPass, } err = webapi.Start(ctx, shutdownRequestChannel, &shutdownWg, cfg.Listen, db, dcrd, wallets, cfg.WebServerDebug, apiCfg) diff --git a/webapi/admin.go b/webapi/admin.go new file mode 100644 index 00000000..056f4859 --- /dev/null +++ b/webapi/admin.go @@ -0,0 +1,69 @@ +package webapi + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/gorilla/sessions" +) + +// adminPage is the handler for "GET /admin". The admin template will be +// rendered if the current session is authenticated as an admin, otherwise the +// login template will be rendered. +func adminPage(c *gin.Context) { + session := c.MustGet("session").(*sessions.Session) + admin := session.Values["admin"] + + if admin == nil { + c.HTML(http.StatusUnauthorized, "login.html", gin.H{}) + return + } + + tickets, err := db.GetAllTickets() + if err != nil { + log.Errorf("GetAllTickets error: %v", err) + c.String(http.StatusInternalServerError, "Error getting tickets from db") + return + } + + c.HTML(http.StatusOK, "admin.html", gin.H{ + "Tickets": tickets, + }) +} + +// adminLogin is the handler for "POST /admin". If a valid password is provided, +// the current session will be authenticated as an admin. +func adminLogin(c *gin.Context) { + password := c.PostForm("password") + + if password != cfg.AdminPass { + log.Warnf("Failed login attempt from %s", c.ClientIP()) + c.HTML(http.StatusUnauthorized, "login.html", gin.H{ + "IncorrectPassword": true, + }) + return + } + + setAdminStatus(true, c) +} + +// adminLogout is the handler for "POST /admin/logout". The current session will +// have its admin authentication removed. +func adminLogout(c *gin.Context) { + setAdminStatus(nil, c) +} + +// setAdminStatus stores the authentication status of the current session. +func setAdminStatus(admin interface{}, c *gin.Context) { + session := c.MustGet("session").(*sessions.Session) + session.Values["admin"] = admin + err := session.Save(c.Request, c.Writer) + if err != nil { + log.Errorf("Error saving session: %v", err) + c.String(http.StatusInternalServerError, "Error saving session") + return + } + + c.Redirect(http.StatusFound, "/admin") + c.Abort() +} diff --git a/webapi/middleware.go b/webapi/middleware.go index 7b765514..65647998 100644 --- a/webapi/middleware.go +++ b/webapi/middleware.go @@ -2,16 +2,51 @@ package webapi import ( "net/http" + "strings" "github.com/decred/vspd/rpc" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" + "github.com/gorilla/sessions" ) type ticketHashRequest struct { 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. +func withSession(store *sessions.CookieStore) gin.HandlerFunc { + return func(c *gin.Context) { + session, err := store.Get(c.Request, "vspd-session") + if err != nil { + // "value is not valid" occurs if the cookie secret changes. This is + // common during development (eg. when using the test harness) but + // it should not occur in production. + if strings.Contains(err.Error(), "securecookie: the value is not valid") { + log.Warn("Cookie secret has changed. Generating new session.") + + // Persist the newly generated session. + err = store.Save(c.Request, c.Writer, session) + if err != nil { + log.Errorf("Error saving session: %v", err) + c.String(http.StatusInternalServerError, "Error saving session") + c.Abort() + return + } + } else { + log.Errorf("Session error: %v", err) + c.String(http.StatusInternalServerError, "Error getting session") + c.Abort() + return + } + } + + c.Set("session", session) + } +} + // withDcrdClient middleware adds a dcrd client to the request // context for downstream handlers to make use of. func withDcrdClient() gin.HandlerFunc { diff --git a/webapi/templates/admin.html b/webapi/templates/admin.html new file mode 100644 index 00000000..415aeb15 --- /dev/null +++ b/webapi/templates/admin.html @@ -0,0 +1,41 @@ +{{ template "header" . }} + +
+ +Hash | +CommitmentAddress | +FeeAddressIndex | +FeeAddress | +FeeAmount | +FeeExpiration | +Confirmed | +VoteChoices | +VotingWIF | +FeeTxHex | +FeeTxHash | +FeeConfirmed | +
{{ printf "%.10s" .Hash }}... | +{{ printf "%.10s" .CommitmentAddress }}... | +{{ printf "%d" .FeeAddressIndex }} | +{{ printf "%.10s" .FeeAddress }}... | +{{ printf "%f" .FeeAmount }} | +{{ printf "%d" .FeeExpiration }} | +{{ printf "%t" .Confirmed }} | +{{ printf "%.10s" .VoteChoices }}... | +{{ printf "%.10s" .VotingWIF }}... | +{{ printf "%.10s" .FeeTxHex }}... | +{{ printf "%.10s" .FeeTxHash }}... | +{{ printf "%t" .FeeConfirmed }} | +