From 7b3b46c0833aef9d800ffb6a3c4dc91d5f5ccedd Mon Sep 17 00:00:00 2001 From: cody Date: Mon, 5 Aug 2024 17:01:02 +0800 Subject: [PATCH 01/10] feat: add dashboard login --- core/commands/dashboard.go | 133 +++++++++++++++++++++++++++++++++++++ core/commands/root.go | 3 +- core/corehttp/corehttp.go | 34 ++++++++++ utils/token.go | 95 ++++++++++++++++++++++++++ 4 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 core/commands/dashboard.go create mode 100644 utils/token.go diff --git a/core/commands/dashboard.go b/core/commands/dashboard.go new file mode 100644 index 000000000..6b245ec74 --- /dev/null +++ b/core/commands/dashboard.go @@ -0,0 +1,133 @@ +package commands + +import ( + "bytes" + "errors" + "fmt" + cmds "github.com/bittorrent/go-btfs-cmds" + "github.com/bittorrent/go-btfs/core/commands/cmdenv" + "github.com/bittorrent/go-btfs/utils" + ds "github.com/ipfs/go-datastore" +) + +const DashboardPasswordPrefix = "/dashboard_password" + +const TokenOption = "token" + +var dashboardCmd = &cmds.Command{ + Helptext: cmds.HelpText{ + Tagline: "dashboard password operation", + }, + + Subcommands: map[string]*cmds.Command{ + "check": checkCmd, + "set": setCmd, + "login": loginCmd, + "reset": resetCmd, + }, +} + +var checkCmd = &cmds.Command{ + Helptext: cmds.HelpText{ + Tagline: "check if password is set", + }, + Run: func(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) error { + node, err := cmdenv.GetNode(env) + if err != nil { + return err + } + + value, err := node.Repo.Datastore().Get(req.Context, ds.NewKey(DashboardPasswordPrefix)) + if err != nil { + log.Info("check password error", err) + return errors.New("password is not set") + } + fmt.Println("password..............", string(value)) + return re.Emit(bytes.NewReader([]byte("check"))) + }, +} + +var setCmd = &cmds.Command{ + Helptext: cmds.HelpText{ + Tagline: "set password", + }, + Arguments: []cmds.Argument{ + cmds.StringArg("password", true, false, "set password"), + }, + Run: func(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) error { + // 写入leveldb + node, err := cmdenv.GetNode(env) + if err != nil { + return err + } + datastore := node.Repo.Datastore() + key := ds.NewKey(DashboardPasswordPrefix) + err = datastore.Put(req.Context, key, []byte(req.Arguments[0])) + if err != nil { + return err + } + return re.Emit(bytes.NewReader([]byte("set"))) + }, +} + +var loginCmd = &cmds.Command{ + Helptext: cmds.HelpText{ + Tagline: "login password", + }, + Arguments: []cmds.Argument{ + cmds.StringArg("password", true, false, "set password"), + }, + Run: func(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) error { + node, err := cmdenv.GetNode(env) + if err != nil { + return err + } + value, err := node.Repo.Datastore().Get(req.Context, ds.NewKey(DashboardPasswordPrefix)) + if err != nil { + return err + } + if string(value) != req.Arguments[0] { + return errors.New("password is not correct") + } + log.Info("login password is correct") + + config, err := node.Repo.Config() + + publicKey := config.Identity.PeerID + + token, err := utils.GenerateToken(publicKey, req.Arguments[0], 60*60) + if err != nil { + return err + } + + return re.Emit(bytes.NewReader([]byte(token))) + }, +} + +var resetCmd = &cmds.Command{ + Helptext: cmds.HelpText{ + Tagline: "reset password", + }, + Arguments: []cmds.Argument{ + cmds.StringArg("password", true, false, "reset password"), + }, + Run: func(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) error { + node, err := cmdenv.GetNode(env) + if err != nil { + return err + } + datastore := node.Repo.Datastore() + value, err := datastore.Get(req.Context, ds.NewKey(DashboardPasswordPrefix)) + if err != nil { + return err + } + if string(value) != req.Arguments[0] { + return errors.New("password is not correct") + } + err = datastore.Put(req.Context, ds.NewKey(DashboardPasswordPrefix), []byte("")) + if err != nil { + return err + } + return re.Emit(bytes.NewReader([]byte("reset"))) + }, +} diff --git a/core/commands/root.go b/core/commands/root.go index 0b41fb0ef..162b51993 100644 --- a/core/commands/root.go +++ b/core/commands/root.go @@ -175,7 +175,7 @@ var rootSubcommands = map[string]*cmds.Command{ "vault": vault.VaultCmd, "bttc": bttc.BttcCmd, "settlement": settlement.SettlementCmd, - //"update": ExternalBinary(), + // "update": ExternalBinary(), "network": NetworkCmd, "statuscontract": StatusContractCmd, "bittorrent": bittorrentCmd, @@ -185,6 +185,7 @@ var rootSubcommands = map[string]*cmds.Command{ "accesskey": AccessKeyCmd, "encrypt": encryptCmd, "decrypt": decryptCmd, + "passwd": dashboardCmd, } // RootRO is the readonly version of Root diff --git a/core/corehttp/corehttp.go b/core/corehttp/corehttp.go index 906974aa9..83f671e1f 100644 --- a/core/corehttp/corehttp.go +++ b/core/corehttp/corehttp.go @@ -7,6 +7,9 @@ package corehttp import ( "context" "fmt" + "github.com/bittorrent/go-btfs/core/commands" + "github.com/bittorrent/go-btfs/utils" + ds "github.com/ipfs/go-datastore" "net" "net/http" "time" @@ -51,6 +54,12 @@ func makeHandler(n *core.IpfsNode, l net.Listener, options ...ServeOption) (http w.WriteHeader(http.StatusOK) return } + + // err := interceptorBeforeReq(r, n) + // if err != nil { + // return + // } + topMux.ServeHTTP(w, r) }) return handler, nil @@ -139,3 +148,28 @@ func Serve(node *core.IpfsNode, lis net.Listener, options ...ServeOption) error log.Infof("server at %s terminated", addr) return serverError } + +func interceptorBeforeReq(r *http.Request, n *core.IpfsNode) error { + if r.URL.Path == APIPath+"/passwd/login" { + return nil + } + args := r.URL.Query() + token := args.Get("token") + password, err := n.Repo.Datastore().Get(r.Context(), ds.NewKey(commands.DashboardPasswordPrefix)) + if err != nil { + return err + } + claims, err := utils.VerifyToken(token, string(password)) + if err != nil { + return err + } + if claims.PeerId != n.Identity.String() { + return fmt.Errorf("token is invalid") + } + + return nil +} + +func filterUrl() { + +} diff --git a/utils/token.go b/utils/token.go new file mode 100644 index 000000000..570a90e14 --- /dev/null +++ b/utils/token.go @@ -0,0 +1,95 @@ +package utils + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "time" +) + +type Claims struct { + PeerId string + Expiry int64 +} + +func GenerateToken(peerId, secret string, expiryDuration time.Duration) (string, error) { + expiryTime := time.Now().Add(expiryDuration).Unix() + claims := Claims{ + PeerId: peerId, + Expiry: expiryTime, + } + + claimsBytes, err := json.Marshal(claims) + if err != nil { + return "", err + } + + header := base64.URLEncoding.EncodeToString([]byte(`{"alg":"HS256","typ":"JWT"}`)) + payload := base64.URLEncoding.EncodeToString(claimsBytes) + signature := computeHMACSHA256(header+"."+payload, []byte(secret)) + + token := header + "." + payload + "." + signature + return token, nil +} + +func VerifyToken(token string, secret string) (*Claims, error) { + parts := splitToken(token) + if len(parts) != 3 { + return nil, fmt.Errorf("invalid token format") + } + + header := parts[0] + payload := parts[1] + receivedSignature := parts[2] + + expectedSignature := computeHMACSHA256(header+"."+payload, []byte(secret)) + if receivedSignature != expectedSignature { + return nil, fmt.Errorf("invalid token signature") + } + + claimsBytes, err := base64.URLEncoding.DecodeString(payload) + if err != nil { + return nil, err + } + + var claims Claims + err = json.Unmarshal(claimsBytes, &claims) + if err != nil { + return nil, err + } + + if time.Now().Unix() > claims.Expiry { + return nil, fmt.Errorf("token has expired") + } + + return &claims, nil +} + +func computeHMACSHA256(message string, key []byte) string { + mac := hmac.New(sha256.New, key) + mac.Write([]byte(message)) + return base64.URLEncoding.EncodeToString(mac.Sum(nil)) +} + +func splitToken(token string) []string { + return splitString(token, '.') +} + +func splitString(s string, sep rune) []string { + var result []string + var buffer []rune + for _, char := range s { + if char == sep { + result = append(result, string(buffer)) + buffer = buffer[:0] + } else { + buffer = append(buffer, char) + } + } + if len(buffer) > 0 { + result = append(result, string(buffer)) + } + return result +} From 1d4ae637b3ad5453efd0032bd5a5f50487aa652a Mon Sep 17 00:00:00 2001 From: cody Date: Tue, 6 Aug 2024 17:34:28 +0800 Subject: [PATCH 02/10] feat: add subcommand for dashboard login --- core/commands/dashboard.go | 113 +++++++++++++++++++++++++++++++------ core/commands/root.go | 2 +- core/corehttp/corehttp.go | 18 +++++- 3 files changed, 112 insertions(+), 21 deletions(-) diff --git a/core/commands/dashboard.go b/core/commands/dashboard.go index 6b245ec74..21782f898 100644 --- a/core/commands/dashboard.go +++ b/core/commands/dashboard.go @@ -2,6 +2,7 @@ package commands import ( "bytes" + "encoding/hex" "errors" "fmt" cmds "github.com/bittorrent/go-btfs-cmds" @@ -11,8 +12,9 @@ import ( ) const DashboardPasswordPrefix = "/dashboard_password" +const TokenExpire = 60 * 60 * 24 * 1 -const TokenOption = "token" +var IsLogin = false var dashboardCmd = &cmds.Command{ Helptext: cmds.HelpText{ @@ -20,10 +22,12 @@ var dashboardCmd = &cmds.Command{ }, Subcommands: map[string]*cmds.Command{ - "check": checkCmd, - "set": setCmd, - "login": loginCmd, - "reset": resetCmd, + "check": checkCmd, + "set": setCmd, + "reset": resetCmd, + "change": changeCmd, + "login": loginCmd, + "logout": logoutCmd, }, } @@ -37,13 +41,17 @@ var checkCmd = &cmds.Command{ return err } - value, err := node.Repo.Datastore().Get(req.Context, ds.NewKey(DashboardPasswordPrefix)) + _, err = node.Repo.Datastore().Get(req.Context, ds.NewKey(DashboardPasswordPrefix)) + + if err != nil && errors.Is(err, ds.ErrNotFound) { + return re.Emit(bytes.NewReader([]byte("passwd is not set"))) + } + if err != nil { log.Info("check password error", err) - return errors.New("password is not set") + return fmt.Errorf("check passwd error: %v\n", err) } - fmt.Println("password..............", string(value)) - return re.Emit(bytes.NewReader([]byte("check"))) + return re.Emit(bytes.NewReader([]byte("password was set"))) }, } @@ -55,24 +63,33 @@ var setCmd = &cmds.Command{ cmds.StringArg("password", true, false, "set password"), }, Run: func(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) error { - // 写入leveldb node, err := cmdenv.GetNode(env) if err != nil { return err } + // check if password has set + _, err = node.Repo.Datastore().Get(req.Context, ds.NewKey(DashboardPasswordPrefix)) + if err != nil && !errors.Is(err, ds.ErrNotFound) { + return fmt.Errorf("set password error: %v\n", err) + } + + if err == nil { + return fmt.Errorf("password has set, if you want to reset your password, please use reset command instead") + } + datastore := node.Repo.Datastore() key := ds.NewKey(DashboardPasswordPrefix) err = datastore.Put(req.Context, key, []byte(req.Arguments[0])) if err != nil { return err } - return re.Emit(bytes.NewReader([]byte("set"))) + return re.Emit(bytes.NewReader([]byte("password set success!"))) }, } var loginCmd = &cmds.Command{ Helptext: cmds.HelpText{ - Tagline: "login password", + Tagline: "login with passwd and get the token", }, Arguments: []cmds.Argument{ cmds.StringArg("password", true, false, "set password"), @@ -83,23 +100,28 @@ var loginCmd = &cmds.Command{ return err } value, err := node.Repo.Datastore().Get(req.Context, ds.NewKey(DashboardPasswordPrefix)) + if errors.Is(err, ds.ErrNotFound) { + return errors.New("password has not set, please set passwd first") + } if err != nil { return err } if string(value) != req.Arguments[0] { + log.Info("login password is correct") return errors.New("password is not correct") } - log.Info("login password is correct") config, err := node.Repo.Config() publicKey := config.Identity.PeerID - token, err := utils.GenerateToken(publicKey, req.Arguments[0], 60*60) + token, err := utils.GenerateToken(publicKey, req.Arguments[0], TokenExpire) if err != nil { return err } + IsLogin = true + return re.Emit(bytes.NewReader([]byte(token))) }, } @@ -109,25 +131,82 @@ var resetCmd = &cmds.Command{ Tagline: "reset password", }, Arguments: []cmds.Argument{ - cmds.StringArg("password", true, false, "reset password"), + cmds.StringArg("privateKey", true, false, "reset password"), + cmds.StringArg("oldPassword", true, false, "reset password"), + cmds.StringArg("newPassword", true, false, "reset password"), }, + Run: func(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) error { node, err := cmdenv.GetNode(env) if err != nil { return err } + + raw, err := node.PrivateKey.Raw() + if err != nil { + return err + } + + if hex.EncodeToString(raw) != req.Arguments[0] { + return errors.New("private key is not correct") + } datastore := node.Repo.Datastore() + value, err := datastore.Get(req.Context, ds.NewKey(DashboardPasswordPrefix)) if err != nil { return err } - if string(value) != req.Arguments[0] { + if string(value) != req.Arguments[1] { return errors.New("password is not correct") } - err = datastore.Put(req.Context, ds.NewKey(DashboardPasswordPrefix), []byte("")) + err = datastore.Put(req.Context, ds.NewKey(DashboardPasswordPrefix), []byte(req.Arguments[2])) if err != nil { return err } return re.Emit(bytes.NewReader([]byte("reset"))) }, } + +var changeCmd = &cmds.Command{ + Helptext: cmds.HelpText{ + Tagline: "change password", + }, + Arguments: []cmds.Argument{ + cmds.StringArg("oldPassword", true, false, "change password"), + cmds.StringArg("newPassword", true, false, "change password"), + }, + Run: func(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) error { + // change password + node, err := cmdenv.GetNode(env) + if err != nil { + return err + } + datastore := node.Repo.Datastore() + value, err := datastore.Get(req.Context, ds.NewKey(DashboardPasswordPrefix)) + if err != nil { + return err + } + if string(value) != req.Arguments[0] { + return errors.New("password is not correct") + } + err = datastore.Put(req.Context, ds.NewKey(DashboardPasswordPrefix), []byte(req.Arguments[1])) + if err != nil { + return err + } + return re.Emit(bytes.NewReader([]byte("change password ok"))) + }, +} + +var logoutCmd = &cmds.Command{ + Helptext: cmds.HelpText{ + Tagline: "logout", + }, + Arguments: []cmds.Argument{ + cmds.StringArg("token", true, false, "logout"), + }, + Run: func(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) error { + // set token expire to 0 + IsLogin = false + return re.Emit(bytes.NewReader([]byte("logout success"))) + }, +} diff --git a/core/commands/root.go b/core/commands/root.go index 162b51993..da48d40ea 100644 --- a/core/commands/root.go +++ b/core/commands/root.go @@ -185,7 +185,7 @@ var rootSubcommands = map[string]*cmds.Command{ "accesskey": AccessKeyCmd, "encrypt": encryptCmd, "decrypt": decryptCmd, - "passwd": dashboardCmd, + "dashboard": dashboardCmd, } // RootRO is the readonly version of Root diff --git a/core/corehttp/corehttp.go b/core/corehttp/corehttp.go index 83f671e1f..a550f60f3 100644 --- a/core/corehttp/corehttp.go +++ b/core/corehttp/corehttp.go @@ -57,6 +57,8 @@ func makeHandler(n *core.IpfsNode, l net.Listener, options ...ServeOption) (http // err := interceptorBeforeReq(r, n) // if err != nil { + // w.WriteHeader(http.StatusOK) + // w.Write([]byte(err.Error())) // return // } @@ -150,9 +152,13 @@ func Serve(node *core.IpfsNode, lis net.Listener, options ...ServeOption) error } func interceptorBeforeReq(r *http.Request, n *core.IpfsNode) error { - if r.URL.Path == APIPath+"/passwd/login" { + if filterUrl()[r.URL.Path] { return nil } + + if !commands.IsLogin { + return fmt.Errorf("please login") + } args := r.URL.Query() token := args.Get("token") password, err := n.Repo.Datastore().Get(r.Context(), ds.NewKey(commands.DashboardPasswordPrefix)) @@ -170,6 +176,12 @@ func interceptorBeforeReq(r *http.Request, n *core.IpfsNode) error { return nil } -func filterUrl() { - +func filterUrl() map[string]bool { + return map[string]bool{ + "/dashboard": true, + "/hostui": true, + APIPath + "/dashboard/check": true, + APIPath + "/dashboard/login": true, + APIPath + "/dashboard/reset": true, + } } From 484ba1e50e0b052de8dc7b73a7e03e177e049af0 Mon Sep 17 00:00:00 2001 From: cody Date: Tue, 6 Aug 2024 17:42:24 +0800 Subject: [PATCH 03/10] feat: add dashboard login command test --- core/commands/commands_test.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/core/commands/commands_test.go b/core/commands/commands_test.go index ed804deb5..50f6b1deb 100644 --- a/core/commands/commands_test.go +++ b/core/commands/commands_test.go @@ -93,8 +93,8 @@ func TestCommands(t *testing.T) { "/config/replace", "/config/reset", "/config/show", - //"/config/profile", - //"/config/profile/apply", + // "/config/profile", + // "/config/profile/apply", "/config/storage-host-enable", "/config/sync-chain-info", "/config/sync-simple-mode", @@ -332,7 +332,7 @@ func TestCommands(t *testing.T) { "/statuscontract/lastinfo", "/statuscontract/config", "/statuscontract/report_online_server", - //"/statuscontract/report_status_contract", + // "/statuscontract/report_status_contract", "/statuscontract/daily_report_online_server", "/statuscontract/daily_report_list", "/statuscontract/daily_total", @@ -361,6 +361,12 @@ func TestCommands(t *testing.T) { "/cheque/fix_cheque_cashout", "/encrypt", "/decrypt", + "/dashboard/check", + "/dashboard/set", + "/dashboard/reset", + "/dashboard/login", + "/dashboard/logout", + "/dashboard/change", } cmdSet := make(map[string]struct{}) From 7dcc1149e1c996ff77290e9a823c828aa211022c Mon Sep 17 00:00:00 2001 From: cody Date: Mon, 12 Aug 2024 15:01:16 +0800 Subject: [PATCH 04/10] feat: add cros for token intercept --- core/corehttp/corehttp.go | 59 +++++------------ core/corehttp/corehttp_interceptor.go | 91 +++++++++++++++++++++++++++ go.mod | 2 +- go.sum | 2 + 4 files changed, 108 insertions(+), 46 deletions(-) create mode 100644 core/corehttp/corehttp_interceptor.go diff --git a/core/corehttp/corehttp.go b/core/corehttp/corehttp.go index a550f60f3..27401ee49 100644 --- a/core/corehttp/corehttp.go +++ b/core/corehttp/corehttp.go @@ -7,9 +7,6 @@ package corehttp import ( "context" "fmt" - "github.com/bittorrent/go-btfs/core/commands" - "github.com/bittorrent/go-btfs/utils" - ds "github.com/ipfs/go-datastore" "net" "net/http" "time" @@ -55,12 +52,19 @@ func makeHandler(n *core.IpfsNode, l net.Listener, options ...ServeOption) (http return } - // err := interceptorBeforeReq(r, n) - // if err != nil { - // w.WriteHeader(http.StatusOK) - // w.Write([]byte(err.Error())) - // return - // } + err := interceptorBeforeReq(r, n) + if err != nil { + // set allow origin + w.Header().Set("Access-Control-Allow-Origin", "*") + if r.Method == http.MethodOptions { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Headers", "X-Stream-Output, X-Chunked-Output, X-Content-Length") + w.WriteHeader(http.StatusOK) + return + } + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } topMux.ServeHTTP(w, r) }) @@ -149,39 +153,4 @@ func Serve(node *core.IpfsNode, lis net.Listener, options ...ServeOption) error log.Infof("server at %s terminated", addr) return serverError -} - -func interceptorBeforeReq(r *http.Request, n *core.IpfsNode) error { - if filterUrl()[r.URL.Path] { - return nil - } - - if !commands.IsLogin { - return fmt.Errorf("please login") - } - args := r.URL.Query() - token := args.Get("token") - password, err := n.Repo.Datastore().Get(r.Context(), ds.NewKey(commands.DashboardPasswordPrefix)) - if err != nil { - return err - } - claims, err := utils.VerifyToken(token, string(password)) - if err != nil { - return err - } - if claims.PeerId != n.Identity.String() { - return fmt.Errorf("token is invalid") - } - - return nil -} - -func filterUrl() map[string]bool { - return map[string]bool{ - "/dashboard": true, - "/hostui": true, - APIPath + "/dashboard/check": true, - APIPath + "/dashboard/login": true, - APIPath + "/dashboard/reset": true, - } -} +} \ No newline at end of file diff --git a/core/corehttp/corehttp_interceptor.go b/core/corehttp/corehttp_interceptor.go new file mode 100644 index 000000000..95890929d --- /dev/null +++ b/core/corehttp/corehttp_interceptor.go @@ -0,0 +1,91 @@ +package corehttp + +import ( + "fmt" + "github.com/bittorrent/go-btfs/core" + "github.com/bittorrent/go-btfs/core/commands" + "github.com/bittorrent/go-btfs/utils" + ds "github.com/ipfs/go-datastore" + "net/http" +) + +func interceptorBeforeReq(r *http.Request, n *core.IpfsNode) error { + config, err := n.Repo.Config() + if err != nil { + return err + } + + if config.API.EnableTokenAuth { + err := tokenCheckInterceptor(r, n) + if err != nil { + return err + } + } + + return nil +} + +func tokenCheckInterceptor(r *http.Request, n *core.IpfsNode) error { + if filterNoNeedTokenCheckReq(r) { + return nil + } + if !commands.IsLogin { + return fmt.Errorf("please login") + } + args := r.URL.Query() + token := args.Get("token") + password, err := n.Repo.Datastore().Get(r.Context(), ds.NewKey(commands.DashboardPasswordPrefix)) + if err != nil { + return err + } + claims, err := utils.VerifyToken(token, string(password)) + if err != nil { + return err + } + if claims.PeerId != n.Identity.String() { + return fmt.Errorf("token is invalid") + } + + return nil +} + +func filterNoNeedTokenCheckReq(r *http.Request) bool { + if filterUrl(r) || filterP2pSchema(r) || filterLocalShellApi(r) { + return true + } + return false +} + +func filterUrl(r *http.Request) bool { + urls := map[string]bool{ + // local + "/dashboard": true, + "/hostui": true, + // no need url + APIPath + "/id": true, + APIPath + "/dashboard/check": true, + APIPath + "/dashboard/login": true, + APIPath + "/dashboard/reset": true, + } + + return urls[r.URL.Path] +} + +const defaultUserAgent = "Go-http-client/1.1" + +func filterLocalShellApi(r *http.Request) bool { + host := r.Host + ua := r.Header.Get("User-Agent") + // ua is not Go-http-client + if host == "127.0.0.1:5001" && ua == defaultUserAgent { + return true + } + return false +} + +func filterP2pSchema(r *http.Request) bool { + if r.URL.Scheme == "libp2p" { + return true + } + return false +} \ No newline at end of file diff --git a/go.mod b/go.mod index ee21874d6..c306160df 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/bittorrent/go-btfs-chunker v0.4.0 github.com/bittorrent/go-btfs-cmds v0.3.0 github.com/bittorrent/go-btfs-common v0.9.0 - github.com/bittorrent/go-btfs-config v0.13.2 + github.com/bittorrent/go-btfs-config v0.13.3-0.20240812024507-e1f8e9bdabe0 github.com/bittorrent/go-btfs-files v0.3.1 github.com/bittorrent/go-btns v0.2.0 github.com/bittorrent/go-common/v2 v2.4.0 diff --git a/go.sum b/go.sum index 683346ce1..fa48a012d 100644 --- a/go.sum +++ b/go.sum @@ -210,6 +210,8 @@ github.com/bittorrent/go-btfs-common v0.9.0 h1:jHcFvYQmvmA4IdvVtkI5d/S/HW65Qz21C github.com/bittorrent/go-btfs-common v0.9.0/go.mod h1:OG1n3DfcTxQYfLd5zco54LfL3IiDDaw3s7Igahu0Rj0= github.com/bittorrent/go-btfs-config v0.13.2 h1:wTS5kz3w4xRLfggubPq+uvveKA9WFBYFj8mCIba0oHQ= github.com/bittorrent/go-btfs-config v0.13.2/go.mod h1:DNaHVC9wU84KLKoC4HkvdoFJKVZ7TF530qzfYu30fCI= +github.com/bittorrent/go-btfs-config v0.13.3-0.20240812024507-e1f8e9bdabe0 h1:Maik8cP43hhU689rnCAb3aUZck99lFv4h3KBjVqRxt8= +github.com/bittorrent/go-btfs-config v0.13.3-0.20240812024507-e1f8e9bdabe0/go.mod h1:DNaHVC9wU84KLKoC4HkvdoFJKVZ7TF530qzfYu30fCI= github.com/bittorrent/go-btfs-files v0.3.0/go.mod h1:ylMf73m6oK94hL7VPblY1ZZpePsr6XbPV4BaNUwGZR0= github.com/bittorrent/go-btfs-files v0.3.1 h1:esq3j+6FtZ+SlaxKjVtiYgvXk/SWUiTcv0Q1MeJoPnQ= github.com/bittorrent/go-btfs-files v0.3.1/go.mod h1:ylMf73m6oK94hL7VPblY1ZZpePsr6XbPV4BaNUwGZR0= From 927441361a36d158f38eb31b8a4aabe5c0091e87 Mon Sep 17 00:00:00 2001 From: cody Date: Mon, 12 Aug 2024 16:36:25 +0800 Subject: [PATCH 05/10] feat: change dashboard api response --- core/commands/commands_test.go | 3 +- core/commands/dashboard.go | 52 ++++++++++++++++++++-------------- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/core/commands/commands_test.go b/core/commands/commands_test.go index 50f6b1deb..e0f6bf2df 100644 --- a/core/commands/commands_test.go +++ b/core/commands/commands_test.go @@ -361,6 +361,7 @@ func TestCommands(t *testing.T) { "/cheque/fix_cheque_cashout", "/encrypt", "/decrypt", + "dashboard", "/dashboard/check", "/dashboard/set", "/dashboard/reset", @@ -394,4 +395,4 @@ func TestCommands(t *testing.T) { t.Errorf("subcommand %q is nil even though there was no error", path) } } -} +} \ No newline at end of file diff --git a/core/commands/dashboard.go b/core/commands/dashboard.go index 21782f898..ca7bad4ef 100644 --- a/core/commands/dashboard.go +++ b/core/commands/dashboard.go @@ -1,10 +1,8 @@ package commands import ( - "bytes" "encoding/hex" "errors" - "fmt" cmds "github.com/bittorrent/go-btfs-cmds" "github.com/bittorrent/go-btfs/core/commands/cmdenv" "github.com/bittorrent/go-btfs/utils" @@ -16,6 +14,11 @@ const TokenExpire = 60 * 60 * 24 * 1 var IsLogin = false +type DashboardResponse struct { + Success bool + Text string +} + var dashboardCmd = &cmds.Command{ Helptext: cmds.HelpText{ Tagline: "dashboard password operation", @@ -44,14 +47,14 @@ var checkCmd = &cmds.Command{ _, err = node.Repo.Datastore().Get(req.Context, ds.NewKey(DashboardPasswordPrefix)) if err != nil && errors.Is(err, ds.ErrNotFound) { - return re.Emit(bytes.NewReader([]byte("passwd is not set"))) + return re.Emit(&DashboardResponse{Success: false, Text: "passwd is not set"}) } if err != nil { log.Info("check password error", err) - return fmt.Errorf("check passwd error: %v\n", err) + return err } - return re.Emit(bytes.NewReader([]byte("password was set"))) + return re.Emit(DashboardResponse{Success: true, Text: "password was set"}) }, } @@ -70,11 +73,15 @@ var setCmd = &cmds.Command{ // check if password has set _, err = node.Repo.Datastore().Get(req.Context, ds.NewKey(DashboardPasswordPrefix)) if err != nil && !errors.Is(err, ds.ErrNotFound) { - return fmt.Errorf("set password error: %v\n", err) + log.Info("set password error", err) + return err } if err == nil { - return fmt.Errorf("password has set, if you want to reset your password, please use reset command instead") + return re.Emit(&DashboardResponse{ + Success: false, + Text: "password has set, if you want to reset your password, please use reset command instead", + }) } datastore := node.Repo.Datastore() @@ -83,7 +90,7 @@ var setCmd = &cmds.Command{ if err != nil { return err } - return re.Emit(bytes.NewReader([]byte("password set success!"))) + return re.Emit(&DashboardResponse{Success: true, Text: "password set success!"}) }, } @@ -101,14 +108,14 @@ var loginCmd = &cmds.Command{ } value, err := node.Repo.Datastore().Get(req.Context, ds.NewKey(DashboardPasswordPrefix)) if errors.Is(err, ds.ErrNotFound) { - return errors.New("password has not set, please set passwd first") + return re.Emit(&DashboardResponse{Success: false, Text: "password has not set, please set passwd first"}) } if err != nil { return err } if string(value) != req.Arguments[0] { log.Info("login password is correct") - return errors.New("password is not correct") + return re.Emit(&DashboardResponse{Success: false, Text: "password is not correct"}) } config, err := node.Repo.Config() @@ -122,7 +129,10 @@ var loginCmd = &cmds.Command{ IsLogin = true - return re.Emit(bytes.NewReader([]byte(token))) + return re.Emit(&DashboardResponse{ + Success: true, + Text: token, + }) }, } @@ -131,9 +141,9 @@ var resetCmd = &cmds.Command{ Tagline: "reset password", }, Arguments: []cmds.Argument{ - cmds.StringArg("privateKey", true, false, "reset password"), - cmds.StringArg("oldPassword", true, false, "reset password"), - cmds.StringArg("newPassword", true, false, "reset password"), + cmds.StringArg("privateKey", true, false, "private key"), + cmds.StringArg("oldPassword", true, false, "old password"), + cmds.StringArg("newPassword", true, false, "new password"), }, Run: func(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) error { @@ -148,7 +158,7 @@ var resetCmd = &cmds.Command{ } if hex.EncodeToString(raw) != req.Arguments[0] { - return errors.New("private key is not correct") + return re.Emit(&DashboardResponse{Success: false, Text: "private key is not correct"}) } datastore := node.Repo.Datastore() @@ -157,13 +167,13 @@ var resetCmd = &cmds.Command{ return err } if string(value) != req.Arguments[1] { - return errors.New("password is not correct") + return re.Emit(&DashboardResponse{Success: false, Text: "the old password is not correct"}) } err = datastore.Put(req.Context, ds.NewKey(DashboardPasswordPrefix), []byte(req.Arguments[2])) if err != nil { return err } - return re.Emit(bytes.NewReader([]byte("reset"))) + return re.Emit(&DashboardResponse{Success: true, Text: "password reset success!"}) }, } @@ -187,13 +197,13 @@ var changeCmd = &cmds.Command{ return err } if string(value) != req.Arguments[0] { - return errors.New("password is not correct") + return re.Emit(&DashboardResponse{Success: false, Text: "the old password is not correct"}) } err = datastore.Put(req.Context, ds.NewKey(DashboardPasswordPrefix), []byte(req.Arguments[1])) if err != nil { return err } - return re.Emit(bytes.NewReader([]byte("change password ok"))) + return re.Emit(&DashboardResponse{Success: true, Text: "password change success!"}) }, } @@ -207,6 +217,6 @@ var logoutCmd = &cmds.Command{ Run: func(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) error { // set token expire to 0 IsLogin = false - return re.Emit(bytes.NewReader([]byte("logout success"))) + return re.Emit(&DashboardResponse{Success: true, Text: "logout success!"}) }, -} +} \ No newline at end of file From a80ec9598fd766f4fedbb0e2dc0ebcfb4fc6bca2 Mon Sep 17 00:00:00 2001 From: cody Date: Mon, 12 Aug 2024 18:43:59 +0800 Subject: [PATCH 06/10] feat: add two step check for some url --- core/commands/dashboard.go | 25 ++++++--- core/corehttp/corehttp.go | 6 +++ core/corehttp/corehttp_interceptor.go | 73 ++++++++++++++++++++++++++- 3 files changed, 97 insertions(+), 7 deletions(-) diff --git a/core/commands/dashboard.go b/core/commands/dashboard.go index ca7bad4ef..3cf2550dd 100644 --- a/core/commands/dashboard.go +++ b/core/commands/dashboard.go @@ -25,12 +25,13 @@ var dashboardCmd = &cmds.Command{ }, Subcommands: map[string]*cmds.Command{ - "check": checkCmd, - "set": setCmd, - "reset": resetCmd, - "change": changeCmd, - "login": loginCmd, - "logout": logoutCmd, + "check": checkCmd, + "set": setCmd, + "reset": resetCmd, + "change": changeCmd, + "login": loginCmd, + "logout": logoutCmd, + "validate": validateCmd, }, } @@ -219,4 +220,16 @@ var logoutCmd = &cmds.Command{ IsLogin = false return re.Emit(&DashboardResponse{Success: true, Text: "logout success!"}) }, +} + +var validateCmd = &cmds.Command{ + Helptext: cmds.HelpText{ + Tagline: "check passwd", + }, + Arguments: []cmds.Argument{ + cmds.StringArg("password", true, false, "check passwd"), + }, + Run: func(r *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) error { + return nil + }, } \ No newline at end of file diff --git a/core/corehttp/corehttp.go b/core/corehttp/corehttp.go index 27401ee49..31b769300 100644 --- a/core/corehttp/corehttp.go +++ b/core/corehttp/corehttp.go @@ -67,6 +67,12 @@ func makeHandler(n *core.IpfsNode, l net.Listener, options ...ServeOption) (http } topMux.ServeHTTP(w, r) + + err = interceptorAfterResp(r, w, n) + if err != nil { + return + } + }) return handler, nil } diff --git a/core/corehttp/corehttp_interceptor.go b/core/corehttp/corehttp_interceptor.go index 95890929d..209aa0be5 100644 --- a/core/corehttp/corehttp_interceptor.go +++ b/core/corehttp/corehttp_interceptor.go @@ -1,14 +1,21 @@ package corehttp import ( + "errors" "fmt" "github.com/bittorrent/go-btfs/core" "github.com/bittorrent/go-btfs/core/commands" "github.com/bittorrent/go-btfs/utils" ds "github.com/ipfs/go-datastore" "net/http" + "strings" + "time" ) +const defaultTwoStepDuration = 30 * time.Minute + +const firstStepUrl = "/api/v1/dashboard/validate" + func interceptorBeforeReq(r *http.Request, n *core.IpfsNode) error { config, err := n.Repo.Config() if err != nil { @@ -22,6 +29,30 @@ func interceptorBeforeReq(r *http.Request, n *core.IpfsNode) error { } } + err = twoStepCheckInterceptor(r) + if err != nil { + return err + } + + return nil +} + +func twoStepCheckInterceptor(r *http.Request) error { + if !need2StepCheckUrl(r.URL.Path) { + return nil + } + if currentStep == secondStep { + return nil + } + + return errors.New("please validate your password first") +} + +func interceptorAfterResp(r *http.Request, w http.ResponseWriter, n *core.IpfsNode) error { + err := passwordCheckInterceptor(r) + if err != nil { + return err + } return nil } @@ -50,7 +81,14 @@ func tokenCheckInterceptor(r *http.Request, n *core.IpfsNode) error { } func filterNoNeedTokenCheckReq(r *http.Request) bool { - if filterUrl(r) || filterP2pSchema(r) || filterLocalShellApi(r) { + if filterUrl(r) || filterP2pSchema(r) || filterLocalShellApi(r) || filterGatewayUrl(r) { + return true + } + return false +} + +func filterGatewayUrl(r *http.Request) bool { + if strings.HasPrefix(r.URL.Path, "/btfs/") || strings.HasPrefix(r.URL.Path, "/btns/") { return true } return false @@ -88,4 +126,37 @@ func filterP2pSchema(r *http.Request) bool { return true } return false +} + +const ( + _ int = iota + firstStep + secondStep +) + +var currentStep = firstStep + +func passwordCheckInterceptor(r *http.Request) error { + if r.URL.Path == firstStepUrl && currentStep == firstStep { + currentStep = secondStep + // if next step was done after the default duration + go func() { + <-time.After(defaultTwoStepDuration) + currentStep = firstStep + }() + return nil + } + + if need2StepCheckUrl(r.URL.Path) && currentStep == secondStep { + currentStep = firstStep + } + + return nil +} + +func need2StepCheckUrl(path string) bool { + if path == "/api/v1/xxx" { + return true + } + return false } \ No newline at end of file From 83082453f2b46e9bcd2825eb136f15ca5862f89d Mon Sep 17 00:00:00 2001 From: cody Date: Tue, 13 Aug 2024 11:38:07 +0800 Subject: [PATCH 07/10] feat: add dashboard logout --- core/commands/dashboard.go | 39 +++++++++++++-------------- core/corehttp/corehttp.go | 3 +-- core/corehttp/corehttp_interceptor.go | 11 +++++--- utils/token.go | 4 +-- 4 files changed, 29 insertions(+), 28 deletions(-) diff --git a/core/commands/dashboard.go b/core/commands/dashboard.go index 3cf2550dd..4e8b01e49 100644 --- a/core/commands/dashboard.go +++ b/core/commands/dashboard.go @@ -12,7 +12,7 @@ import ( const DashboardPasswordPrefix = "/dashboard_password" const TokenExpire = 60 * 60 * 24 * 1 -var IsLogin = false +var IsLogin bool type DashboardResponse struct { Success bool @@ -123,7 +123,7 @@ var loginCmd = &cmds.Command{ publicKey := config.Identity.PeerID - token, err := utils.GenerateToken(publicKey, req.Arguments[0], TokenExpire) + token, err := utils.GenerateToken(publicKey, string(value), TokenExpire) if err != nil { return err } @@ -143,7 +143,6 @@ var resetCmd = &cmds.Command{ }, Arguments: []cmds.Argument{ cmds.StringArg("privateKey", true, false, "private key"), - cmds.StringArg("oldPassword", true, false, "old password"), cmds.StringArg("newPassword", true, false, "new password"), }, @@ -162,15 +161,7 @@ var resetCmd = &cmds.Command{ return re.Emit(&DashboardResponse{Success: false, Text: "private key is not correct"}) } datastore := node.Repo.Datastore() - - value, err := datastore.Get(req.Context, ds.NewKey(DashboardPasswordPrefix)) - if err != nil { - return err - } - if string(value) != req.Arguments[1] { - return re.Emit(&DashboardResponse{Success: false, Text: "the old password is not correct"}) - } - err = datastore.Put(req.Context, ds.NewKey(DashboardPasswordPrefix), []byte(req.Arguments[2])) + err = datastore.Put(req.Context, ds.NewKey(DashboardPasswordPrefix), []byte(req.Arguments[1])) if err != nil { return err } @@ -187,7 +178,6 @@ var changeCmd = &cmds.Command{ cmds.StringArg("newPassword", true, false, "change password"), }, Run: func(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) error { - // change password node, err := cmdenv.GetNode(env) if err != nil { return err @@ -212,11 +202,7 @@ var logoutCmd = &cmds.Command{ Helptext: cmds.HelpText{ Tagline: "logout", }, - Arguments: []cmds.Argument{ - cmds.StringArg("token", true, false, "logout"), - }, Run: func(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) error { - // set token expire to 0 IsLogin = false return re.Emit(&DashboardResponse{Success: true, Text: "logout success!"}) }, @@ -224,12 +210,25 @@ var logoutCmd = &cmds.Command{ var validateCmd = &cmds.Command{ Helptext: cmds.HelpText{ - Tagline: "check passwd", + Tagline: "validate passwd", }, Arguments: []cmds.Argument{ - cmds.StringArg("password", true, false, "check passwd"), + cmds.StringArg("password", true, false, "validate passwd"), }, Run: func(r *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) error { - return nil + node, err := cmdenv.GetNode(env) + if err != nil { + return err + } + datastore := node.Repo.Datastore() + value, err := datastore.Get(r.Context, ds.NewKey(DashboardPasswordPrefix)) + if err != nil { + return err + } + + if string(value) != r.Arguments[0] { + return re.Emit(&DashboardResponse{Success: false, Text: "password is not correct"}) + } + return re.Emit(&DashboardResponse{Success: true, Text: "password is correct"}) }, } \ No newline at end of file diff --git a/core/corehttp/corehttp.go b/core/corehttp/corehttp.go index 31b769300..f6f396aad 100644 --- a/core/corehttp/corehttp.go +++ b/core/corehttp/corehttp.go @@ -58,8 +58,7 @@ func makeHandler(n *core.IpfsNode, l net.Listener, options ...ServeOption) (http w.Header().Set("Access-Control-Allow-Origin", "*") if r.Method == http.MethodOptions { w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Headers", "X-Stream-Output, X-Chunked-Output, X-Content-Length") - w.WriteHeader(http.StatusOK) + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-Stream-Output, X-Chunked-Output, X-Content-Length") return } http.Error(w, err.Error(), http.StatusUnauthorized) diff --git a/core/corehttp/corehttp_interceptor.go b/core/corehttp/corehttp_interceptor.go index 209aa0be5..6cb8f1f37 100644 --- a/core/corehttp/corehttp_interceptor.go +++ b/core/corehttp/corehttp_interceptor.go @@ -95,13 +95,16 @@ func filterGatewayUrl(r *http.Request) bool { } func filterUrl(r *http.Request) bool { + if strings.HasPrefix(r.URL.Path, "/dashboard") { + return true + } + if strings.HasPrefix(r.URL.Path, "/hostui") { + return true + } urls := map[string]bool{ - // local - "/dashboard": true, - "/hostui": true, - // no need url APIPath + "/id": true, APIPath + "/dashboard/check": true, + APIPath + "/dashboard/set": true, APIPath + "/dashboard/login": true, APIPath + "/dashboard/reset": true, } diff --git a/utils/token.go b/utils/token.go index 570a90e14..f17523a7a 100644 --- a/utils/token.go +++ b/utils/token.go @@ -15,7 +15,7 @@ type Claims struct { } func GenerateToken(peerId, secret string, expiryDuration time.Duration) (string, error) { - expiryTime := time.Now().Add(expiryDuration).Unix() + expiryTime := time.Now().Add(expiryDuration * time.Second).Unix() claims := Claims{ PeerId: peerId, Expiry: expiryTime, @@ -92,4 +92,4 @@ func splitString(s string, sep rune) []string { result = append(result, string(buffer)) } return result -} +} \ No newline at end of file From 601126ef7dc5af37264a37b6ef72ffc6fc0417a8 Mon Sep 17 00:00:00 2001 From: cody Date: Tue, 13 Aug 2024 15:10:40 +0800 Subject: [PATCH 08/10] feat: transfer api two step validate --- core/corehttp/corehttp_interceptor.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/core/corehttp/corehttp_interceptor.go b/core/corehttp/corehttp_interceptor.go index 6cb8f1f37..fa42cc6da 100644 --- a/core/corehttp/corehttp_interceptor.go +++ b/core/corehttp/corehttp_interceptor.go @@ -38,7 +38,7 @@ func interceptorBeforeReq(r *http.Request, n *core.IpfsNode) error { } func twoStepCheckInterceptor(r *http.Request) error { - if !need2StepCheckUrl(r.URL.Path) { + if !needTwoStepCheckUrl(r.URL.Path) { return nil } if currentStep == secondStep { @@ -150,15 +150,20 @@ func passwordCheckInterceptor(r *http.Request) error { return nil } - if need2StepCheckUrl(r.URL.Path) && currentStep == secondStep { + if needTwoStepCheckUrl(r.URL.Path) && currentStep == secondStep { currentStep = firstStep } return nil } -func need2StepCheckUrl(path string) bool { - if path == "/api/v1/xxx" { +func needTwoStepCheckUrl(path string) bool { + urls := map[string]bool{ + APIPath + "/bttc/send-btt-to": true, + APIPath + "/bttc/send-wbtt-to": true, + APIPath + "/bttc/send-token-to": true, + } + if urls[path] { return true } return false From a40b6a845e038d23f9a6f997043fee80c952848c Mon Sep 17 00:00:00 2001 From: cody Date: Tue, 13 Aug 2024 15:19:40 +0800 Subject: [PATCH 09/10] chore: format --- core/commands/commands_test.go | 2 +- core/commands/dashboard.go | 2 +- core/corehttp/corehttp.go | 2 +- core/corehttp/corehttp_interceptor.go | 2 +- utils/token.go | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/commands/commands_test.go b/core/commands/commands_test.go index e0f6bf2df..e902e36cc 100644 --- a/core/commands/commands_test.go +++ b/core/commands/commands_test.go @@ -395,4 +395,4 @@ func TestCommands(t *testing.T) { t.Errorf("subcommand %q is nil even though there was no error", path) } } -} \ No newline at end of file +} diff --git a/core/commands/dashboard.go b/core/commands/dashboard.go index 4e8b01e49..e00f1aa53 100644 --- a/core/commands/dashboard.go +++ b/core/commands/dashboard.go @@ -231,4 +231,4 @@ var validateCmd = &cmds.Command{ } return re.Emit(&DashboardResponse{Success: true, Text: "password is correct"}) }, -} \ No newline at end of file +} diff --git a/core/corehttp/corehttp.go b/core/corehttp/corehttp.go index f6f396aad..5b1dae616 100644 --- a/core/corehttp/corehttp.go +++ b/core/corehttp/corehttp.go @@ -158,4 +158,4 @@ func Serve(node *core.IpfsNode, lis net.Listener, options ...ServeOption) error log.Infof("server at %s terminated", addr) return serverError -} \ No newline at end of file +} diff --git a/core/corehttp/corehttp_interceptor.go b/core/corehttp/corehttp_interceptor.go index fa42cc6da..0d2fb5bc1 100644 --- a/core/corehttp/corehttp_interceptor.go +++ b/core/corehttp/corehttp_interceptor.go @@ -167,4 +167,4 @@ func needTwoStepCheckUrl(path string) bool { return true } return false -} \ No newline at end of file +} diff --git a/utils/token.go b/utils/token.go index f17523a7a..1be7e554f 100644 --- a/utils/token.go +++ b/utils/token.go @@ -92,4 +92,4 @@ func splitString(s string, sep rune) []string { result = append(result, string(buffer)) } return result -} \ No newline at end of file +} From 06e704d6b9856ef81f578041326b80848001182a Mon Sep 17 00:00:00 2001 From: cody Date: Tue, 13 Aug 2024 15:29:43 +0800 Subject: [PATCH 10/10] test: add dashboard command test --- core/commands/commands_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/commands/commands_test.go b/core/commands/commands_test.go index e902e36cc..0002f2d72 100644 --- a/core/commands/commands_test.go +++ b/core/commands/commands_test.go @@ -361,13 +361,14 @@ func TestCommands(t *testing.T) { "/cheque/fix_cheque_cashout", "/encrypt", "/decrypt", - "dashboard", + "/dashboard", "/dashboard/check", "/dashboard/set", "/dashboard/reset", "/dashboard/login", "/dashboard/logout", "/dashboard/change", + "/dashboard/validate", } cmdSet := make(map[string]struct{})