From 5555d8ab59a3acf2aa94c9a03b2908af0cd282b6 Mon Sep 17 00:00:00 2001 From: ivcosla Date: Wed, 6 Mar 2019 21:54:06 +0100 Subject: [PATCH 01/18] added multiple paths for config --- cmd/skywire-node/commands/root.go | 12 +++++++++ internal/pathutil/data.go | 42 +++++++++++++++++++++++++++++++ internal/pathutil/homedir.go | 19 ++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 internal/pathutil/data.go create mode 100644 internal/pathutil/homedir.go diff --git a/cmd/skywire-node/commands/root.go b/cmd/skywire-node/commands/root.go index aac57eb323..2dd6ba0015 100644 --- a/cmd/skywire-node/commands/root.go +++ b/cmd/skywire-node/commands/root.go @@ -9,6 +9,8 @@ import ( "syscall" "time" + "github.com/skycoin/skywire/internal/pathutil" + "github.com/spf13/cobra" "github.com/skycoin/skywire/pkg/node" @@ -21,8 +23,18 @@ var rootCmd = &cobra.Command{ configFile := "skywire.json" if len(args) > 0 { configFile = args[0] + } else if conf, ok := os.LookupEnv("SKYWIRE_CONFIG"); ok { + configFile = conf + } else { + conf, err := pathutil.Find("skywire.json") + if err != nil { + log.Fatalln(err) + } + configFile = conf } + log.Println("using conf file at: ", configFile) + file, err := os.Open(configFile) if err != nil { log.Fatalf("Failed to open config: %s", err) diff --git a/internal/pathutil/data.go b/internal/pathutil/data.go new file mode 100644 index 0000000000..0f340f3505 --- /dev/null +++ b/internal/pathutil/data.go @@ -0,0 +1,42 @@ +package pathutil + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" +) + +var ( + ErrDirectoryNotFound = errors.New(fmt.Sprintf( + "directory not found in any of the following paths: %s", strings.Join(paths(), ", "))) +) + +// paths return the paths in which skywire looks for data directories +func paths() []string { + // if we are unable to find the local directory try next option + localDir, _ := os.Getwd() // nolint: errcheck + + return []string{ + localDir, + filepath.Join(HomeDir(), ".skycoin/skywire"), + "/usr/local/skycoin/skywire", + } +} + +// Find tries to find given directory or file in: +// 1) local app directory +// 2) ${HOME}/.skycoin/skywire/{directory} +// 3) /usr/local/skycoin/skywire/{directory} +// It will return the first path, including given directory if found, by preference. +func Find(name string) (string, error) { + for _, path := range paths() { + _, err := os.Stat(filepath.Join(path, name)) + if err == nil { + return filepath.Join(path, name), nil + } + } + + return "", ErrDirectoryNotFound +} diff --git a/internal/pathutil/homedir.go b/internal/pathutil/homedir.go new file mode 100644 index 0000000000..442a989938 --- /dev/null +++ b/internal/pathutil/homedir.go @@ -0,0 +1,19 @@ +package pathutil + +import ( + "os" + "runtime" +) + +// HomeDir obtains the path to the user's home directory via ENVs. +// SRC: https://github.com/spf13/viper/blob/80ab6657f9ec7e5761f6603320d3d58dfe6970f6/util.go#L144-L153 +func HomeDir() string { + if runtime.GOOS == "windows" { + home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") + if home == "" { + home = os.Getenv("USERPROFILE") + } + return home + } + return os.Getenv("HOME") +} From 3fb44b9337020ee34c5fd724ddf1029dd1618918 Mon Sep 17 00:00:00 2001 From: ivcosla Date: Thu, 7 Mar 2019 15:53:50 +0100 Subject: [PATCH 02/18] linted --- cmd/skywire-node/commands/root.go | 2 +- internal/pathutil/data.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/skywire-node/commands/root.go b/cmd/skywire-node/commands/root.go index 2dd6ba0015..7f97e78b07 100644 --- a/cmd/skywire-node/commands/root.go +++ b/cmd/skywire-node/commands/root.go @@ -20,7 +20,7 @@ var rootCmd = &cobra.Command{ Use: "skywire-node [skywire.json]", Short: "App Node for skywire", Run: func(_ *cobra.Command, args []string) { - configFile := "skywire.json" + var configFile string if len(args) > 0 { configFile = args[0] } else if conf, ok := os.LookupEnv("SKYWIRE_CONFIG"); ok { diff --git a/internal/pathutil/data.go b/internal/pathutil/data.go index 0f340f3505..94ecf10bfc 100644 --- a/internal/pathutil/data.go +++ b/internal/pathutil/data.go @@ -1,16 +1,16 @@ package pathutil import ( - "errors" "fmt" "os" "path/filepath" "strings" ) +// pathutil errors var ( - ErrDirectoryNotFound = errors.New(fmt.Sprintf( - "directory not found in any of the following paths: %s", strings.Join(paths(), ", "))) + ErrDirectoryNotFound = fmt.Errorf( + "directory not found in any of the following paths: %s", strings.Join(paths(), ", ")) ) // paths return the paths in which skywire looks for data directories From bd51899a34d3efdf89487e2b2a2f320f00ef324e Mon Sep 17 00:00:00 2001 From: ivcosla Date: Mon, 11 Mar 2019 17:51:15 +0100 Subject: [PATCH 03/18] added config modes --- cmd/skywire-cli/commands/config.go | 41 +++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/cmd/skywire-cli/commands/config.go b/cmd/skywire-cli/commands/config.go index 547c3a148f..4b32da96de 100644 --- a/cmd/skywire-cli/commands/config.go +++ b/cmd/skywire-cli/commands/config.go @@ -5,6 +5,9 @@ import ( "encoding/json" "log" "os" + "path/filepath" + + "github.com/skycoin/skywire/internal/pathutil" "github.com/spf13/cobra" @@ -16,6 +19,10 @@ func init() { rootCmd.AddCommand(configCmd) } +var ( + configMode string +) + var configCmd = &cobra.Command{ Use: "config [skywire.json]", Short: "Generate default config file", @@ -25,7 +32,15 @@ var configCmd = &cobra.Command{ configFile = args[0] } - conf := defaultConfig() + var conf *node.Config + switch configMode { + case "local": + conf = defaultConfig() + case "home": + conf = homeConfig() + case "system": + conf = systemConfig() + } confFile, err := os.OpenFile(configFile, os.O_RDWR|os.O_CREATE, 0600) if err != nil { log.Fatal("Failed to create config files: ", err) @@ -41,6 +56,26 @@ var configCmd = &cobra.Command{ }, } +func homeConfig() *node.Config { + c := defaultConfig() + + c.AppsPath = filepath.Join(pathutil.HomeDir(), ".skycoin/skywire/apps") + c.Transport.LogStore.Location = filepath.Join(pathutil.HomeDir(), ".skycoin/skywire/transport_logs") + c.Routing.Table.Location = filepath.Join(pathutil.HomeDir(), ".skycoin/skywire/routing.db") + + return c +} + +func systemConfig() *node.Config { + c := defaultConfig() + + c.AppsPath = "/usr/local/skycoin/skywire/apps" + c.Transport.LogStore.Location = "/usr/local/skycoin/skywire/transport_logs" + c.Routing.Table.Location = "/usr/local/skycoin/skywire/routing.db" + + return c +} + func defaultConfig() *node.Config { conf := &node.Config{} conf.Version = "1.0" @@ -82,3 +117,7 @@ func defaultConfig() *node.Config { return conf } + +func init() { + configCmd.Flags().StringVar(&configMode, "mode", "home", "either home or local") +} From 12ab1648513e040e625ed221277505ee3a463623 Mon Sep 17 00:00:00 2001 From: Ivan Costa Date: Mon, 18 Mar 2019 10:42:37 +0100 Subject: [PATCH 04/18] generating local config in docker node --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 348ca9d87a..7b993c8207 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -OPTS?=GO111MODULE=on +OPTS?=GO111MODULE=on DOCKER_IMAGE?=skywire-runner # docker image to use for running skywire-node.`golang`, `buildpack-deps:stretch-scm` is OK too DOCKER_NETWORK?=SKYNET DOCKER_NODE?=SKY01 @@ -80,7 +80,7 @@ docker-bin: docker-volume: docker-apps docker-bin bin - ./skywire-cli config ./node/skywire.json + ./skywire-cli config --mode local ./node/skywire.json cat ./node/skywire.json|grep static_public_key |cut -d ':' -f2 |tr -d '"'','' ' > ./node/PK cat ./node/PK From 5dcb7dfb7171aab37c77924499305f9f32a1b26a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E5=BF=97=E5=AE=87?= Date: Thu, 21 Mar 2019 00:24:42 +0800 Subject: [PATCH 05/18] initial progress on manager node auth --- go.mod | 6 + go.sum | 22 ++++ pkg/manager/auth.go | 51 ++++++++ pkg/manager/auth_store.go | 194 ++++++++++++++++++++++++++++ pkg/manager/node.go | 11 +- pkg/routing/boltdb_routing_table.go | 22 ++-- 6 files changed, 293 insertions(+), 13 deletions(-) create mode 100644 pkg/manager/auth.go create mode 100644 pkg/manager/auth_store.go diff --git a/go.mod b/go.mod index 79fc0b8b87..f2450d6c9d 100644 --- a/go.mod +++ b/go.mod @@ -6,19 +6,25 @@ require ( github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/flynn/noise v0.0.0-20180327030543-2492fe189ae6 github.com/go-chi/chi v4.0.2+incompatible + github.com/google/go-cmp v0.2.0 // indirect github.com/google/uuid v1.1.1 + github.com/gorilla/sessions v1.1.3 // indirect github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/kr/pty v1.1.3 github.com/mattn/go-colorable v0.1.1 // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/mitchellh/go-homedir v1.1.0 + github.com/pkg/errors v0.8.1 // indirect github.com/sirupsen/logrus v1.4.0 // indirect github.com/skycoin/skycoin v0.25.1 github.com/spf13/cobra v0.0.3 github.com/spf13/pflag v1.0.3 // indirect github.com/stretchr/testify v1.3.0 + github.com/volatiletech/authboss v2.2.0+incompatible // indirect + github.com/volatiletech/authboss-clientstate v0.0.0-20190112194853-0943df8b4e05 // indirect go.etcd.io/bbolt v1.3.2 golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a golang.org/x/net v0.0.0-20190313220215-9f648a60d977 + golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 // indirect ) diff --git a/go.sum b/go.sum index e67587f0b5..102a2829de 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= @@ -8,8 +9,17 @@ github.com/flynn/noise v0.0.0-20180327030543-2492fe189ae6 h1:u/UEqS66A5ckRmS4yNp github.com/flynn/noise v0.0.0-20180327030543-2492fe189ae6/go.mod h1:1i71OnUq3iUe1ma7Lr6yG6/rjvM3emb6yoL7xLFzcVQ= github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs= github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +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.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU= +github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= @@ -26,6 +36,8 @@ github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1f github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.4.0 h1:yKenngtzGh+cUSSh6GWbxW2abRqhYUSR/t/6+2QqNvE= @@ -41,17 +53,27 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/volatiletech/authboss v2.2.0+incompatible h1:12TCBRepupxjb9X59jTs+otNxfiY2a/elx2Dx0kPu9s= +github.com/volatiletech/authboss v2.2.0+incompatible/go.mod h1:EDBO8V+iiBoUR721My3a+iIeuH/1t6VcrCd5bl3v8Bs= +github.com/volatiletech/authboss-clientstate v0.0.0-20190112194853-0943df8b4e05 h1:djgQBr+jocgrtoRacNPgFCkg0ib6UtLSGzJ76jT+7yM= +github.com/volatiletech/authboss-clientstate v0.0.0-20190112194853-0943df8b4e05/go.mod h1:OfM17hvA6F9YRmXy2itY6fJiR4j4gXIIVs3NT1UPvoE= go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a h1:YX8ljsm6wXlHZO+aRz9Exqr0evNhKRNe5K/gi+zKh4U= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190313220215-9f648a60d977 h1:actzWV6iWn3GLqN8dZjzsB+CLt+gaV2+wsxroxiQI8I= golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= diff --git a/pkg/manager/auth.go b/pkg/manager/auth.go new file mode 100644 index 0000000000..8238782b61 --- /dev/null +++ b/pkg/manager/auth.go @@ -0,0 +1,51 @@ +package manager + +import ( + "net/http" + "time" +) + +const ( + sessionCookieName = "swm_session" + + boltTimeout = 10 * time.Second + boltUserBucketName = "users" + boltSessionBucketName = "sessions" +) + +type CookieConfig struct { + Domain string // optional + MaxAge int + HTTPOnly bool + Secure bool + SameSite http.SameSite +} + +type AuthConfig struct { + StorePath string +} + +type Auth struct { + +} + +func NewAuth(c AuthConfig) (*Auth, error) { + return nil, nil +} + +func (a *Auth) ChangePassword() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + } +} + +func (a *Auth) Login() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + } +} + +func (a *Auth) Logout() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + } +} diff --git a/pkg/manager/auth_store.go b/pkg/manager/auth_store.go new file mode 100644 index 0000000000..f71e2e3efe --- /dev/null +++ b/pkg/manager/auth_store.go @@ -0,0 +1,194 @@ +package manager + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/skycoin/skywire/internal/httputil" + "github.com/skycoin/skywire/pkg/cipher" + "go.etcd.io/bbolt" + "net/http" + "os" + "path/filepath" +) + +type AuthErrorType string + +const ( + UnknownType = AuthErrorType("Unknown") + UserAlreadyExistsType = AuthErrorType("UserAlreadyExists") + VerificationFailedType = AuthErrorType("VerificationFailed") +) + +type authError struct { + Type AuthErrorType + HTTPStatus int + HTTPMsg string + LogMsg string +} + +func (e authError) Error() string { + if e.LogMsg != "" { + return e.LogMsg + } + return e.HTTPMsg +} + +func (e authError) WriteHTTP(w http.ResponseWriter, r *http.Request) { + httputil.WriteJSON(w, r, e.HTTPStatus, errors.New(e.HTTPMsg)) +} + +func AuthError(err error) *authError { + authErr, ok := err.(*authError) + if !ok { + authErr = &authError{ + Type: UnknownType, + HTTPStatus: http.StatusInternalServerError, + HTTPMsg: "unknown error", + LogMsg: err.Error(), + } + } + return authErr +} + +func ErrUserAlreadyExists(username string) error { + return &authError{ + Type: UserAlreadyExistsType, + HTTPStatus: http.StatusForbidden, + HTTPMsg: fmt.Sprintf("user of name '%s' already exists", username), + } +} + +func ErrVerificationFailed(username string, userExists bool) error { + var logMsg string + if userExists { + logMsg = fmt.Sprintf("verification failed: invalid password provided for existing user '%s'", username) + } else { + logMsg = fmt.Sprintf("verification failed: no such user of username '%s'", username) + } + return &authError{ + Type: VerificationFailedType, + HTTPStatus: http.StatusUnauthorized, + HTTPMsg: "username or password incorrect", + LogMsg: logMsg, + } +} + +type UserEntry struct { + Name string `json:"name"` + PwSalt []byte `json:"pw_salt"` + PwHash cipher.SHA256 `json:"pw_hash"` +} + +func (u *UserEntry) SetPassword(saltLen int, password string) { + u.PwSalt = cipher.RandByte(saltLen) + u.PwHash = cipher.SumSHA256(append([]byte(password), u.PwSalt...)) +} + +func (u *UserEntry) Verify(username, password string) error { + if u.Name != username { + return errors.New("invalid bbolt user entry: username does not match") + } + if cipher.SumSHA256(append([]byte(password), u.PwSalt...)) != u.PwHash { + return ErrVerificationFailed(username, true) + } + return nil +} + +func (u *UserEntry) Encode() []byte { + raw, err := json.Marshal(u) + if err != nil { + panic(err) // TODO: log this. + } + return raw +} + +func (u *UserEntry) Decode(username string, raw []byte) { + if err := json.Unmarshal(raw, u); err != nil { + panic(err) // TODO: log this. + } +} + +type AuthStorer interface { + NewUser(username, password string) error + DeleteUser(username string) error + VerifyUser(username, password string) error + ChangePassword(username, password string) error + + NewSession(username string) (*http.Cookie, error) + DeleteSession(value string) error + UserSessions(username string) ([]*http.Cookie, error) +} + +type BoltAuthStore struct { + db *bbolt.DB + saltLen int +} + +func NewBoltAuthStore(path string, saltLen int) (*BoltAuthStore, error) { + if err := os.MkdirAll(filepath.Dir(path), os.FileMode(0700)); err != nil { + return nil, err + } + db, err := bbolt.Open(path, os.FileMode(0600), &bbolt.Options{Timeout: boltTimeout}) + if err != nil { + return nil, err + } + err = db.Update(func(tx *bbolt.Tx) error { + if _, err := tx.CreateBucketIfNotExists([]byte(boltUserBucketName)); err != nil { + return err + } + if _, err := tx.CreateBucketIfNotExists([]byte(boltSessionBucketName)); err != nil { + return err + } + return nil + }) + return &BoltAuthStore{db: db, saltLen: saltLen}, err +} + +func (s *BoltAuthStore) NewUser(username, password string) error { + return s.db.Update(func(tx *bbolt.Tx) error { + b := tx.Bucket([]byte(boltUserBucketName)) + if len(b.Get([]byte(username))) > 0 { + return ErrUserAlreadyExists(username) + } + entry := UserEntry{Name: username} + entry.SetPassword(s.saltLen, password) + return b.Put([]byte(username), entry.Encode()) + }) +} + +func (s *BoltAuthStore) DeleteUser(username string) error { + return s.db.Update(func(tx *bbolt.Tx) error { + return tx. + Bucket([]byte(boltUserBucketName)). + Delete([]byte(username)) + }) +} + +func (s *BoltAuthStore) VerifyUser(username, password string) error { + return s.db.View(func(tx *bbolt.Tx) error { + raw := tx. + Bucket([]byte(boltUserBucketName)). + Get([]byte(username)) + if len(raw) == 0 { + return ErrVerificationFailed(username, false) + } + var entry UserEntry + entry.Decode(username, raw) + return entry.Verify(username, password) + }) +} + +func (s *BoltAuthStore) ChangePassword(username, password string) error { + return s.db.Update(func(tx *bbolt.Tx) error { + b := tx.Bucket([]byte(boltUserBucketName)) + raw := b.Get([]byte(username)) + if len(raw) == 0 { + return ErrVerificationFailed(username, false) // TODO: Change + } + var entry UserEntry + entry.Decode(username, raw) + entry.SetPassword(s.saltLen, password) + return b.Put([]byte(boltUserBucketName), entry.Encode()) + }) +} \ No newline at end of file diff --git a/pkg/manager/node.go b/pkg/manager/node.go index 60b5058db4..5a027f6b23 100644 --- a/pkg/manager/node.go +++ b/pkg/manager/node.go @@ -84,13 +84,20 @@ func (m *Node) ServeHTTP(w http.ResponseWriter, r *http.Request) { mux.Use(middleware.Timeout(time.Second * 30)) mux.Use(middleware.Logger) - mux.Mount("/api", m.apiRouter()) + mux.Mount("/auth", m.authHandler()) + mux.Mount("/api", m.apiHandler()) mux.ServeHTTP(w, r) } -func (m *Node) apiRouter() http.Handler { +func (m *Node) authHandler() http.Handler { r := chi.NewRouter() + return r +} + +func (m *Node) apiHandler() http.Handler { + r := chi.NewRouter() + r.Use() // TODO: Complete! r.Get("/nodes", m.getNodes()) r.Get("/nodes/{pk}", m.getNode()) diff --git a/pkg/routing/boltdb_routing_table.go b/pkg/routing/boltdb_routing_table.go index 0c045fdfc5..e2394740bc 100644 --- a/pkg/routing/boltdb_routing_table.go +++ b/pkg/routing/boltdb_routing_table.go @@ -6,24 +6,24 @@ import ( "fmt" "math" - bolt "go.etcd.io/bbolt" + "go.etcd.io/bbolt" ) var boltDBBucket = []byte("routing") // BoltDBRoutingTable implements RoutingTable on top of BoltDB. type boltDBRoutingTable struct { - db *bolt.DB + db *bbolt.DB } // BoltDBRoutingTable consturcts a new BoldDBRoutingTable. func BoltDBRoutingTable(path string) (Table, error) { - db, err := bolt.Open(path, 0600, nil) + db, err := bbolt.Open(path, 0600, nil) if err != nil { return nil, err } - err = db.Update(func(tx *bolt.Tx) error { + err = db.Update(func(tx *bbolt.Tx) error { if _, err := tx.CreateBucketIfNotExists(boltDBBucket); err != nil { return fmt.Errorf("failed to create bucket: %s", err) } @@ -39,7 +39,7 @@ func BoltDBRoutingTable(path string) (Table, error) { // AddRule adds routing rule to the table and returns assigned Route ID. func (rt *boltDBRoutingTable) AddRule(rule Rule) (routeID RouteID, err error) { - err = rt.db.Update(func(tx *bolt.Tx) error { + err = rt.db.Update(func(tx *bbolt.Tx) error { b := tx.Bucket(boltDBBucket) nextID, _ := b.NextSequence() // nolint @@ -56,7 +56,7 @@ func (rt *boltDBRoutingTable) AddRule(rule Rule) (routeID RouteID, err error) { // SetRule sets RoutingRule for a given RouteID. func (rt *boltDBRoutingTable) SetRule(routeID RouteID, rule Rule) error { - return rt.db.Update(func(tx *bolt.Tx) error { + return rt.db.Update(func(tx *bbolt.Tx) error { b := tx.Bucket(boltDBBucket) return b.Put(binaryID(routeID), []byte(rule)) @@ -66,7 +66,7 @@ func (rt *boltDBRoutingTable) SetRule(routeID RouteID, rule Rule) error { // Rule returns RoutingRule with a given RouteID. func (rt *boltDBRoutingTable) Rule(routeID RouteID) (Rule, error) { var rule Rule - err := rt.db.View(func(tx *bolt.Tx) error { // nolint: unparam + err := rt.db.View(func(tx *bbolt.Tx) error { // nolint: unparam b := tx.Bucket(boltDBBucket) rule = b.Get(binaryID(routeID)) return nil @@ -77,7 +77,7 @@ func (rt *boltDBRoutingTable) Rule(routeID RouteID) (Rule, error) { // RangeRules iterates over all rules and yields values to the rangeFunc until `next` is false. func (rt *boltDBRoutingTable) RangeRules(rangeFunc RangeFunc) error { - return rt.db.View(func(tx *bolt.Tx) error { // nolint: unparam + return rt.db.View(func(tx *bbolt.Tx) error { // nolint: unparam b := tx.Bucket(boltDBBucket) b.ForEach(func(k, v []byte) error { // nolint if !rangeFunc(RouteID(binary.BigEndian.Uint32(k)), v) { @@ -93,7 +93,7 @@ func (rt *boltDBRoutingTable) RangeRules(rangeFunc RangeFunc) error { // Rules returns RoutingRules for a given RouteIDs. func (rt *boltDBRoutingTable) Rules(routeIDs ...RouteID) (rules []Rule, err error) { rules = []Rule{} - err = rt.db.View(func(tx *bolt.Tx) error { // nolint: unparam + err = rt.db.View(func(tx *bbolt.Tx) error { // nolint: unparam b := tx.Bucket(boltDBBucket) for _, routeID := range routeIDs { @@ -110,7 +110,7 @@ func (rt *boltDBRoutingTable) Rules(routeIDs ...RouteID) (rules []Rule, err erro // DeleteRules removes RoutingRules with a given a RouteIDs. func (rt *boltDBRoutingTable) DeleteRules(routeIDs ...RouteID) error { - return rt.db.Update(func(tx *bolt.Tx) error { + return rt.db.Update(func(tx *bbolt.Tx) error { b := tx.Bucket(boltDBBucket) var dErr error @@ -126,7 +126,7 @@ func (rt *boltDBRoutingTable) DeleteRules(routeIDs ...RouteID) error { // Count returns the number of routing rules stored. func (rt *boltDBRoutingTable) Count() (count int) { - err := rt.db.View(func(tx *bolt.Tx) error { // nolint: unparam + err := rt.db.View(func(tx *bbolt.Tx) error { // nolint: unparam b := tx.Bucket(boltDBBucket) stats := b.Stats() From 3fb368ed101e2a72a7dad96469196ddb0c08a5fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E5=BF=97=E5=AE=87?= Date: Thu, 21 Mar 2019 00:28:59 +0800 Subject: [PATCH 06/18] format and vendor --- go.mod | 8 ++------ go.sum | 30 ++++++++---------------------- pkg/manager/auth.go | 1 - pkg/manager/auth_store.go | 16 +++++++++------- 4 files changed, 19 insertions(+), 36 deletions(-) diff --git a/go.mod b/go.mod index f2450d6c9d..ca0e63f1f0 100644 --- a/go.mod +++ b/go.mod @@ -6,25 +6,21 @@ require ( github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/flynn/noise v0.0.0-20180327030543-2492fe189ae6 github.com/go-chi/chi v4.0.2+incompatible - github.com/google/go-cmp v0.2.0 // indirect github.com/google/uuid v1.1.1 - github.com/gorilla/sessions v1.1.3 // indirect github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/kr/pretty v0.1.0 // indirect github.com/kr/pty v1.1.3 github.com/mattn/go-colorable v0.1.1 // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/mitchellh/go-homedir v1.1.0 - github.com/pkg/errors v0.8.1 // indirect github.com/sirupsen/logrus v1.4.0 // indirect github.com/skycoin/skycoin v0.25.1 github.com/spf13/cobra v0.0.3 github.com/spf13/pflag v1.0.3 // indirect github.com/stretchr/testify v1.3.0 - github.com/volatiletech/authboss v2.2.0+incompatible // indirect - github.com/volatiletech/authboss-clientstate v0.0.0-20190112194853-0943df8b4e05 // indirect go.etcd.io/bbolt v1.3.2 golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a golang.org/x/net v0.0.0-20190313220215-9f648a60d977 - golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) diff --git a/go.sum b/go.sum index 102a2829de..a7b357e465 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,3 @@ -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= @@ -9,25 +8,21 @@ github.com/flynn/noise v0.0.0-20180327030543-2492fe189ae6 h1:u/UEqS66A5ckRmS4yNp github.com/flynn/noise v0.0.0-20180327030543-2492fe189ae6/go.mod h1:1i71OnUq3iUe1ma7Lr6yG6/rjvM3emb6yoL7xLFzcVQ= github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs= github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= -github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -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.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU= -github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.3 h1:/Um6a/ZmD5tF7peoOJ5oN5KMQ0DrGVQSXLNwyckutPk= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-isatty v0.0.5 h1:tHXDdz1cpzGaovsTB+TVB8q90WEokoVmfMqoVcrLUgw= @@ -36,8 +31,6 @@ github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1f github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.4.0 h1:yKenngtzGh+cUSSh6GWbxW2abRqhYUSR/t/6+2QqNvE= @@ -53,27 +46,20 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/volatiletech/authboss v2.2.0+incompatible h1:12TCBRepupxjb9X59jTs+otNxfiY2a/elx2Dx0kPu9s= -github.com/volatiletech/authboss v2.2.0+incompatible/go.mod h1:EDBO8V+iiBoUR721My3a+iIeuH/1t6VcrCd5bl3v8Bs= -github.com/volatiletech/authboss-clientstate v0.0.0-20190112194853-0943df8b4e05 h1:djgQBr+jocgrtoRacNPgFCkg0ib6UtLSGzJ76jT+7yM= -github.com/volatiletech/authboss-clientstate v0.0.0-20190112194853-0943df8b4e05/go.mod h1:OfM17hvA6F9YRmXy2itY6fJiR4j4gXIIVs3NT1UPvoE= go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a h1:YX8ljsm6wXlHZO+aRz9Exqr0evNhKRNe5K/gi+zKh4U= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190313220215-9f648a60d977 h1:actzWV6iWn3GLqN8dZjzsB+CLt+gaV2+wsxroxiQI8I= golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/manager/auth.go b/pkg/manager/auth.go index 8238782b61..b4910ea2a8 100644 --- a/pkg/manager/auth.go +++ b/pkg/manager/auth.go @@ -26,7 +26,6 @@ type AuthConfig struct { } type Auth struct { - } func NewAuth(c AuthConfig) (*Auth, error) { diff --git a/pkg/manager/auth_store.go b/pkg/manager/auth_store.go index f71e2e3efe..2fbd8f25f1 100644 --- a/pkg/manager/auth_store.go +++ b/pkg/manager/auth_store.go @@ -4,12 +4,14 @@ import ( "encoding/json" "errors" "fmt" - "github.com/skycoin/skywire/internal/httputil" - "github.com/skycoin/skywire/pkg/cipher" - "go.etcd.io/bbolt" "net/http" "os" "path/filepath" + + "go.etcd.io/bbolt" + + "github.com/skycoin/skywire/internal/httputil" + "github.com/skycoin/skywire/pkg/cipher" ) type AuthErrorType string @@ -22,9 +24,9 @@ const ( type authError struct { Type AuthErrorType - HTTPStatus int - HTTPMsg string - LogMsg string + HTTPStatus int + HTTPMsg string + LogMsg string } func (e authError) Error() string { @@ -191,4 +193,4 @@ func (s *BoltAuthStore) ChangePassword(username, password string) error { entry.SetPassword(s.saltLen, password) return b.Put([]byte(boltUserBucketName), entry.Encode()) }) -} \ No newline at end of file +} From 2963076dd61a63ab786354481b152db59370a90d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E5=BF=97=E5=AE=87?= Date: Fri, 22 Mar 2019 10:52:18 +0800 Subject: [PATCH 07/18] inital implementation of manager login/logout --- cmd/manager-node/commands/root.go | 5 +- go.mod | 1 + go.sum | 6 + pkg/manager/auth.go | 50 -------- pkg/manager/auth_store.go | 196 ------------------------------ pkg/manager/node.go | 37 ++++-- pkg/manager/sessions.go | 169 ++++++++++++++++++++++++++ pkg/manager/users.go | 158 ++++++++++++++++++++++++ 8 files changed, 367 insertions(+), 255 deletions(-) delete mode 100644 pkg/manager/auth.go delete mode 100644 pkg/manager/auth_store.go create mode 100644 pkg/manager/sessions.go create mode 100644 pkg/manager/users.go diff --git a/cmd/manager-node/commands/root.go b/cmd/manager-node/commands/root.go index f2a570c271..6c78802004 100644 --- a/cmd/manager-node/commands/root.go +++ b/cmd/manager-node/commands/root.go @@ -57,7 +57,10 @@ var rootCmd = &cobra.Command{ log.Println("SK:", sk) }, Run: func(_ *cobra.Command, _ []string) { - m := manager.NewNode(pk, sk) + m, err := manager.NewNode(manager.Config{}) // TODO: complete + if err != nil { + log.Fatalln("Failed to start manager:", err) + } go func() { l, err := net.Listen("tcp", rpcAddr) if err != nil { diff --git a/go.mod b/go.mod index ca0e63f1f0..0c5f3b50f1 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/flynn/noise v0.0.0-20180327030543-2492fe189ae6 github.com/go-chi/chi v4.0.2+incompatible github.com/google/uuid v1.1.1 + github.com/gorilla/sessions v1.1.3 // indirect github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/kr/pretty v0.1.0 // indirect diff --git a/go.sum b/go.sum index a7b357e465..5825ed9ef7 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,12 @@ github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAU github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +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.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU= +github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= diff --git a/pkg/manager/auth.go b/pkg/manager/auth.go deleted file mode 100644 index b4910ea2a8..0000000000 --- a/pkg/manager/auth.go +++ /dev/null @@ -1,50 +0,0 @@ -package manager - -import ( - "net/http" - "time" -) - -const ( - sessionCookieName = "swm_session" - - boltTimeout = 10 * time.Second - boltUserBucketName = "users" - boltSessionBucketName = "sessions" -) - -type CookieConfig struct { - Domain string // optional - MaxAge int - HTTPOnly bool - Secure bool - SameSite http.SameSite -} - -type AuthConfig struct { - StorePath string -} - -type Auth struct { -} - -func NewAuth(c AuthConfig) (*Auth, error) { - return nil, nil -} - -func (a *Auth) ChangePassword() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - - } -} - -func (a *Auth) Login() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - } -} - -func (a *Auth) Logout() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - - } -} diff --git a/pkg/manager/auth_store.go b/pkg/manager/auth_store.go deleted file mode 100644 index 2fbd8f25f1..0000000000 --- a/pkg/manager/auth_store.go +++ /dev/null @@ -1,196 +0,0 @@ -package manager - -import ( - "encoding/json" - "errors" - "fmt" - "net/http" - "os" - "path/filepath" - - "go.etcd.io/bbolt" - - "github.com/skycoin/skywire/internal/httputil" - "github.com/skycoin/skywire/pkg/cipher" -) - -type AuthErrorType string - -const ( - UnknownType = AuthErrorType("Unknown") - UserAlreadyExistsType = AuthErrorType("UserAlreadyExists") - VerificationFailedType = AuthErrorType("VerificationFailed") -) - -type authError struct { - Type AuthErrorType - HTTPStatus int - HTTPMsg string - LogMsg string -} - -func (e authError) Error() string { - if e.LogMsg != "" { - return e.LogMsg - } - return e.HTTPMsg -} - -func (e authError) WriteHTTP(w http.ResponseWriter, r *http.Request) { - httputil.WriteJSON(w, r, e.HTTPStatus, errors.New(e.HTTPMsg)) -} - -func AuthError(err error) *authError { - authErr, ok := err.(*authError) - if !ok { - authErr = &authError{ - Type: UnknownType, - HTTPStatus: http.StatusInternalServerError, - HTTPMsg: "unknown error", - LogMsg: err.Error(), - } - } - return authErr -} - -func ErrUserAlreadyExists(username string) error { - return &authError{ - Type: UserAlreadyExistsType, - HTTPStatus: http.StatusForbidden, - HTTPMsg: fmt.Sprintf("user of name '%s' already exists", username), - } -} - -func ErrVerificationFailed(username string, userExists bool) error { - var logMsg string - if userExists { - logMsg = fmt.Sprintf("verification failed: invalid password provided for existing user '%s'", username) - } else { - logMsg = fmt.Sprintf("verification failed: no such user of username '%s'", username) - } - return &authError{ - Type: VerificationFailedType, - HTTPStatus: http.StatusUnauthorized, - HTTPMsg: "username or password incorrect", - LogMsg: logMsg, - } -} - -type UserEntry struct { - Name string `json:"name"` - PwSalt []byte `json:"pw_salt"` - PwHash cipher.SHA256 `json:"pw_hash"` -} - -func (u *UserEntry) SetPassword(saltLen int, password string) { - u.PwSalt = cipher.RandByte(saltLen) - u.PwHash = cipher.SumSHA256(append([]byte(password), u.PwSalt...)) -} - -func (u *UserEntry) Verify(username, password string) error { - if u.Name != username { - return errors.New("invalid bbolt user entry: username does not match") - } - if cipher.SumSHA256(append([]byte(password), u.PwSalt...)) != u.PwHash { - return ErrVerificationFailed(username, true) - } - return nil -} - -func (u *UserEntry) Encode() []byte { - raw, err := json.Marshal(u) - if err != nil { - panic(err) // TODO: log this. - } - return raw -} - -func (u *UserEntry) Decode(username string, raw []byte) { - if err := json.Unmarshal(raw, u); err != nil { - panic(err) // TODO: log this. - } -} - -type AuthStorer interface { - NewUser(username, password string) error - DeleteUser(username string) error - VerifyUser(username, password string) error - ChangePassword(username, password string) error - - NewSession(username string) (*http.Cookie, error) - DeleteSession(value string) error - UserSessions(username string) ([]*http.Cookie, error) -} - -type BoltAuthStore struct { - db *bbolt.DB - saltLen int -} - -func NewBoltAuthStore(path string, saltLen int) (*BoltAuthStore, error) { - if err := os.MkdirAll(filepath.Dir(path), os.FileMode(0700)); err != nil { - return nil, err - } - db, err := bbolt.Open(path, os.FileMode(0600), &bbolt.Options{Timeout: boltTimeout}) - if err != nil { - return nil, err - } - err = db.Update(func(tx *bbolt.Tx) error { - if _, err := tx.CreateBucketIfNotExists([]byte(boltUserBucketName)); err != nil { - return err - } - if _, err := tx.CreateBucketIfNotExists([]byte(boltSessionBucketName)); err != nil { - return err - } - return nil - }) - return &BoltAuthStore{db: db, saltLen: saltLen}, err -} - -func (s *BoltAuthStore) NewUser(username, password string) error { - return s.db.Update(func(tx *bbolt.Tx) error { - b := tx.Bucket([]byte(boltUserBucketName)) - if len(b.Get([]byte(username))) > 0 { - return ErrUserAlreadyExists(username) - } - entry := UserEntry{Name: username} - entry.SetPassword(s.saltLen, password) - return b.Put([]byte(username), entry.Encode()) - }) -} - -func (s *BoltAuthStore) DeleteUser(username string) error { - return s.db.Update(func(tx *bbolt.Tx) error { - return tx. - Bucket([]byte(boltUserBucketName)). - Delete([]byte(username)) - }) -} - -func (s *BoltAuthStore) VerifyUser(username, password string) error { - return s.db.View(func(tx *bbolt.Tx) error { - raw := tx. - Bucket([]byte(boltUserBucketName)). - Get([]byte(username)) - if len(raw) == 0 { - return ErrVerificationFailed(username, false) - } - var entry UserEntry - entry.Decode(username, raw) - return entry.Verify(username, password) - }) -} - -func (s *BoltAuthStore) ChangePassword(username, password string) error { - return s.db.Update(func(tx *bbolt.Tx) error { - b := tx.Bucket([]byte(boltUserBucketName)) - raw := b.Get([]byte(username)) - if len(raw) == 0 { - return ErrVerificationFailed(username, false) // TODO: Change - } - var entry UserEntry - entry.Decode(username, raw) - entry.SetPassword(s.saltLen, password) - return b.Put([]byte(boltUserBucketName), entry.Encode()) - }) -} diff --git a/pkg/manager/node.go b/pkg/manager/node.go index 5a027f6b23..379fc80062 100644 --- a/pkg/manager/node.go +++ b/pkg/manager/node.go @@ -26,28 +26,41 @@ import ( "github.com/skycoin/skywire/pkg/routing" ) +type Config struct { + PK cipher.PubKey + SK cipher.SecKey + Users UsersConfig + Sessions SessionsConfig +} + // Node manages AppNodes. type Node struct { - pk cipher.PubKey - sk cipher.SecKey + c Config nodes map[cipher.PubKey]node.RPCClient // connected remote nodes. + users UserStorer + sessions *SessionsManager mu *sync.RWMutex } // NewNode creates a new Node. -func NewNode(pk cipher.PubKey, sk cipher.SecKey) *Node { +func NewNode(config Config) (*Node, error) { + users, err := NewBoltUserStore(config.Users) + if err != nil { + return nil, err + } return &Node{ - pk: pk, - sk: sk, + c: config, nodes: make(map[cipher.PubKey]node.RPCClient), + users: users, + sessions: NewSessionsManager(users, config.Sessions), mu: new(sync.RWMutex), - } + }, nil } // ServeRPC serves RPC of a Node. func (m *Node) ServeRPC(lis net.Listener) error { for { - conn, err := noise.WrapListener(lis, m.pk, m.sk, false, noise.HandshakeXK).Accept() + conn, err := noise.WrapListener(lis, m.c.PK, m.c.SK, false, noise.HandshakeXK).Accept() if err != nil { return err } @@ -92,12 +105,14 @@ func (m *Node) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (m *Node) authHandler() http.Handler { r := chi.NewRouter() + r.Post("/login", m.sessions.Login()) + r.Post("/logout", m.sessions.Logout()) return r } func (m *Node) apiHandler() http.Handler { r := chi.NewRouter() - r.Use() // TODO: Complete! + r.Use(m.sessions.Authorize) r.Get("/nodes", m.getNodes()) r.Get("/nodes/{pk}", m.getNode()) @@ -533,3 +548,9 @@ func (m *Node) ctxRoute(next routeHandlerFunc) http.HandlerFunc { next(w, r, routeCtx{nodeCtx: ctx, RtKey: rid}) }) } + +func catch(err error) { + if err != nil { + panic(err) + } +} \ No newline at end of file diff --git a/pkg/manager/sessions.go b/pkg/manager/sessions.go new file mode 100644 index 0000000000..281d401b8f --- /dev/null +++ b/pkg/manager/sessions.go @@ -0,0 +1,169 @@ +package manager + +import ( + "encoding/gob" + "errors" + "github.com/google/uuid" + "github.com/gorilla/securecookie" + "github.com/skycoin/skywire/internal/httputil" + "net/http" + "sync" + "time" +) + +const ( + sessionCookieName = "swm_session" +) + +func init() { + gob.Register(uuid.UUID{}) +} + +type Session struct { + User string + Expiry time.Time +} + +type SessionsConfig struct { + HashKey []byte + BlockKey []byte + + Path string // optional + Domain string // optional + Expires time.Time // optional + Secure bool + HttpOnly bool + SameSite http.SameSite +} + +type SessionsManager struct { + users UserStorer + + config SessionsConfig + sessions map[uuid.UUID]*Session + crypto *securecookie.SecureCookie + mu *sync.RWMutex +} + +func NewSessionsManager(users UserStorer, config SessionsConfig) *SessionsManager { + return &SessionsManager{ + users: users, + config: config, + sessions: make(map[uuid.UUID]*Session), + crypto: securecookie.New(config.HashKey, config.BlockKey), + mu: new(sync.RWMutex), + } +} + +func (s *SessionsManager) Login() http.HandlerFunc { + type Request struct { + Username string `json:"username"` + Password string `json:"password"` + } + return func(w http.ResponseWriter, r *http.Request) { + if _, err := r.Cookie(sessionCookieName); err != http.ErrNoCookie { + httputil.WriteJSON(w, r, http.StatusBadRequest, errors.New("not logged out")) + return + } + var req Request + if err := httputil.ReadJSON(r, &req); err != nil { + httputil.WriteJSON(w, r, http.StatusBadRequest, errors.New("cannot read request body")) + return + } + ok := s.users.VerifyPassword(req.Username, req.Password) + if !ok { + httputil.WriteJSON(w, r, http.StatusUnauthorized, errors.New("incorrect username or password")) + return + } + s.newSession(w, &Session{ + User: req.Username, + Expiry: time.Now().Add(time.Hour * 12), // TODO: Set default expiry. + }) + httputil.WriteJSON(w, r, http.StatusOK, ok) + } +} + +func (s *SessionsManager) Logout() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if err := s.delSession(w, r); err != nil { + httputil.WriteJSON(w, r, http.StatusBadRequest, errors.New("not logged in")) + return + } + httputil.WriteJSON(w, r, http.StatusOK, true) + } +} + +func (s *SessionsManager) Authorize(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := s.checkSession(r); err != nil { + httputil.WriteJSON(w, r, http.StatusUnauthorized, err) + return + } + next.ServeHTTP(w, r) + }) +} + +func (s *SessionsManager) newSession(w http.ResponseWriter, session *Session) { + sid := uuid.New() + s.mu.Lock() + s.sessions[sid] = session + s.mu.Unlock() + value, err := s.crypto.Encode(sessionCookieName, sid) + catch(err) + http.SetCookie(w, &http.Cookie{ + Name: sessionCookieName, + Value: value, + Domain: s.config.Domain, + Expires: s.config.Expires, + Secure: s.config.Secure, + HttpOnly: s.config.HttpOnly, + SameSite: s.config.SameSite, + }) +} + +func (s *SessionsManager) delSession(w http.ResponseWriter, r *http.Request) error { + cookie, err := r.Cookie(sessionCookieName) + if err != nil { + return err + } + var sid uuid.UUID + if err := s.crypto.Decode(sessionCookieName, cookie.Value, &sid); err != nil { + return err + } + s.mu.Lock() + delete(s.sessions, sid) + s.mu.Unlock() + http.SetCookie(w, &http.Cookie{ + Name: sessionCookieName, + Domain: s.config.Domain, + MaxAge: -1, + Secure: s.config.Secure, + HttpOnly: s.config.HttpOnly, + SameSite: s.config.SameSite, + }) + return nil +} + +func (s *SessionsManager) checkSession(r *http.Request) error { + cookie, err := r.Cookie(sessionCookieName) + if err != nil { + return err + } + var sid uuid.UUID + if err := s.crypto.Decode(sessionCookieName, cookie.Value, &sid); err != nil { + return err + } + s.mu.RLock() + session, ok := s.sessions[sid] + s.mu.RUnlock() + if !ok { + return errors.New("invalid session") // TODO: proper error + } + if time.Now().After(session.Expiry) { + s.mu.Lock() + delete(s.sessions, sid) + s.mu.Unlock() + return errors.New("invalid session") // TODO: proper error + } + return nil +} \ No newline at end of file diff --git a/pkg/manager/users.go b/pkg/manager/users.go new file mode 100644 index 0000000000..5a45bdb472 --- /dev/null +++ b/pkg/manager/users.go @@ -0,0 +1,158 @@ +package manager + +import ( + "bytes" + "encoding/gob" + "errors" + "github.com/skycoin/skywire/pkg/cipher" + "go.etcd.io/bbolt" + "os" + "path/filepath" + "time" +) + +const ( + boltTimeout = 10 * time.Second + boltUserBucketName = "users" +) + +func init() { + gob.Register(User{}) +} + +type User struct { + Name string + PwSalt []byte + PwHash cipher.SHA256 +} + +func (u *User) SetPassword(saltLen int, password string) { + u.PwSalt = cipher.RandByte(saltLen) + u.PwHash = cipher.SumSHA256(append([]byte(password), u.PwSalt...)) +} + +func (u *User) Verify(username, password string) bool { + if u.Name != username { + panic(errors.New("invalid bbolt user entry: username does not match")) // TODO: log. + } + return cipher.SumSHA256(append([]byte(password), u.PwSalt...)) == u.PwHash +} + +func (u *User) Encode() []byte { + var buf bytes.Buffer + if err := gob.NewEncoder(&buf).Encode(u); err != nil { + panic(err) // TODO: log. + } + return buf.Bytes() +} + +func DecodeBoltUser(raw []byte) *User { + var user User + if err := gob.NewDecoder(bytes.NewReader(raw)).Decode(&user); err != nil { + panic(err) // TODO: log this. + } + return &user +} + +type UserStorer interface { + NewUser(username, password string) bool + DeleteUser(username string) + HasUser(username string) bool + VerifyPassword(username, password string) bool + ChangePassword(username, newPassword string) bool +} + +type UsersConfig struct { + DBPath string + SaltLen int // Salt Len for password verification data. + UsernamePattern string // regular expression for usernames (no check if empty). TODO + PasswordPattern string // regular expression for passwords (no check of empty). TODO +} + +type BoltUserStore struct { + db *bbolt.DB + c UsersConfig +} + +func NewBoltUserStore(config UsersConfig) (*BoltUserStore, error) { + if err := os.MkdirAll(filepath.Dir(config.DBPath), os.FileMode(0700)); err != nil { + return nil, err + } + db, err := bbolt.Open(config.DBPath, os.FileMode(0600), &bbolt.Options{Timeout: boltTimeout}) + if err != nil { + return nil, err + } + err = db.Update(func(tx *bbolt.Tx) error { + _, err := tx.CreateBucketIfNotExists([]byte(boltUserBucketName)) + return err + }) + return &BoltUserStore{db: db, c: config}, err +} + +func (s *BoltUserStore) NewUser(username, password string) bool { + var ok bool + catch(s.db.Update(func(tx *bbolt.Tx) error { + users := tx.Bucket([]byte(boltUserBucketName)) + if len(users.Get([]byte(username))) > 0 { + ok = false + return nil + } + user := User{Name: username} + user.SetPassword(s.c.SaltLen, password) + if err := users.Put([]byte(username), user.Encode()); err != nil { + return err + } + ok = true + return nil + })) + return ok +} + +func (s *BoltUserStore) DeleteUser(username string) { + catch(s.db.Update(func(tx *bbolt.Tx) error { + return tx.Bucket([]byte(boltUserBucketName)).Delete([]byte(username)) + })) +} + +func (s *BoltUserStore) HasUser(username string) bool { + var ok bool + catch(s.db.View(func(tx *bbolt.Tx) error { + ok = tx.Bucket([]byte(boltUserBucketName)).Get([]byte(username)) != nil + return nil + })) + return ok +} + +func (s *BoltUserStore) VerifyPassword(username, password string) bool { + var ok bool + catch(s.db.View(func(tx *bbolt.Tx) error { + raw := tx.Bucket([]byte(boltUserBucketName)).Get([]byte(username)) + if len(raw) == 0 { + ok = false + return nil + } + ok = DecodeBoltUser(raw).Verify(username, password) + return nil + })) + return ok +} + +func (s *BoltUserStore) ChangePassword(username, newPassword string) bool { + var ok bool + catch(s.db.Update(func(tx *bbolt.Tx) error { + users := tx.Bucket([]byte(boltUserBucketName)) + rawUser := users.Get([]byte(username)) + if len(rawUser) == 0 { + ok = false + return nil + } + user := DecodeBoltUser(rawUser) + user.SetPassword(s.c.SaltLen, newPassword) + if err := users.Put([]byte(boltUserBucketName), user.Encode()); err != nil { + return err + } + ok = true + return nil + })) + return ok +} From 4ce0be3e67a82d75c9760140c3cc57b3761b887e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E5=BF=97=E5=AE=87?= Date: Fri, 22 Mar 2019 10:53:06 +0800 Subject: [PATCH 08/18] format and vendor --- go.mod | 1 + pkg/manager/node.go | 20 +- pkg/manager/sessions.go | 12 +- pkg/manager/users.go | 14 +- .../gorilla/securecookie/.travis.yml | 19 + .../github.com/gorilla/securecookie/LICENSE | 27 + .../github.com/gorilla/securecookie/README.md | 80 +++ vendor/github.com/gorilla/securecookie/doc.go | 61 ++ .../github.com/gorilla/securecookie/fuzz.go | 25 + .../gorilla/securecookie/securecookie.go | 646 ++++++++++++++++++ vendor/modules.txt | 2 + 11 files changed, 886 insertions(+), 21 deletions(-) create mode 100644 vendor/github.com/gorilla/securecookie/.travis.yml create mode 100644 vendor/github.com/gorilla/securecookie/LICENSE create mode 100644 vendor/github.com/gorilla/securecookie/README.md create mode 100644 vendor/github.com/gorilla/securecookie/doc.go create mode 100644 vendor/github.com/gorilla/securecookie/fuzz.go create mode 100644 vendor/github.com/gorilla/securecookie/securecookie.go diff --git a/go.mod b/go.mod index 0c5f3b50f1..7aa65110eb 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/flynn/noise v0.0.0-20180327030543-2492fe189ae6 github.com/go-chi/chi v4.0.2+incompatible github.com/google/uuid v1.1.1 + github.com/gorilla/securecookie v1.1.1 github.com/gorilla/sessions v1.1.3 // indirect github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d github.com/inconshreveable/mousetrap v1.0.0 // indirect diff --git a/pkg/manager/node.go b/pkg/manager/node.go index 379fc80062..2befce7237 100644 --- a/pkg/manager/node.go +++ b/pkg/manager/node.go @@ -35,11 +35,11 @@ type Config struct { // Node manages AppNodes. type Node struct { - c Config - nodes map[cipher.PubKey]node.RPCClient // connected remote nodes. - users UserStorer - sessions *SessionsManager - mu *sync.RWMutex + c Config + nodes map[cipher.PubKey]node.RPCClient // connected remote nodes. + users UserStorer + sessions *SessionsManager + mu *sync.RWMutex } // NewNode creates a new Node. @@ -49,11 +49,11 @@ func NewNode(config Config) (*Node, error) { return nil, err } return &Node{ - c: config, - nodes: make(map[cipher.PubKey]node.RPCClient), - users: users, + c: config, + nodes: make(map[cipher.PubKey]node.RPCClient), + users: users, sessions: NewSessionsManager(users, config.Sessions), - mu: new(sync.RWMutex), + mu: new(sync.RWMutex), }, nil } @@ -553,4 +553,4 @@ func catch(err error) { if err != nil { panic(err) } -} \ No newline at end of file +} diff --git a/pkg/manager/sessions.go b/pkg/manager/sessions.go index 281d401b8f..422999cf0d 100644 --- a/pkg/manager/sessions.go +++ b/pkg/manager/sessions.go @@ -3,12 +3,14 @@ package manager import ( "encoding/gob" "errors" - "github.com/google/uuid" - "github.com/gorilla/securecookie" - "github.com/skycoin/skywire/internal/httputil" "net/http" "sync" "time" + + "github.com/google/uuid" + "github.com/gorilla/securecookie" + + "github.com/skycoin/skywire/internal/httputil" ) const ( @@ -134,7 +136,7 @@ func (s *SessionsManager) delSession(w http.ResponseWriter, r *http.Request) err delete(s.sessions, sid) s.mu.Unlock() http.SetCookie(w, &http.Cookie{ - Name: sessionCookieName, + Name: sessionCookieName, Domain: s.config.Domain, MaxAge: -1, Secure: s.config.Secure, @@ -166,4 +168,4 @@ func (s *SessionsManager) checkSession(r *http.Request) error { return errors.New("invalid session") // TODO: proper error } return nil -} \ No newline at end of file +} diff --git a/pkg/manager/users.go b/pkg/manager/users.go index 5a45bdb472..1c18d641f0 100644 --- a/pkg/manager/users.go +++ b/pkg/manager/users.go @@ -4,11 +4,13 @@ import ( "bytes" "encoding/gob" "errors" - "github.com/skycoin/skywire/pkg/cipher" - "go.etcd.io/bbolt" "os" "path/filepath" "time" + + "go.etcd.io/bbolt" + + "github.com/skycoin/skywire/pkg/cipher" ) const ( @@ -63,8 +65,8 @@ type UserStorer interface { } type UsersConfig struct { - DBPath string - SaltLen int // Salt Len for password verification data. + DBPath string + SaltLen int // Salt Len for password verification data. UsernamePattern string // regular expression for usernames (no check if empty). TODO PasswordPattern string // regular expression for passwords (no check of empty). TODO } @@ -109,9 +111,9 @@ func (s *BoltUserStore) NewUser(username, password string) bool { } func (s *BoltUserStore) DeleteUser(username string) { - catch(s.db.Update(func(tx *bbolt.Tx) error { + catch(s.db.Update(func(tx *bbolt.Tx) error { return tx.Bucket([]byte(boltUserBucketName)).Delete([]byte(username)) - })) + })) } func (s *BoltUserStore) HasUser(username string) bool { diff --git a/vendor/github.com/gorilla/securecookie/.travis.yml b/vendor/github.com/gorilla/securecookie/.travis.yml new file mode 100644 index 0000000000..6f440f1e42 --- /dev/null +++ b/vendor/github.com/gorilla/securecookie/.travis.yml @@ -0,0 +1,19 @@ +language: go +sudo: false + +matrix: + include: + - go: 1.3 + - go: 1.4 + - go: 1.5 + - go: 1.6 + - go: 1.7 + - go: tip + allow_failures: + - go: tip + +script: + - go get -t -v ./... + - diff -u <(echo -n) <(gofmt -d .) + - go vet $(go list ./... | grep -v /vendor/) + - go test -v -race ./... diff --git a/vendor/github.com/gorilla/securecookie/LICENSE b/vendor/github.com/gorilla/securecookie/LICENSE new file mode 100644 index 0000000000..0e5fb87280 --- /dev/null +++ b/vendor/github.com/gorilla/securecookie/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012 Rodrigo Moraes. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/gorilla/securecookie/README.md b/vendor/github.com/gorilla/securecookie/README.md new file mode 100644 index 0000000000..aa7bd1a5b2 --- /dev/null +++ b/vendor/github.com/gorilla/securecookie/README.md @@ -0,0 +1,80 @@ +securecookie +============ +[![GoDoc](https://godoc.org/github.com/gorilla/securecookie?status.svg)](https://godoc.org/github.com/gorilla/securecookie) [![Build Status](https://travis-ci.org/gorilla/securecookie.png?branch=master)](https://travis-ci.org/gorilla/securecookie) +[![Sourcegraph](https://sourcegraph.com/github.com/gorilla/securecookie/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/securecookie?badge) + + +securecookie encodes and decodes authenticated and optionally encrypted +cookie values. + +Secure cookies can't be forged, because their values are validated using HMAC. +When encrypted, the content is also inaccessible to malicious eyes. It is still +recommended that sensitive data not be stored in cookies, and that HTTPS be used +to prevent cookie [replay attacks](https://en.wikipedia.org/wiki/Replay_attack). + +## Examples + +To use it, first create a new SecureCookie instance: + +```go +// Hash keys should be at least 32 bytes long +var hashKey = []byte("very-secret") +// Block keys should be 16 bytes (AES-128) or 32 bytes (AES-256) long. +// Shorter keys may weaken the encryption used. +var blockKey = []byte("a-lot-secret") +var s = securecookie.New(hashKey, blockKey) +``` + +The hashKey is required, used to authenticate the cookie value using HMAC. +It is recommended to use a key with 32 or 64 bytes. + +The blockKey is optional, used to encrypt the cookie value -- set it to nil +to not use encryption. If set, the length must correspond to the block size +of the encryption algorithm. For AES, used by default, valid lengths are +16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. + +Strong keys can be created using the convenience function GenerateRandomKey(). + +Once a SecureCookie instance is set, use it to encode a cookie value: + +```go +func SetCookieHandler(w http.ResponseWriter, r *http.Request) { + value := map[string]string{ + "foo": "bar", + } + if encoded, err := s.Encode("cookie-name", value); err == nil { + cookie := &http.Cookie{ + Name: "cookie-name", + Value: encoded, + Path: "/", + Secure: true, + HttpOnly: true, + } + http.SetCookie(w, cookie) + } +} +``` + +Later, use the same SecureCookie instance to decode and validate a cookie +value: + +```go +func ReadCookieHandler(w http.ResponseWriter, r *http.Request) { + if cookie, err := r.Cookie("cookie-name"); err == nil { + value := make(map[string]string) + if err = s2.Decode("cookie-name", cookie.Value, &value); err == nil { + fmt.Fprintf(w, "The value of foo is %q", value["foo"]) + } + } +} +``` + +We stored a map[string]string, but secure cookies can hold any value that +can be encoded using `encoding/gob`. To store custom types, they must be +registered first using gob.Register(). For basic types this is not needed; +it works out of the box. An optional JSON encoder that uses `encoding/json` is +available for types compatible with JSON. + +## License + +BSD licensed. See the LICENSE file for details. diff --git a/vendor/github.com/gorilla/securecookie/doc.go b/vendor/github.com/gorilla/securecookie/doc.go new file mode 100644 index 0000000000..ae89408d9d --- /dev/null +++ b/vendor/github.com/gorilla/securecookie/doc.go @@ -0,0 +1,61 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package securecookie encodes and decodes authenticated and optionally +encrypted cookie values. + +Secure cookies can't be forged, because their values are validated using HMAC. +When encrypted, the content is also inaccessible to malicious eyes. + +To use it, first create a new SecureCookie instance: + + var hashKey = []byte("very-secret") + var blockKey = []byte("a-lot-secret") + var s = securecookie.New(hashKey, blockKey) + +The hashKey is required, used to authenticate the cookie value using HMAC. +It is recommended to use a key with 32 or 64 bytes. + +The blockKey is optional, used to encrypt the cookie value -- set it to nil +to not use encryption. If set, the length must correspond to the block size +of the encryption algorithm. For AES, used by default, valid lengths are +16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. + +Strong keys can be created using the convenience function GenerateRandomKey(). + +Once a SecureCookie instance is set, use it to encode a cookie value: + + func SetCookieHandler(w http.ResponseWriter, r *http.Request) { + value := map[string]string{ + "foo": "bar", + } + if encoded, err := s.Encode("cookie-name", value); err == nil { + cookie := &http.Cookie{ + Name: "cookie-name", + Value: encoded, + Path: "/", + } + http.SetCookie(w, cookie) + } + } + +Later, use the same SecureCookie instance to decode and validate a cookie +value: + + func ReadCookieHandler(w http.ResponseWriter, r *http.Request) { + if cookie, err := r.Cookie("cookie-name"); err == nil { + value := make(map[string]string) + if err = s2.Decode("cookie-name", cookie.Value, &value); err == nil { + fmt.Fprintf(w, "The value of foo is %q", value["foo"]) + } + } + } + +We stored a map[string]string, but secure cookies can hold any value that +can be encoded using encoding/gob. To store custom types, they must be +registered first using gob.Register(). For basic types this is not needed; +it works out of the box. +*/ +package securecookie diff --git a/vendor/github.com/gorilla/securecookie/fuzz.go b/vendor/github.com/gorilla/securecookie/fuzz.go new file mode 100644 index 0000000000..e4d0534e41 --- /dev/null +++ b/vendor/github.com/gorilla/securecookie/fuzz.go @@ -0,0 +1,25 @@ +// +build gofuzz + +package securecookie + +var hashKey = []byte("very-secret12345") +var blockKey = []byte("a-lot-secret1234") +var s = New(hashKey, blockKey) + +type Cookie struct { + B bool + I int + S string +} + +func Fuzz(data []byte) int { + datas := string(data) + var c Cookie + if err := s.Decode("fuzz", datas, &c); err != nil { + return 0 + } + if _, err := s.Encode("fuzz", c); err != nil { + panic(err) + } + return 1 +} diff --git a/vendor/github.com/gorilla/securecookie/securecookie.go b/vendor/github.com/gorilla/securecookie/securecookie.go new file mode 100644 index 0000000000..cd4e0976d6 --- /dev/null +++ b/vendor/github.com/gorilla/securecookie/securecookie.go @@ -0,0 +1,646 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package securecookie + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + "encoding/gob" + "encoding/json" + "fmt" + "hash" + "io" + "strconv" + "strings" + "time" +) + +// Error is the interface of all errors returned by functions in this library. +type Error interface { + error + + // IsUsage returns true for errors indicating the client code probably + // uses this library incorrectly. For example, the client may have + // failed to provide a valid hash key, or may have failed to configure + // the Serializer adequately for encoding value. + IsUsage() bool + + // IsDecode returns true for errors indicating that a cookie could not + // be decoded and validated. Since cookies are usually untrusted + // user-provided input, errors of this type should be expected. + // Usually, the proper action is simply to reject the request. + IsDecode() bool + + // IsInternal returns true for unexpected errors occurring in the + // securecookie implementation. + IsInternal() bool + + // Cause, if it returns a non-nil value, indicates that this error was + // propagated from some underlying library. If this method returns nil, + // this error was raised directly by this library. + // + // Cause is provided principally for debugging/logging purposes; it is + // rare that application logic should perform meaningfully different + // logic based on Cause. See, for example, the caveats described on + // (MultiError).Cause(). + Cause() error +} + +// errorType is a bitmask giving the error type(s) of an cookieError value. +type errorType int + +const ( + usageError = errorType(1 << iota) + decodeError + internalError +) + +type cookieError struct { + typ errorType + msg string + cause error +} + +func (e cookieError) IsUsage() bool { return (e.typ & usageError) != 0 } +func (e cookieError) IsDecode() bool { return (e.typ & decodeError) != 0 } +func (e cookieError) IsInternal() bool { return (e.typ & internalError) != 0 } + +func (e cookieError) Cause() error { return e.cause } + +func (e cookieError) Error() string { + parts := []string{"securecookie: "} + if e.msg == "" { + parts = append(parts, "error") + } else { + parts = append(parts, e.msg) + } + if c := e.Cause(); c != nil { + parts = append(parts, " - caused by: ", c.Error()) + } + return strings.Join(parts, "") +} + +var ( + errGeneratingIV = cookieError{typ: internalError, msg: "failed to generate random iv"} + + errNoCodecs = cookieError{typ: usageError, msg: "no codecs provided"} + errHashKeyNotSet = cookieError{typ: usageError, msg: "hash key is not set"} + errBlockKeyNotSet = cookieError{typ: usageError, msg: "block key is not set"} + errEncodedValueTooLong = cookieError{typ: usageError, msg: "the value is too long"} + + errValueToDecodeTooLong = cookieError{typ: decodeError, msg: "the value is too long"} + errTimestampInvalid = cookieError{typ: decodeError, msg: "invalid timestamp"} + errTimestampTooNew = cookieError{typ: decodeError, msg: "timestamp is too new"} + errTimestampExpired = cookieError{typ: decodeError, msg: "expired timestamp"} + errDecryptionFailed = cookieError{typ: decodeError, msg: "the value could not be decrypted"} + errValueNotByte = cookieError{typ: decodeError, msg: "value not a []byte."} + errValueNotBytePtr = cookieError{typ: decodeError, msg: "value not a pointer to []byte."} + + // ErrMacInvalid indicates that cookie decoding failed because the HMAC + // could not be extracted and verified. Direct use of this error + // variable is deprecated; it is public only for legacy compatibility, + // and may be privatized in the future, as it is rarely useful to + // distinguish between this error and other Error implementations. + ErrMacInvalid = cookieError{typ: decodeError, msg: "the value is not valid"} +) + +// Codec defines an interface to encode and decode cookie values. +type Codec interface { + Encode(name string, value interface{}) (string, error) + Decode(name, value string, dst interface{}) error +} + +// New returns a new SecureCookie. +// +// hashKey is required, used to authenticate values using HMAC. Create it using +// GenerateRandomKey(). It is recommended to use a key with 32 or 64 bytes. +// +// blockKey is optional, used to encrypt values. Create it using +// GenerateRandomKey(). The key length must correspond to the block size +// of the encryption algorithm. For AES, used by default, valid lengths are +// 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. +// The default encoder used for cookie serialization is encoding/gob. +// +// Note that keys created using GenerateRandomKey() are not automatically +// persisted. New keys will be created when the application is restarted, and +// previously issued cookies will not be able to be decoded. +func New(hashKey, blockKey []byte) *SecureCookie { + s := &SecureCookie{ + hashKey: hashKey, + blockKey: blockKey, + hashFunc: sha256.New, + maxAge: 86400 * 30, + maxLength: 4096, + sz: GobEncoder{}, + } + if hashKey == nil { + s.err = errHashKeyNotSet + } + if blockKey != nil { + s.BlockFunc(aes.NewCipher) + } + return s +} + +// SecureCookie encodes and decodes authenticated and optionally encrypted +// cookie values. +type SecureCookie struct { + hashKey []byte + hashFunc func() hash.Hash + blockKey []byte + block cipher.Block + maxLength int + maxAge int64 + minAge int64 + err error + sz Serializer + // For testing purposes, the function that returns the current timestamp. + // If not set, it will use time.Now().UTC().Unix(). + timeFunc func() int64 +} + +// Serializer provides an interface for providing custom serializers for cookie +// values. +type Serializer interface { + Serialize(src interface{}) ([]byte, error) + Deserialize(src []byte, dst interface{}) error +} + +// GobEncoder encodes cookie values using encoding/gob. This is the simplest +// encoder and can handle complex types via gob.Register. +type GobEncoder struct{} + +// JSONEncoder encodes cookie values using encoding/json. Users who wish to +// encode complex types need to satisfy the json.Marshaller and +// json.Unmarshaller interfaces. +type JSONEncoder struct{} + +// NopEncoder does not encode cookie values, and instead simply accepts a []byte +// (as an interface{}) and returns a []byte. This is particularly useful when +// you encoding an object upstream and do not wish to re-encode it. +type NopEncoder struct{} + +// MaxLength restricts the maximum length, in bytes, for the cookie value. +// +// Default is 4096, which is the maximum value accepted by Internet Explorer. +func (s *SecureCookie) MaxLength(value int) *SecureCookie { + s.maxLength = value + return s +} + +// MaxAge restricts the maximum age, in seconds, for the cookie value. +// +// Default is 86400 * 30. Set it to 0 for no restriction. +func (s *SecureCookie) MaxAge(value int) *SecureCookie { + s.maxAge = int64(value) + return s +} + +// MinAge restricts the minimum age, in seconds, for the cookie value. +// +// Default is 0 (no restriction). +func (s *SecureCookie) MinAge(value int) *SecureCookie { + s.minAge = int64(value) + return s +} + +// HashFunc sets the hash function used to create HMAC. +// +// Default is crypto/sha256.New. +func (s *SecureCookie) HashFunc(f func() hash.Hash) *SecureCookie { + s.hashFunc = f + return s +} + +// BlockFunc sets the encryption function used to create a cipher.Block. +// +// Default is crypto/aes.New. +func (s *SecureCookie) BlockFunc(f func([]byte) (cipher.Block, error)) *SecureCookie { + if s.blockKey == nil { + s.err = errBlockKeyNotSet + } else if block, err := f(s.blockKey); err == nil { + s.block = block + } else { + s.err = cookieError{cause: err, typ: usageError} + } + return s +} + +// Encoding sets the encoding/serialization method for cookies. +// +// Default is encoding/gob. To encode special structures using encoding/gob, +// they must be registered first using gob.Register(). +func (s *SecureCookie) SetSerializer(sz Serializer) *SecureCookie { + s.sz = sz + + return s +} + +// Encode encodes a cookie value. +// +// It serializes, optionally encrypts, signs with a message authentication code, +// and finally encodes the value. +// +// The name argument is the cookie name. It is stored with the encoded value. +// The value argument is the value to be encoded. It can be any value that can +// be encoded using the currently selected serializer; see SetSerializer(). +// +// It is the client's responsibility to ensure that value, when encoded using +// the current serialization/encryption settings on s and then base64-encoded, +// is shorter than the maximum permissible length. +func (s *SecureCookie) Encode(name string, value interface{}) (string, error) { + if s.err != nil { + return "", s.err + } + if s.hashKey == nil { + s.err = errHashKeyNotSet + return "", s.err + } + var err error + var b []byte + // 1. Serialize. + if b, err = s.sz.Serialize(value); err != nil { + return "", cookieError{cause: err, typ: usageError} + } + // 2. Encrypt (optional). + if s.block != nil { + if b, err = encrypt(s.block, b); err != nil { + return "", cookieError{cause: err, typ: usageError} + } + } + b = encode(b) + // 3. Create MAC for "name|date|value". Extra pipe to be used later. + b = []byte(fmt.Sprintf("%s|%d|%s|", name, s.timestamp(), b)) + mac := createMac(hmac.New(s.hashFunc, s.hashKey), b[:len(b)-1]) + // Append mac, remove name. + b = append(b, mac...)[len(name)+1:] + // 4. Encode to base64. + b = encode(b) + // 5. Check length. + if s.maxLength != 0 && len(b) > s.maxLength { + return "", errEncodedValueTooLong + } + // Done. + return string(b), nil +} + +// Decode decodes a cookie value. +// +// It decodes, verifies a message authentication code, optionally decrypts and +// finally deserializes the value. +// +// The name argument is the cookie name. It must be the same name used when +// it was stored. The value argument is the encoded cookie value. The dst +// argument is where the cookie will be decoded. It must be a pointer. +func (s *SecureCookie) Decode(name, value string, dst interface{}) error { + if s.err != nil { + return s.err + } + if s.hashKey == nil { + s.err = errHashKeyNotSet + return s.err + } + // 1. Check length. + if s.maxLength != 0 && len(value) > s.maxLength { + return errValueToDecodeTooLong + } + // 2. Decode from base64. + b, err := decode([]byte(value)) + if err != nil { + return err + } + // 3. Verify MAC. Value is "date|value|mac". + parts := bytes.SplitN(b, []byte("|"), 3) + if len(parts) != 3 { + return ErrMacInvalid + } + h := hmac.New(s.hashFunc, s.hashKey) + b = append([]byte(name+"|"), b[:len(b)-len(parts[2])-1]...) + if err = verifyMac(h, b, parts[2]); err != nil { + return err + } + // 4. Verify date ranges. + var t1 int64 + if t1, err = strconv.ParseInt(string(parts[0]), 10, 64); err != nil { + return errTimestampInvalid + } + t2 := s.timestamp() + if s.minAge != 0 && t1 > t2-s.minAge { + return errTimestampTooNew + } + if s.maxAge != 0 && t1 < t2-s.maxAge { + return errTimestampExpired + } + // 5. Decrypt (optional). + b, err = decode(parts[1]) + if err != nil { + return err + } + if s.block != nil { + if b, err = decrypt(s.block, b); err != nil { + return err + } + } + // 6. Deserialize. + if err = s.sz.Deserialize(b, dst); err != nil { + return cookieError{cause: err, typ: decodeError} + } + // Done. + return nil +} + +// timestamp returns the current timestamp, in seconds. +// +// For testing purposes, the function that generates the timestamp can be +// overridden. If not set, it will return time.Now().UTC().Unix(). +func (s *SecureCookie) timestamp() int64 { + if s.timeFunc == nil { + return time.Now().UTC().Unix() + } + return s.timeFunc() +} + +// Authentication ------------------------------------------------------------- + +// createMac creates a message authentication code (MAC). +func createMac(h hash.Hash, value []byte) []byte { + h.Write(value) + return h.Sum(nil) +} + +// verifyMac verifies that a message authentication code (MAC) is valid. +func verifyMac(h hash.Hash, value []byte, mac []byte) error { + mac2 := createMac(h, value) + // Check that both MACs are of equal length, as subtle.ConstantTimeCompare + // does not do this prior to Go 1.4. + if len(mac) == len(mac2) && subtle.ConstantTimeCompare(mac, mac2) == 1 { + return nil + } + return ErrMacInvalid +} + +// Encryption ----------------------------------------------------------------- + +// encrypt encrypts a value using the given block in counter mode. +// +// A random initialization vector (http://goo.gl/zF67k) with the length of the +// block size is prepended to the resulting ciphertext. +func encrypt(block cipher.Block, value []byte) ([]byte, error) { + iv := GenerateRandomKey(block.BlockSize()) + if iv == nil { + return nil, errGeneratingIV + } + // Encrypt it. + stream := cipher.NewCTR(block, iv) + stream.XORKeyStream(value, value) + // Return iv + ciphertext. + return append(iv, value...), nil +} + +// decrypt decrypts a value using the given block in counter mode. +// +// The value to be decrypted must be prepended by a initialization vector +// (http://goo.gl/zF67k) with the length of the block size. +func decrypt(block cipher.Block, value []byte) ([]byte, error) { + size := block.BlockSize() + if len(value) > size { + // Extract iv. + iv := value[:size] + // Extract ciphertext. + value = value[size:] + // Decrypt it. + stream := cipher.NewCTR(block, iv) + stream.XORKeyStream(value, value) + return value, nil + } + return nil, errDecryptionFailed +} + +// Serialization -------------------------------------------------------------- + +// Serialize encodes a value using gob. +func (e GobEncoder) Serialize(src interface{}) ([]byte, error) { + buf := new(bytes.Buffer) + enc := gob.NewEncoder(buf) + if err := enc.Encode(src); err != nil { + return nil, cookieError{cause: err, typ: usageError} + } + return buf.Bytes(), nil +} + +// Deserialize decodes a value using gob. +func (e GobEncoder) Deserialize(src []byte, dst interface{}) error { + dec := gob.NewDecoder(bytes.NewBuffer(src)) + if err := dec.Decode(dst); err != nil { + return cookieError{cause: err, typ: decodeError} + } + return nil +} + +// Serialize encodes a value using encoding/json. +func (e JSONEncoder) Serialize(src interface{}) ([]byte, error) { + buf := new(bytes.Buffer) + enc := json.NewEncoder(buf) + if err := enc.Encode(src); err != nil { + return nil, cookieError{cause: err, typ: usageError} + } + return buf.Bytes(), nil +} + +// Deserialize decodes a value using encoding/json. +func (e JSONEncoder) Deserialize(src []byte, dst interface{}) error { + dec := json.NewDecoder(bytes.NewReader(src)) + if err := dec.Decode(dst); err != nil { + return cookieError{cause: err, typ: decodeError} + } + return nil +} + +// Serialize passes a []byte through as-is. +func (e NopEncoder) Serialize(src interface{}) ([]byte, error) { + if b, ok := src.([]byte); ok { + return b, nil + } + + return nil, errValueNotByte +} + +// Deserialize passes a []byte through as-is. +func (e NopEncoder) Deserialize(src []byte, dst interface{}) error { + if dat, ok := dst.(*[]byte); ok { + *dat = src + return nil + } + return errValueNotBytePtr +} + +// Encoding ------------------------------------------------------------------- + +// encode encodes a value using base64. +func encode(value []byte) []byte { + encoded := make([]byte, base64.URLEncoding.EncodedLen(len(value))) + base64.URLEncoding.Encode(encoded, value) + return encoded +} + +// decode decodes a cookie using base64. +func decode(value []byte) ([]byte, error) { + decoded := make([]byte, base64.URLEncoding.DecodedLen(len(value))) + b, err := base64.URLEncoding.Decode(decoded, value) + if err != nil { + return nil, cookieError{cause: err, typ: decodeError, msg: "base64 decode failed"} + } + return decoded[:b], nil +} + +// Helpers -------------------------------------------------------------------- + +// GenerateRandomKey creates a random key with the given length in bytes. +// On failure, returns nil. +// +// Callers should explicitly check for the possibility of a nil return, treat +// it as a failure of the system random number generator, and not continue. +func GenerateRandomKey(length int) []byte { + k := make([]byte, length) + if _, err := io.ReadFull(rand.Reader, k); err != nil { + return nil + } + return k +} + +// CodecsFromPairs returns a slice of SecureCookie instances. +// +// It is a convenience function to create a list of codecs for key rotation. Note +// that the generated Codecs will have the default options applied: callers +// should iterate over each Codec and type-assert the underlying *SecureCookie to +// change these. +// +// Example: +// +// codecs := securecookie.CodecsFromPairs( +// []byte("new-hash-key"), +// []byte("new-block-key"), +// []byte("old-hash-key"), +// []byte("old-block-key"), +// ) +// +// // Modify each instance. +// for _, s := range codecs { +// if cookie, ok := s.(*securecookie.SecureCookie); ok { +// cookie.MaxAge(86400 * 7) +// cookie.SetSerializer(securecookie.JSONEncoder{}) +// cookie.HashFunc(sha512.New512_256) +// } +// } +// +func CodecsFromPairs(keyPairs ...[]byte) []Codec { + codecs := make([]Codec, len(keyPairs)/2+len(keyPairs)%2) + for i := 0; i < len(keyPairs); i += 2 { + var blockKey []byte + if i+1 < len(keyPairs) { + blockKey = keyPairs[i+1] + } + codecs[i/2] = New(keyPairs[i], blockKey) + } + return codecs +} + +// EncodeMulti encodes a cookie value using a group of codecs. +// +// The codecs are tried in order. Multiple codecs are accepted to allow +// key rotation. +// +// On error, may return a MultiError. +func EncodeMulti(name string, value interface{}, codecs ...Codec) (string, error) { + if len(codecs) == 0 { + return "", errNoCodecs + } + + var errors MultiError + for _, codec := range codecs { + encoded, err := codec.Encode(name, value) + if err == nil { + return encoded, nil + } + errors = append(errors, err) + } + return "", errors +} + +// DecodeMulti decodes a cookie value using a group of codecs. +// +// The codecs are tried in order. Multiple codecs are accepted to allow +// key rotation. +// +// On error, may return a MultiError. +func DecodeMulti(name string, value string, dst interface{}, codecs ...Codec) error { + if len(codecs) == 0 { + return errNoCodecs + } + + var errors MultiError + for _, codec := range codecs { + err := codec.Decode(name, value, dst) + if err == nil { + return nil + } + errors = append(errors, err) + } + return errors +} + +// MultiError groups multiple errors. +type MultiError []error + +func (m MultiError) IsUsage() bool { return m.any(func(e Error) bool { return e.IsUsage() }) } +func (m MultiError) IsDecode() bool { return m.any(func(e Error) bool { return e.IsDecode() }) } +func (m MultiError) IsInternal() bool { return m.any(func(e Error) bool { return e.IsInternal() }) } + +// Cause returns nil for MultiError; there is no unique underlying cause in the +// general case. +// +// Note: we could conceivably return a non-nil Cause only when there is exactly +// one child error with a Cause. However, it would be brittle for client code +// to rely on the arity of causes inside a MultiError, so we have opted not to +// provide this functionality. Clients which really wish to access the Causes +// of the underlying errors are free to iterate through the errors themselves. +func (m MultiError) Cause() error { return nil } + +func (m MultiError) Error() string { + s, n := "", 0 + for _, e := range m { + if e != nil { + if n == 0 { + s = e.Error() + } + n++ + } + } + switch n { + case 0: + return "(0 errors)" + case 1: + return s + case 2: + return s + " (and 1 other error)" + } + return fmt.Sprintf("%s (and %d other errors)", s, n-1) +} + +// any returns true if any element of m is an Error for which pred returns true. +func (m MultiError) any(pred func(Error) bool) bool { + for _, e := range m { + if ourErr, ok := e.(Error); ok && pred(ourErr) { + return true + } + } + return false +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 62f420ff71..d41cb5330f 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -9,6 +9,8 @@ github.com/go-chi/chi github.com/go-chi/chi/middleware # github.com/google/uuid v1.1.1 github.com/google/uuid +# github.com/gorilla/securecookie v1.1.1 +github.com/gorilla/securecookie # github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d github.com/hashicorp/yamux # github.com/inconshreveable/mousetrap v1.0.0 From 466dbac55621e99e312d0b2811c2153a1ea9139f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E5=BF=97=E5=AE=87?= Date: Wed, 27 Mar 2019 03:36:45 +0800 Subject: [PATCH 09/18] finalized manager node auth endpoints --- cmd/manager-node/commands/root.go | 4 +- cmd/manager-node/manager.db | Bin 0 -> 32768 bytes pkg/manager/config.go | 58 +++++++ pkg/manager/node.go | 90 +++++------ pkg/manager/sessions.go | 171 -------------------- pkg/manager/user.go | 194 +++++++++++++++++++++++ pkg/manager/user_manager.go | 248 ++++++++++++++++++++++++++++++ pkg/manager/users.go | 160 ------------------- 8 files changed, 542 insertions(+), 383 deletions(-) create mode 100644 cmd/manager-node/manager.db create mode 100644 pkg/manager/config.go delete mode 100644 pkg/manager/sessions.go create mode 100644 pkg/manager/user.go create mode 100644 pkg/manager/user_manager.go delete mode 100644 pkg/manager/users.go diff --git a/cmd/manager-node/commands/root.go b/cmd/manager-node/commands/root.go index 6c78802004..696e076c43 100644 --- a/cmd/manager-node/commands/root.go +++ b/cmd/manager-node/commands/root.go @@ -57,10 +57,11 @@ var rootCmd = &cobra.Command{ log.Println("SK:", sk) }, Run: func(_ *cobra.Command, _ []string) { - m, err := manager.NewNode(manager.Config{}) // TODO: complete + m, err := manager.NewNode(manager.MakeConfig("manager.db")) // TODO: complete if err != nil { log.Fatalln("Failed to start manager:", err) } + log.Infof("serving RPC on '%s'", rpcAddr) go func() { l, err := net.Listen("tcp", rpcAddr) if err != nil { @@ -80,6 +81,7 @@ var rootCmd = &cobra.Command{ log.Fatalln("Failed to add mock data:", err) } } + log.Infof("serving HTTP on '%s'", httpAddr) if err := http.ListenAndServe(httpAddr, m); err != nil { log.Fatalln("Manager exited with error:", err) } diff --git a/cmd/manager-node/manager.db b/cmd/manager-node/manager.db new file mode 100644 index 0000000000000000000000000000000000000000..b0dc825f647938e07c9da4b0952453ac24b3b1ab GIT binary patch literal 32768 zcmeI(I|{-;5CG8iTUi8KK|Fxico{EXsfA+W89bql*x6`rXJU2=LBTTO3nVin8<>6F zB)ciqM>ogoad2%{dGUHbKTf@z^mBZUE%Wt!G~KW6w-_f9AV7cs0RjXF5FkK+009Ec z5oqO7zU}{`-;e&kli$CL7w6M#cxcYdR6>9N0RjXF5FkK+009C72!ufN^F^w&=;z}< ze~c3e5FkK+009C72oNAZfB=Et6^MR6z61R3OjJ&Q009C72oNAZfB*pk1PFW*h;{$% zRPO|+uGQB6>i_?wJa^mWW?RO$hmQttS==N*fB*pk1PBlyK!5-N0! 0 { - ok = false - return nil - } - user := User{Name: username} - user.SetPassword(s.c.SaltLen, password) - if err := users.Put([]byte(username), user.Encode()); err != nil { - return err - } - ok = true - return nil - })) - return ok -} - -func (s *BoltUserStore) DeleteUser(username string) { - catch(s.db.Update(func(tx *bbolt.Tx) error { - return tx.Bucket([]byte(boltUserBucketName)).Delete([]byte(username)) - })) -} - -func (s *BoltUserStore) HasUser(username string) bool { - var ok bool - catch(s.db.View(func(tx *bbolt.Tx) error { - ok = tx.Bucket([]byte(boltUserBucketName)).Get([]byte(username)) != nil - return nil - })) - return ok -} - -func (s *BoltUserStore) VerifyPassword(username, password string) bool { - var ok bool - catch(s.db.View(func(tx *bbolt.Tx) error { - raw := tx.Bucket([]byte(boltUserBucketName)).Get([]byte(username)) - if len(raw) == 0 { - ok = false - return nil - } - ok = DecodeBoltUser(raw).Verify(username, password) - return nil - })) - return ok -} - -func (s *BoltUserStore) ChangePassword(username, newPassword string) bool { - var ok bool - catch(s.db.Update(func(tx *bbolt.Tx) error { - users := tx.Bucket([]byte(boltUserBucketName)) - rawUser := users.Get([]byte(username)) - if len(rawUser) == 0 { - ok = false - return nil - } - user := DecodeBoltUser(rawUser) - user.SetPassword(s.c.SaltLen, newPassword) - if err := users.Put([]byte(boltUserBucketName), user.Encode()); err != nil { - return err - } - ok = true - return nil - })) - return ok -} From 0cbba055072f2e8d29515062411610719fede01c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E5=BF=97=E5=AE=87?= Date: Wed, 27 Mar 2019 12:49:42 +0800 Subject: [PATCH 10/18] added manager node config and associated commands --- cmd/manager-node/commands/init-config.go | 68 ++++++++++++++++ cmd/manager-node/commands/root.go | 76 +++++++++++------- cmd/manager-node/manager.db | Bin 32768 -> 0 bytes internal/pathutil/homedir.go | 19 +++++ pkg/manager/config.go | 95 ++++++++++++++++++----- pkg/manager/node.go | 19 +++-- pkg/manager/user.go | 10 +-- pkg/manager/user_manager.go | 9 +-- 8 files changed, 232 insertions(+), 64 deletions(-) create mode 100644 cmd/manager-node/commands/init-config.go delete mode 100644 cmd/manager-node/manager.db create mode 100644 internal/pathutil/homedir.go diff --git a/cmd/manager-node/commands/init-config.go b/cmd/manager-node/commands/init-config.go new file mode 100644 index 0000000000..4a4743da46 --- /dev/null +++ b/cmd/manager-node/commands/init-config.go @@ -0,0 +1,68 @@ +package commands + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/spf13/cobra" + + "github.com/skycoin/skywire/pkg/manager" +) + +const ( + homeMode = "HOME" + localMode = "LOCAL" +) + +var initConfigModes = []string{homeMode, localMode} + +var ( + output string + replace bool + mode string +) + +func init() { + rootCmd.AddCommand(initConfigCmd) + + initConfigCmd.Flags().StringVarP(&output, "output", "o", defaultConfigPaths[0], "path of output config file.") + initConfigCmd.Flags().BoolVarP(&replace, "replace", "r", false, "whether to allow rewrite of a file that already exists.") + initConfigCmd.Flags().StringVarP(&mode, "mode", "m", homeMode, fmt.Sprintf("config generation mode. Valid values: %v", initConfigModes)) +} + +var initConfigCmd = &cobra.Command{ + Use: "init-config", + Short: "generates a configuration file", + Run: func(_ *cobra.Command, _ []string) { + output, err := filepath.Abs(output) + if err != nil { + log.WithError(err).Fatalln("invalid output provided") + } + var conf manager.Config + switch mode { + case homeMode: + conf = manager.GenerateHomeConfig() + case localMode: + conf = manager.GenerateLocalConfig() + default: + log.Fatalln("invalid mode:", mode) + } + raw, err := json.MarshalIndent(conf, "", " ") + if err != nil { + log.WithError(err).Fatal("unexpected error, report to dev") + } + if _, err := os.Stat(output); !replace && err == nil { + log.Fatalf("file %s already exists, stopping as 'replace,r' flag is not set", output) + } + if err := os.MkdirAll(filepath.Dir(output), 0750); err != nil { + log.WithError(err).Fatalln("failed to create output directory") + } + if err := ioutil.WriteFile(output, raw, 0744); err != nil { + log.WithError(err).Fatalln("failed to write file") + } + log.Infof("Wrote %d bytes to %s\n%s", len(raw), output, string(raw)) + }, +} diff --git a/cmd/manager-node/commands/root.go b/cmd/manager-node/commands/root.go index 696e076c43..2cc05250f9 100644 --- a/cmd/manager-node/commands/root.go +++ b/cmd/manager-node/commands/root.go @@ -1,37 +1,48 @@ package commands import ( + "errors" "fmt" "net" "net/http" "os" + "path/filepath" "github.com/skycoin/skycoin/src/util/logging" "github.com/spf13/cobra" - "github.com/skycoin/skywire/pkg/cipher" + "github.com/skycoin/skywire/internal/pathutil" "github.com/skycoin/skywire/pkg/manager" ) var ( - pk cipher.PubKey - sk cipher.SecKey - rpcAddr string - httpAddr string + log = logging.MustGetLogger("manager-node") + mock bool mockNodes int mockMaxTps int mockMaxRoutes int - log = logging.MustGetLogger("manager-node") + defaultConfigPaths = [2]string{ + filepath.Join(pathutil.HomeDir(), ".skycoin/skywire-manager/config.json"), + "/usr/local/skycoin/skywire-manager/config.json", + } ) -func init() { - rootCmd.PersistentFlags().Var(&pk, "pk", "manager node's public key") - rootCmd.PersistentFlags().Var(&sk, "sk", "manager node's secret key") - rootCmd.PersistentFlags().StringVar(&httpAddr, "http-addr", ":8080", "address to serve HTTP RESTful API and Web interface") +func findConfigPath() (string, error) { + log.Info("configuration file is not explicitly specified, attempting to find one in default paths ...") + for i, cPath := range defaultConfigPaths { + if _, err := os.Stat(cPath); err != nil { + log.Infof("- [%d/%d] '%s' does not exist", i, len(defaultConfigPaths), cPath) + } else { + log.Infof("- [%d/%d] '%s' exists (using this one)", i, len(defaultConfigPaths), cPath) + return cPath, nil + } + } + return "", errors.New("no configuration file found") +} - rootCmd.Flags().StringVar(&rpcAddr, "rpc-addr", ":7080", "address to serve RPC client interface") +func init() { rootCmd.Flags().BoolVar(&mock, "mock", false, "whether to run manager node with mock data") rootCmd.Flags().IntVar(&mockNodes, "mock-nodes", 5, "number of app nodes to have in mock mode") rootCmd.Flags().IntVar(&mockMaxTps, "mock-max-tps", 10, "max number of transports per mock app node") @@ -39,28 +50,36 @@ func init() { } var rootCmd = &cobra.Command{ - Use: "manager-node", + Use: "manager-node [config-path]", Short: "Manages Skywire App Nodes", - PreRun: func(_ *cobra.Command, _ []string) { - if pk.Null() && sk.Null() { - pk, sk = cipher.GenerateKeyPair() - log.Println("No keys are set. Randomly generating...") - } - cPK, err := sk.PubKey() - if err != nil { - log.Fatalln("Key pair check failed:", err) + Run: func(_ *cobra.Command, args []string) { + + var configPath string + if len(args) == 0 { + var err error + if configPath, err = findConfigPath(); err != nil { + log.WithError(err).Fatal() + } + } else { + configPath = args[0] } - if cPK != pk { - log.Fatalln("SK and PK provided do not match.") + log.Infof("config path: '%s'", configPath) + + var config manager.Config + config.FillDefaults() + if err := config.Parse(configPath); err != nil { + log.WithError(err).Fatalln("failed to parse config file") } - log.Println("PK:", pk) - log.Println("SK:", sk) - }, - Run: func(_ *cobra.Command, _ []string) { - m, err := manager.NewNode(manager.MakeConfig("manager.db")) // TODO: complete + var ( + httpAddr = config.Interfaces.HTTPAddr + rpcAddr = config.Interfaces.RPCAddr + ) + + m, err := manager.NewNode(config) if err != nil { log.Fatalln("Failed to start manager:", err) } + log.Infof("serving RPC on '%s'", rpcAddr) go func() { l, err := net.Listen("tcp", rpcAddr) @@ -71,6 +90,7 @@ var rootCmd = &cobra.Command{ log.Fatalln("Failed to serve RPC:", err) } }() + if mock { err := m.AddMockData(&manager.MockConfig{ Nodes: mockNodes, @@ -81,10 +101,12 @@ var rootCmd = &cobra.Command{ log.Fatalln("Failed to add mock data:", err) } } + log.Infof("serving HTTP on '%s'", httpAddr) if err := http.ListenAndServe(httpAddr, m); err != nil { log.Fatalln("Manager exited with error:", err) } + log.Println("Good bye!") }, } diff --git a/cmd/manager-node/manager.db b/cmd/manager-node/manager.db deleted file mode 100644 index b0dc825f647938e07c9da4b0952453ac24b3b1ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI(I|{-;5CG8iTUi8KK|Fxico{EXsfA+W89bql*x6`rXJU2=LBTTO3nVin8<>6F zB)ciqM>ogoad2%{dGUHbKTf@z^mBZUE%Wt!G~KW6w-_f9AV7cs0RjXF5FkK+009Ec z5oqO7zU}{`-;e&kli$CL7w6M#cxcYdR6>9N0RjXF5FkK+009C72!ufN^F^w&=;z}< ze~c3e5FkK+009C72oNAZfB=Et6^MR6z61R3OjJ&Q009C72oNAZfB*pk1PFW*h;{$% zRPO|+uGQB6>i_?wJa^mWW?RO$hmQttS==N*fB*pk1PBlyK!5-N0! 0 { + log.Fatalln(append(msgs, err.Error())) + } else { + log.Fatalln(err) + } } } diff --git a/pkg/manager/user.go b/pkg/manager/user.go index 4d9b65a541..7fe686b5da 100644 --- a/pkg/manager/user.go +++ b/pkg/manager/user.go @@ -31,9 +31,7 @@ type User struct { func (u *User) SetName(pattern, name string) bool { if pattern != "" { ok, err := regexp.MatchString(pattern, name) - if err != nil { - panic(err) // TODO: log - } + catch(err, "invalid username regex:") if !ok { return false } @@ -46,7 +44,7 @@ func (u *User) SetPassword(saltLen int, pattern, password string) bool { if pattern != "" { ok, err := regexp.MatchString(pattern, password) if err != nil { - panic(err) // TODO: log + catch(err, "invalid password regex:") } if !ok { return false @@ -64,7 +62,7 @@ func (u *User) VerifyPassword(password string) bool { func (u *User) Encode() []byte { var buf bytes.Buffer if err := gob.NewEncoder(&buf).Encode(u); err != nil { - panic(err) // TODO: log. + catch(err, "unexpected user encode error:") } return buf.Bytes() } @@ -72,7 +70,7 @@ func (u *User) Encode() []byte { func DecodeUser(raw []byte) User { var user User if err := gob.NewDecoder(bytes.NewReader(raw)).Decode(&user); err != nil { - panic(err) // TODO: log this. + catch(err, "unexpected decode user error:") } return user } diff --git a/pkg/manager/user_manager.go b/pkg/manager/user_manager.go index 67a9bdfd7a..4df1fa5363 100644 --- a/pkg/manager/user_manager.go +++ b/pkg/manager/user_manager.go @@ -2,7 +2,6 @@ package manager import ( "context" - "encoding/gob" "errors" "fmt" "net/http" @@ -19,10 +18,6 @@ const ( sessionCookieName = "swm_session" ) -func init() { - gob.Register(uuid.UUID{}) -} - type Session struct { SID uuid.UUID `json:"sid"` User string `json:"username"` @@ -230,7 +225,7 @@ func (s *UserManager) session(r *http.Request) (User, Session, error) { session, ok := s.sessions[sid] s.mu.RUnlock() if !ok { - return User{}, Session{}, errors.New("invalid session") // TODO: proper error + return User{}, Session{}, errors.New("invalid session") } user, ok := s.db.User(session.User) if !ok { @@ -240,7 +235,7 @@ func (s *UserManager) session(r *http.Request) (User, Session, error) { s.mu.Lock() delete(s.sessions, sid) s.mu.Unlock() - return User{}, Session{}, errors.New("invalid session") // TODO: proper error + return User{}, Session{}, errors.New("invalid session") } return user, session, nil } From da4f009bc187b93ab1bb310116c465ea1ed1c0d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E5=BF=97=E5=AE=87?= Date: Wed, 27 Mar 2019 12:56:16 +0800 Subject: [PATCH 11/18] fix decode bug --- cmd/manager-node/commands/root.go | 4 ++-- pkg/manager/config.go | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cmd/manager-node/commands/root.go b/cmd/manager-node/commands/root.go index 2cc05250f9..0ead506a5b 100644 --- a/cmd/manager-node/commands/root.go +++ b/cmd/manager-node/commands/root.go @@ -33,9 +33,9 @@ func findConfigPath() (string, error) { log.Info("configuration file is not explicitly specified, attempting to find one in default paths ...") for i, cPath := range defaultConfigPaths { if _, err := os.Stat(cPath); err != nil { - log.Infof("- [%d/%d] '%s' does not exist", i, len(defaultConfigPaths), cPath) + log.Infof("- [%d/%d] '%s' does not exist", i+1, len(defaultConfigPaths), cPath) } else { - log.Infof("- [%d/%d] '%s' exists (using this one)", i, len(defaultConfigPaths), cPath) + log.Infof("- [%d/%d] '%s' exists (using this one)", i+1, len(defaultConfigPaths), cPath) return cPath, nil } } diff --git a/pkg/manager/config.go b/pkg/manager/config.go index 0dd77f01d9..440a161133 100644 --- a/pkg/manager/config.go +++ b/pkg/manager/config.go @@ -24,6 +24,7 @@ func (hk Key) MarshalText() ([]byte, error) { } func (hk *Key) UnmarshalText(text []byte) error { + *hk = make([]byte, hex.DecodedLen(len(text))) _, err := hex.Decode(*hk, text) return err } From f51ef1176b7a91c2816e8ab3b2189e8756838426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E5=BF=97=E5=AE=87?= Date: Thu, 28 Mar 2019 03:11:27 +0800 Subject: [PATCH 12/18] added manager login/logout tests --- cmd/manager-node/commands/root.go | 2 +- pkg/manager/config.go | 6 +- pkg/manager/node.go | 53 +++--- pkg/manager/node_test.go | 293 ++++++++++++++++++++++++++++++ pkg/manager/user.go | 34 ++-- pkg/manager/user_manager.go | 59 +++--- 6 files changed, 376 insertions(+), 71 deletions(-) create mode 100644 pkg/manager/node_test.go diff --git a/cmd/manager-node/commands/root.go b/cmd/manager-node/commands/root.go index 0ead506a5b..bdf5d9118a 100644 --- a/cmd/manager-node/commands/root.go +++ b/cmd/manager-node/commands/root.go @@ -92,7 +92,7 @@ var rootCmd = &cobra.Command{ }() if mock { - err := m.AddMockData(&manager.MockConfig{ + err := m.AddMockData(manager.MockConfig{ Nodes: mockNodes, MaxTpsPerNode: mockMaxTps, MaxRoutesPerNode: mockMaxRoutes, diff --git a/pkg/manager/config.go b/pkg/manager/config.go index 440a161133..1999b06042 100644 --- a/pkg/manager/config.go +++ b/pkg/manager/config.go @@ -33,8 +33,6 @@ type Config struct { PK cipher.PubKey `json:"public_key"` SK cipher.SecKey `json:"secret_key"` DBPath string `json:"db_path"` - NameRegexp string `json:"username_regexp"` // regular expression for usernames (no check if empty). TODO - PassRegexp string `json:"password_regexp"` // regular expression for passwords (no check of empty). TODO PassSaltLen int `json:"password_salt_len"` // Salt Len for password verification data. Cookies CookieConfig `json:"cookies"` Interfaces InterfaceConfig `json:"interfaces"` @@ -64,8 +62,6 @@ func GenerateLocalConfig() Config { } func (c *Config) FillDefaults() { - c.NameRegexp = `^(admin)$` - c.PassRegexp = `((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{6,20})` c.PassSaltLen = 16 c.Cookies.FillDefaults() c.Interfaces.FillDefaults() @@ -98,8 +94,8 @@ type CookieConfig struct { } func (c *CookieConfig) FillDefaults() { - c.Path = "/" c.ExpiresDuration = time.Hour * 12 + c.Path = "/" c.Secure = true c.HttpOnly = true c.SameSite = http.SameSiteDefaultMode diff --git a/pkg/manager/node.go b/pkg/manager/node.go index 87e2d55d65..956192b4bc 100644 --- a/pkg/manager/node.go +++ b/pkg/manager/node.go @@ -76,7 +76,7 @@ type MockConfig struct { } // AddMockData adds mock data to Manager Node. -func (m *Node) AddMockData(config *MockConfig) error { +func (m *Node) AddMockData(config MockConfig) error { r := rand.New(rand.NewSource(time.Now().UnixNano())) for i := 0; i < config.Nodes; i++ { pk, client := node.NewMockRPCClient(r, config.MaxTpsPerNode, config.MaxRoutesPerNode) @@ -94,37 +94,40 @@ func (m *Node) ServeHTTP(w http.ResponseWriter, r *http.Request) { mux.Use(middleware.Timeout(time.Second * 30)) mux.Use(middleware.Logger) - mux.Route("/auth", func(r chi.Router) { - r.Post("/create-account", m.users.CreateAccount(m.c.PassSaltLen, m.c.PassRegexp, m.c.NameRegexp)) - r.Post("/login", m.users.Login()) - r.Post("/logout", m.users.Logout()) - }) - mux.Route("/api", func(r chi.Router) { - r.Use(m.users.Authorize) - r.Get("/user/info", m.users.UserInfo()) - r.Post("/user/change-password", m.users.ChangePassword(m.c.PassSaltLen, m.c.PassRegexp)) + r.Group(func(r chi.Router) { + r.Post("/create-account", m.users.CreateAccount(m.c.PassSaltLen)) + r.Post("/login", m.users.Login()) + r.Post("/logout", m.users.Logout()) + }) + + r.Group(func(r chi.Router) { + r.Use(m.users.Authorize) + + r.Get("/user", m.users.UserInfo()) + r.Post("/change-password", m.users.ChangePassword(m.c.PassSaltLen)) - r.Get("/nodes", m.getNodes()) - r.Get("/nodes/{pk}", m.getNode()) + r.Get("/nodes", m.getNodes()) + r.Get("/nodes/{pk}", m.getNode()) - r.Get("/nodes/{pk}/apps", m.getApps()) - r.Get("/nodes/{pk}/apps/{app}", m.getApp()) - r.Put("/nodes/{pk}/apps/{app}", m.putApp()) + r.Get("/nodes/{pk}/apps", m.getApps()) + r.Get("/nodes/{pk}/apps/{app}", m.getApp()) + r.Put("/nodes/{pk}/apps/{app}", m.putApp()) - r.Get("/nodes/{pk}/transport-types", m.getTransportTypes()) + r.Get("/nodes/{pk}/transport-types", m.getTransportTypes()) - r.Get("/nodes/{pk}/transports", m.getTransports()) - r.Post("/nodes/{pk}/transports", m.postTransport()) - r.Get("/nodes/{pk}/transports/{tid}", m.getTransport()) - r.Delete("/nodes/{pk}/transports/{tid}", m.deleteTransport()) + r.Get("/nodes/{pk}/transports", m.getTransports()) + r.Post("/nodes/{pk}/transports", m.postTransport()) + r.Get("/nodes/{pk}/transports/{tid}", m.getTransport()) + r.Delete("/nodes/{pk}/transports/{tid}", m.deleteTransport()) - r.Get("/nodes/{pk}/routes", m.getRoutes()) - r.Post("/nodes/{pk}/routes", m.postRoute()) - r.Get("/nodes/{pk}/routes/{rid}", m.getRoute()) - r.Put("/nodes/{pk}/routes/{rid}", m.putRoute()) - r.Delete("/nodes/{pk}/routes/{rid}", m.deleteRoute()) + r.Get("/nodes/{pk}/routes", m.getRoutes()) + r.Post("/nodes/{pk}/routes", m.postRoute()) + r.Get("/nodes/{pk}/routes/{rid}", m.getRoute()) + r.Put("/nodes/{pk}/routes/{rid}", m.putRoute()) + r.Delete("/nodes/{pk}/routes/{rid}", m.deleteRoute()) + }) }) mux.ServeHTTP(w, r) diff --git a/pkg/manager/node_test.go b/pkg/manager/node_test.go new file mode 100644 index 0000000000..9e43fe6553 --- /dev/null +++ b/pkg/manager/node_test.go @@ -0,0 +1,293 @@ +package manager + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/http/cookiejar" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TODO(evanlinjin): tests to write: +// - no access to any endpoint without login / signup. + +func TestNewNode(t *testing.T) { + config := makeConfig() + + confDir, err := ioutil.TempDir(os.TempDir(), "SWM") + require.NoError(t, err) + config.DBPath = filepath.Join(confDir, "users.db") + + startNode := func(mock MockConfig) (string, *http.Client, func()) { + node, err := NewNode(config) + require.NoError(t, err) + require.NoError(t, node.AddMockData(mock)) + + srv := httptest.NewTLSServer(node) + node.c.Cookies.Domain = srv.Listener.Addr().String() + + client := srv.Client() + jar, err := cookiejar.New(&cookiejar.Options{}) + require.NoError(t, err) + client.Jar = jar + + return srv.Listener.Addr().String(), client, func() { + srv.Close() + require.NoError(t, os.Remove(config.DBPath)) + } + } + + type TestCase struct { + Method string + URI string + Body io.Reader + RespStatus int + RespBody func(t *testing.T, resp *http.Response) + } + + testCases := func(t *testing.T, addr string, client *http.Client, cases []TestCase) { + for i, tc := range cases { + testTag := fmt.Sprintf("[%d] %s", i, tc.URI) + + req, err := http.NewRequest(tc.Method, "https://"+addr+tc.URI, tc.Body) + require.NoError(t, err, testTag) + + resp, err := client.Do(req) + require.NoError(t, err, testTag) + + assert.Equal(t, tc.RespStatus, resp.StatusCode, testTag) + if tc.RespBody != nil { + tc.RespBody(t, resp) + } + } + } + + t.Run("no_access_without_login", func(t *testing.T) { + addr, client, stop := startNode(MockConfig{5, 10, 10}) + defer stop() + + makeCase := func(method string, uri string, body io.Reader) TestCase { + return TestCase{ + Method: method, + URI: uri, + Body: body, + RespStatus: http.StatusUnauthorized, + RespBody: func(t *testing.T, r *http.Response) { + body, err := decodeErrorBody(r.Body) + assert.NoError(t, err) + assert.Equal(t, ErrBadSession.Error(), body.Error) + }, + } + } + + testCases(t, addr, client, []TestCase{ + makeCase(http.MethodGet, "/api/user", nil), + makeCase(http.MethodPost, "/api/change-password", strings.NewReader(`{"old_password":"old","new_password":"new"}`)), + makeCase(http.MethodGet, "/api/nodes", nil), + }) + }) + + t.Run("only_admin_account_allowed", func(t *testing.T) { + addr, client, stop := startNode(MockConfig{5, 10, 10}) + defer stop() + + testCases(t, addr, client, []TestCase{ + { + Method: http.MethodPost, + URI: "/api/create-account", + Body: strings.NewReader(`{"username":"invalid_user","password":"Secure1234"}`), + RespStatus: http.StatusForbidden, + RespBody: func(t *testing.T, r *http.Response) { + body, err := decodeErrorBody(r.Body) + assert.NoError(t, err) + assert.Equal(t, ErrUserNotCreated.Error(), body.Error) + }, + }, + { + Method: http.MethodPost, + URI: "/api/create-account", + Body: strings.NewReader(`{"username":"admin","password":"Secure1234"}`), + RespStatus: http.StatusOK, + RespBody: func(t *testing.T, r *http.Response) { + var ok bool + assert.NoError(t, json.NewDecoder(r.Body).Decode(&ok)) + assert.True(t, ok) + }, + }, + }) + }) + + t.Run("cannot_login_twice", func(t *testing.T) { + addr, client, stop := startNode(MockConfig{5, 10, 10}) + defer stop() + + testCases(t, addr, client, []TestCase{ + { + Method: http.MethodPost, + URI: "/api/create-account", + Body: strings.NewReader(`{"username":"admin","password":"Secure1234"}`), + RespStatus: http.StatusOK, + RespBody: func(t *testing.T, r *http.Response) { + var ok bool + assert.NoError(t, json.NewDecoder(r.Body).Decode(&ok)) + assert.True(t, ok) + }, + }, + { + Method: http.MethodPost, + URI: "/api/login", + Body: strings.NewReader(`{"username":"admin","password":"Secure1234"}`), + RespStatus: http.StatusOK, + RespBody: func(t *testing.T, r *http.Response) { + var ok bool + assert.NoError(t, json.NewDecoder(r.Body).Decode(&ok)) + assert.True(t, ok) + }, + }, + { + Method: http.MethodPost, + URI: "/api/login", + Body: strings.NewReader(`{"username":"admin","password":"Secure1234"}`), + RespStatus: http.StatusForbidden, + RespBody: func(t *testing.T, r *http.Response) { + body, err := decodeErrorBody(r.Body) + assert.NoError(t, err) + assert.Equal(t, ErrNotLoggedOut.Error(), body.Error) + }, + }, + }) + }) + + t.Run("access_after_login", func(t *testing.T) { + addr, client, stop := startNode(MockConfig{5, 10, 10}) + defer stop() + + testCases(t, addr, client, []TestCase{ + { + Method: http.MethodPost, + URI: "/api/create-account", + Body: strings.NewReader(`{"username":"admin","password":"Secure1234"}`), + RespStatus: http.StatusOK, + RespBody: func(t *testing.T, r *http.Response) { + var ok bool + assert.NoError(t, json.NewDecoder(r.Body).Decode(&ok)) + assert.True(t, ok) + }, + }, + { + Method: http.MethodPost, + URI: "/api/login", + Body: strings.NewReader(`{"username":"admin","password":"Secure1234"}`), + RespStatus: http.StatusOK, + RespBody: func(t *testing.T, r *http.Response) { + fmt.Println(r.Cookies()) + var ok bool + assert.NoError(t, json.NewDecoder(r.Body).Decode(&ok)) + assert.True(t, ok) + }, + }, + { + Method: http.MethodGet, + URI: "/api/user", + RespStatus: http.StatusOK, + }, + { + Method: http.MethodGet, + URI: "/api/nodes", + RespStatus: http.StatusOK, + }, + }) + }) + + t.Run("no_access_after_logout", func(t *testing.T) { + addr, client, stop := startNode(MockConfig{5, 10, 10}) + defer stop() + + testCases(t, addr, client, []TestCase{ + { + Method: http.MethodPost, + URI: "/api/create-account", + Body: strings.NewReader(`{"username":"admin","password":"Secure1234"}`), + RespStatus: http.StatusOK, + RespBody: func(t *testing.T, r *http.Response) { + var ok bool + assert.NoError(t, json.NewDecoder(r.Body).Decode(&ok)) + assert.True(t, ok) + }, + }, + { + Method: http.MethodPost, + URI: "/api/login", + Body: strings.NewReader(`{"username":"admin","password":"Secure1234"}`), + RespStatus: http.StatusOK, + RespBody: func(t *testing.T, r *http.Response) { + fmt.Println(r.Cookies()) + var ok bool + assert.NoError(t, json.NewDecoder(r.Body).Decode(&ok)) + assert.True(t, ok) + }, + }, + { + Method: http.MethodPost, + URI: "/api/logout", + RespStatus: http.StatusOK, + RespBody: func(t *testing.T, r *http.Response) { + fmt.Println(r.Cookies()) + var ok bool + assert.NoError(t, json.NewDecoder(r.Body).Decode(&ok)) + assert.True(t, ok) + }, + }, + { + Method: http.MethodGet, + URI: "/api/user", + RespStatus: http.StatusUnauthorized, + RespBody: func(t *testing.T, r *http.Response) { + body, err := decodeErrorBody(r.Body) + assert.NoError(t, err) + assert.Equal(t, ErrBadSession.Error(), body.Error) + }, + }, + { + Method: http.MethodGet, + URI: "/api/nodes", + RespStatus: http.StatusUnauthorized, + RespBody: func(t *testing.T, r *http.Response) { + body, err := decodeErrorBody(r.Body) + assert.NoError(t, err) + assert.Equal(t, ErrBadSession.Error(), body.Error) + }, + }, + }) + }) + + t.Run("change_password", func(t *testing.T) { + // TODO: + // - Create account. + // - Login. + // - Change Password. + // - Logout. + // - Login with old password (should fail). + // - Login with new password (should succeed). + }) +} + +type ErrorBody struct { + Error string `json:"error"` +} + +func decodeErrorBody(rb io.Reader) (*ErrorBody, error) { + b := new(ErrorBody) + dec := json.NewDecoder(rb) + dec.DisallowUnknownFields() + return b, dec.Decode(b) +} diff --git a/pkg/manager/user.go b/pkg/manager/user.go index 7fe686b5da..cf4d1f37dc 100644 --- a/pkg/manager/user.go +++ b/pkg/manager/user.go @@ -28,27 +28,17 @@ type User struct { PwHash cipher.SHA256 } -func (u *User) SetName(pattern, name string) bool { - if pattern != "" { - ok, err := regexp.MatchString(pattern, name) - catch(err, "invalid username regex:") - if !ok { - return false - } +func (u *User) SetName(name string) bool { + if !UsernameFormatOkay(name) { + return false } u.Name = name return true } -func (u *User) SetPassword(saltLen int, pattern, password string) bool { - if pattern != "" { - ok, err := regexp.MatchString(pattern, password) - if err != nil { - catch(err, "invalid password regex:") - } - if !ok { - return false - } +func (u *User) SetPassword(saltLen int, password string) bool { + if !PasswordFormatOkay(password) { + return false } u.PwSalt = cipher.RandByte(saltLen) u.PwHash = cipher.SumSHA256(append([]byte(password), u.PwSalt...)) @@ -190,3 +180,15 @@ func (s *SingleUserStore) RemoveUser(name string) { func (s *SingleUserStore) allowName(name string) bool { return name == s.username } + +func UsernameFormatOkay(name string) bool { + return regexp.MustCompile(`^[a-z0-9_-]{4,21}$`).MatchString(name) +} + +func PasswordFormatOkay(pass string) bool { + if len(pass) < 6 || len(pass) > 64 { + return false + } + // TODO: implement more advanced password checking. + return true +} diff --git a/pkg/manager/user_manager.go b/pkg/manager/user_manager.go index 4df1fa5363..8c658b0d4e 100644 --- a/pkg/manager/user_manager.go +++ b/pkg/manager/user_manager.go @@ -3,7 +3,6 @@ package manager import ( "context" "errors" - "fmt" "net/http" "sync" "time" @@ -15,7 +14,17 @@ import ( ) const ( - sessionCookieName = "swm_session" + sessionCookieName = "swm-session" +) + +var ( + ErrBadBody = errors.New("ill-formatted request body") + ErrNotLoggedOut = errors.New("not logged out") + ErrBadLogin = errors.New("incorrect username or password") + ErrBadSession = errors.New("session cookie is either non-existent, expired, or ill-formatted") + ErrBadUsernameFormat = errors.New("format of 'username' is not accepted") + ErrBadPasswordFormat = errors.New("format of 'password' is not accepted") + ErrUserNotCreated = errors.New("failed to create new user: username is either already taken, or unaccepted") ) type Session struct { @@ -44,8 +53,8 @@ func NewUserManager(users UserStore, config CookieConfig) *UserManager { func (s *UserManager) Login() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - if _, err := r.Cookie(sessionCookieName); err != http.ErrNoCookie { - httputil.WriteJSON(w, r, http.StatusForbidden, errors.New("not logged out")) + if _, _, ok := s.session(r); ok { + httputil.WriteJSON(w, r, http.StatusForbidden, ErrNotLoggedOut) return } var rb struct { @@ -53,18 +62,19 @@ func (s *UserManager) Login() http.HandlerFunc { Password string `json:"password"` } if err := httputil.ReadJSON(r, &rb); err != nil { - httputil.WriteJSON(w, r, http.StatusBadRequest, errors.New("cannot read request body")) + httputil.WriteJSON(w, r, http.StatusBadRequest, ErrBadBody) return } user, ok := s.db.User(rb.Username) if !ok || !user.VerifyPassword(rb.Password) { - httputil.WriteJSON(w, r, http.StatusUnauthorized, errors.New("incorrect username or password")) + httputil.WriteJSON(w, r, http.StatusUnauthorized, ErrBadLogin) return } s.newSession(w, Session{ User: rb.Username, Expiry: time.Now().Add(time.Hour * 12), // TODO: Set default expiry. }) + //http.SetCookie() httputil.WriteJSON(w, r, http.StatusOK, ok) } } @@ -81,9 +91,9 @@ func (s *UserManager) Logout() http.HandlerFunc { func (s *UserManager) Authorize(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - user, session, err := s.session(r) - if err != nil { - httputil.WriteJSON(w, r, http.StatusUnauthorized, err) + user, session, ok := s.session(r) + if !ok { + httputil.WriteJSON(w, r, http.StatusUnauthorized, ErrBadSession) return } ctx := r.Context() @@ -93,7 +103,7 @@ func (s *UserManager) Authorize(next http.Handler) http.Handler { }) } -func (s *UserManager) ChangePassword(pwSaltLen int, pwPattern string) http.HandlerFunc { +func (s *UserManager) ChangePassword(pwSaltLen int) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var ( user = r.Context().Value("user").(User) @@ -110,7 +120,7 @@ func (s *UserManager) ChangePassword(pwSaltLen int, pwPattern string) http.Handl httputil.WriteJSON(w, r, http.StatusUnauthorized, errors.New("unauthorised")) return } - if ok := user.SetPassword(pwSaltLen, pwPattern, rb.NewPassword); !ok { + if ok := user.SetPassword(pwSaltLen, rb.NewPassword); !ok { httputil.WriteJSON(w, r, http.StatusBadRequest, errors.New("format of 'new_password' is not accepted")) return } @@ -118,7 +128,7 @@ func (s *UserManager) ChangePassword(pwSaltLen int, pwPattern string) http.Handl } } -func (s *UserManager) CreateAccount(pwSaltLen int, pwPattern, unPattern string) http.HandlerFunc { +func (s *UserManager) CreateAccount(pwSaltLen int) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var rb struct { Username string `json:"username"` @@ -129,16 +139,16 @@ func (s *UserManager) CreateAccount(pwSaltLen int, pwPattern, unPattern string) return } var user User - if ok := user.SetName(unPattern, rb.Username); !ok { - httputil.WriteJSON(w, r, http.StatusBadRequest, errors.New("format of 'username' is not accepted")) + if ok := user.SetName(rb.Username); !ok { + httputil.WriteJSON(w, r, http.StatusBadRequest, ErrBadUsernameFormat) return } - if ok := user.SetPassword(pwSaltLen, pwPattern, rb.Password); !ok { - httputil.WriteJSON(w, r, http.StatusBadRequest, errors.New("format of 'password' is not accepted")) + if ok := user.SetPassword(pwSaltLen, rb.Password); !ok { + httputil.WriteJSON(w, r, http.StatusBadRequest, ErrBadPasswordFormat) return } if ok := s.db.AddUser(user); !ok { - httputil.WriteJSON(w, r, http.StatusForbidden, fmt.Errorf("failed to create user of username '%s'", user.Name)) + httputil.WriteJSON(w, r, http.StatusForbidden, ErrUserNotCreated) return } httputil.WriteJSON(w, r, http.StatusOK, true) @@ -212,32 +222,33 @@ func (s *UserManager) delSession(w http.ResponseWriter, r *http.Request) error { return nil } -func (s *UserManager) session(r *http.Request) (User, Session, error) { +func (s *UserManager) session(r *http.Request) (User, Session, bool) { cookie, err := r.Cookie(sessionCookieName) if err != nil { - return User{}, Session{}, err + return User{}, Session{}, false } var sid uuid.UUID if err := s.crypto.Decode(sessionCookieName, cookie.Value, &sid); err != nil { - return User{}, Session{}, err + log.WithError(err).Warn("failed to decode session cookie value") + return User{}, Session{}, false } s.mu.RLock() session, ok := s.sessions[sid] s.mu.RUnlock() if !ok { - return User{}, Session{}, errors.New("invalid session") + return User{}, Session{}, false } user, ok := s.db.User(session.User) if !ok { - return User{}, Session{}, errors.New("invalid session") + return User{}, Session{}, false } if time.Now().After(session.Expiry) { s.mu.Lock() delete(s.sessions, sid) s.mu.Unlock() - return User{}, Session{}, errors.New("invalid session") + return User{}, Session{}, false } - return user, session, nil + return user, session, true } // TODO: getSessionCookie function. From a47d9595aa03eea6d3a47acf32d1998105efabf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E5=BF=97=E5=AE=87?= Date: Thu, 28 Mar 2019 14:18:46 +0800 Subject: [PATCH 13/18] added change-password test for manager --- pkg/manager/node.go | 2 +- pkg/manager/node_test.go | 78 +++++++++++++++++++++++++++++++++---- pkg/manager/user_manager.go | 13 ++++--- 3 files changed, 80 insertions(+), 13 deletions(-) diff --git a/pkg/manager/node.go b/pkg/manager/node.go index 956192b4bc..2b30d08ad4 100644 --- a/pkg/manager/node.go +++ b/pkg/manager/node.go @@ -269,7 +269,7 @@ func (m *Node) postTransport() http.HandlerFunc { httputil.WriteJSON(w, r, http.StatusBadRequest, err) return } - summary, err := ctx.RPC.AddTransport(reqBody.Remote, reqBody.TpType, reqBody.Public, 30*time.Second) // TODO(evanlinjin): add timeout + summary, err := ctx.RPC.AddTransport(reqBody.Remote, reqBody.TpType, reqBody.Public, 30*time.Second) if err != nil { httputil.WriteJSON(w, r, http.StatusInternalServerError, err) return diff --git a/pkg/manager/node_test.go b/pkg/manager/node_test.go index 9e43fe6553..ef5e02ac07 100644 --- a/pkg/manager/node_test.go +++ b/pkg/manager/node_test.go @@ -17,9 +17,6 @@ import ( "github.com/stretchr/testify/require" ) -// TODO(evanlinjin): tests to write: -// - no access to any endpoint without login / signup. - func TestNewNode(t *testing.T) { config := makeConfig() @@ -189,7 +186,6 @@ func TestNewNode(t *testing.T) { Body: strings.NewReader(`{"username":"admin","password":"Secure1234"}`), RespStatus: http.StatusOK, RespBody: func(t *testing.T, r *http.Response) { - fmt.Println(r.Cookies()) var ok bool assert.NoError(t, json.NewDecoder(r.Body).Decode(&ok)) assert.True(t, ok) @@ -230,7 +226,6 @@ func TestNewNode(t *testing.T) { Body: strings.NewReader(`{"username":"admin","password":"Secure1234"}`), RespStatus: http.StatusOK, RespBody: func(t *testing.T, r *http.Response) { - fmt.Println(r.Cookies()) var ok bool assert.NoError(t, json.NewDecoder(r.Body).Decode(&ok)) assert.True(t, ok) @@ -241,7 +236,6 @@ func TestNewNode(t *testing.T) { URI: "/api/logout", RespStatus: http.StatusOK, RespBody: func(t *testing.T, r *http.Response) { - fmt.Println(r.Cookies()) var ok bool assert.NoError(t, json.NewDecoder(r.Body).Decode(&ok)) assert.True(t, ok) @@ -271,13 +265,83 @@ func TestNewNode(t *testing.T) { }) t.Run("change_password", func(t *testing.T) { - // TODO: // - Create account. // - Login. // - Change Password. // - Logout. // - Login with old password (should fail). // - Login with new password (should succeed). + + addr, client, stop := startNode(MockConfig{5, 10, 10}) + defer stop() + + testCases(t, addr, client, []TestCase{ + { + Method: http.MethodPost, + URI: "/api/create-account", + Body: strings.NewReader(`{"username":"admin","password":"Secure1234"}`), + RespStatus: http.StatusOK, + RespBody: func(t *testing.T, r *http.Response) { + var ok bool + assert.NoError(t, json.NewDecoder(r.Body).Decode(&ok)) + assert.True(t, ok) + }, + }, + { + Method: http.MethodPost, + URI: "/api/login", + Body: strings.NewReader(`{"username":"admin","password":"Secure1234"}`), + RespStatus: http.StatusOK, + RespBody: func(t *testing.T, r *http.Response) { + var ok bool + assert.NoError(t, json.NewDecoder(r.Body).Decode(&ok)) + assert.True(t, ok) + }, + }, + { + Method: http.MethodPost, + URI: "/api/change-password", + Body: strings.NewReader(`{"old_password":"Secure1234","new_password":"NewSecure1234"}`), + RespStatus: http.StatusOK, + RespBody: func(t *testing.T, r *http.Response) { + var ok bool + assert.NoError(t, json.NewDecoder(r.Body).Decode(&ok)) + assert.True(t, ok) + }, + }, + { + Method: http.MethodPost, + URI: "/api/logout", + RespStatus: http.StatusOK, + RespBody: func(t *testing.T, r *http.Response) { + var ok bool + assert.NoError(t, json.NewDecoder(r.Body).Decode(&ok)) + assert.True(t, ok) + }, + }, + { + Method: http.MethodPost, + URI: "/api/login", + Body: strings.NewReader(`{"username":"admin","password":"Secure1234"}`), + RespStatus: http.StatusUnauthorized, + RespBody: func(t *testing.T, r *http.Response) { + b, err := decodeErrorBody(r.Body) + assert.NoError(t, err) + require.Equal(t, ErrBadLogin.Error(), b.Error) + }, + }, + { + Method: http.MethodPost, + URI: "/api/login", + Body: strings.NewReader(`{"username":"admin","password":"NewSecure1234"}`), + RespStatus: http.StatusOK, + RespBody: func(t *testing.T, r *http.Response) { + var ok bool + assert.NoError(t, json.NewDecoder(r.Body).Decode(&ok)) + assert.True(t, ok) + }, + }, + }) }) } diff --git a/pkg/manager/user_manager.go b/pkg/manager/user_manager.go index 8c658b0d4e..f8d612f110 100644 --- a/pkg/manager/user_manager.go +++ b/pkg/manager/user_manager.go @@ -25,6 +25,7 @@ var ( ErrBadUsernameFormat = errors.New("format of 'username' is not accepted") ErrBadPasswordFormat = errors.New("format of 'password' is not accepted") ErrUserNotCreated = errors.New("failed to create new user: username is either already taken, or unaccepted") + ErrUserNotFound = errors.New("user is either deleted or not found") ) type Session struct { @@ -72,7 +73,7 @@ func (s *UserManager) Login() http.HandlerFunc { } s.newSession(w, Session{ User: rb.Username, - Expiry: time.Now().Add(time.Hour * 12), // TODO: Set default expiry. + Expiry: time.Now().Add(s.c.ExpiresDuration), }) //http.SetCookie() httputil.WriteJSON(w, r, http.StatusOK, ok) @@ -117,11 +118,15 @@ func (s *UserManager) ChangePassword(pwSaltLen int) http.HandlerFunc { return } if ok := user.VerifyPassword(rb.OldPassword); !ok { - httputil.WriteJSON(w, r, http.StatusUnauthorized, errors.New("unauthorised")) + httputil.WriteJSON(w, r, http.StatusUnauthorized, ErrBadLogin) return } if ok := user.SetPassword(pwSaltLen, rb.NewPassword); !ok { - httputil.WriteJSON(w, r, http.StatusBadRequest, errors.New("format of 'new_password' is not accepted")) + httputil.WriteJSON(w, r, http.StatusBadRequest, ErrBadPasswordFormat) + return + } + if ok := s.db.SetUser(user); !ok { + httputil.WriteJSON(w, r, http.StatusForbidden, ErrUserNotFound) return } httputil.WriteJSON(w, r, http.StatusOK, true) @@ -250,5 +255,3 @@ func (s *UserManager) session(r *http.Request) (User, Session, bool) { } return user, session, true } - -// TODO: getSessionCookie function. From 96946ebfaefd3a10a327ff6713b62fc0afb3c1a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E5=BF=97=E5=AE=87?= Date: Thu, 28 Mar 2019 18:14:25 +0800 Subject: [PATCH 14/18] added comments --- pkg/manager/config.go | 35 +++++++++++++++++++++----------- pkg/manager/node.go | 16 +++++++-------- pkg/manager/user.go | 26 ++++++++++++++++++++++-- pkg/manager/user_manager.go | 40 +++++++++++++++++++++++++++---------- 4 files changed, 84 insertions(+), 33 deletions(-) diff --git a/pkg/manager/config.go b/pkg/manager/config.go index 1999b06042..062b70f647 100644 --- a/pkg/manager/config.go +++ b/pkg/manager/config.go @@ -13,29 +13,33 @@ import ( "github.com/skycoin/skywire/pkg/cipher" ) +// Key allows a byte slice to be marshaled or unmarshaled from a hex string. type Key []byte +// String implements fmt.Stringer func (hk Key) String() string { return hex.EncodeToString(hk) } +// MarshalText implements encoding.TextMarshaler func (hk Key) MarshalText() ([]byte, error) { return []byte(hk.String()), nil } +// UnmarshalText implements encoding.TextUnmarshaler func (hk *Key) UnmarshalText(text []byte) error { *hk = make([]byte, hex.DecodedLen(len(text))) _, err := hex.Decode(*hk, text) return err } +// Config configures the manager node. type Config struct { - PK cipher.PubKey `json:"public_key"` - SK cipher.SecKey `json:"secret_key"` - DBPath string `json:"db_path"` - PassSaltLen int `json:"password_salt_len"` // Salt Len for password verification data. - Cookies CookieConfig `json:"cookies"` - Interfaces InterfaceConfig `json:"interfaces"` + PK cipher.PubKey `json:"public_key"` + SK cipher.SecKey `json:"secret_key"` + DBPath string `json:"db_path"` // Path to store database file. + Cookies CookieConfig `json:"cookies"` // Configures cookies (for session management). + Interfaces InterfaceConfig `json:"interfaces"` // Configures exposed interfaces. } func makeConfig() Config { @@ -49,24 +53,27 @@ func makeConfig() Config { return c } +// GenerateHomeConfig generates a config with default values and uses db from user's home folder. func GenerateHomeConfig() Config { c := makeConfig() c.DBPath = filepath.Join(pathutil.HomeDir(), ".skycoin/skywire-manager/users.db") return c } +// GenerateLocalConfig generates a config with default values and uses db from shared folder. func GenerateLocalConfig() Config { c := makeConfig() c.DBPath = "/usr/local/skycoin/skywire-manager/users.db" return c } +// FillDefaults fills the config with default values. func (c *Config) FillDefaults() { - c.PassSaltLen = 16 c.Cookies.FillDefaults() c.Interfaces.FillDefaults() } +// Parse parses the file in path, and decodes to the config. func (c *Config) Parse(path string) error { var err error if path, err = filepath.Abs(path); err != nil { @@ -80,32 +87,36 @@ func (c *Config) Parse(path string) error { return json.NewDecoder(f).Decode(c) } +// CookieConfig configures cookies used for manager. type CookieConfig struct { - HashKey Key `json:"hash_key"` // 32 or 64 bytes. - BlockKey Key `json:"block_key"` // 16 (AES-128), 24 (AES-192), 32 (AES-256) bytes. (optional) + HashKey Key `json:"hash_key"` // Signs the cookie: 32 or 64 bytes. + BlockKey Key `json:"block_key"` // Encrypts the cookie: 16 (AES-128), 24 (AES-192), 32 (AES-256) bytes. (optional) - ExpiresDuration time.Duration `json:"expires_duration"` + ExpiresDuration time.Duration `json:"expires_duration"` // Used for determining the 'expires' value for cookies. Path string `json:"path"` // optional Domain string `json:"domain"` // optional Secure bool `json:"secure"` - HttpOnly bool `json:"http_only"` + HTTPOnly bool `json:"http_only"` SameSite http.SameSite `json:"same_site"` } +// FillDefaults fills config with default values. func (c *CookieConfig) FillDefaults() { c.ExpiresDuration = time.Hour * 12 c.Path = "/" c.Secure = true - c.HttpOnly = true + c.HTTPOnly = true c.SameSite = http.SameSiteDefaultMode } +// InterfaceConfig configures the interfaces exposed by manager. type InterfaceConfig struct { HTTPAddr string `json:"http_address"` RPCAddr string `json:"rpc_addr"` } +// FillDefaults fills config with default values. func (c *InterfaceConfig) FillDefaults() { c.HTTPAddr = ":8080" c.RPCAddr = ":7080" diff --git a/pkg/manager/node.go b/pkg/manager/node.go index 2b30d08ad4..e48358873e 100644 --- a/pkg/manager/node.go +++ b/pkg/manager/node.go @@ -88,16 +88,16 @@ func (m *Node) AddMockData(config MockConfig) error { } // ServeHTTP implements http.Handler -func (m *Node) ServeHTTP(w http.ResponseWriter, r *http.Request) { - mux := chi.NewRouter() +func (m *Node) ServeHTTP(w http.ResponseWriter, req *http.Request) { + r := chi.NewRouter() - mux.Use(middleware.Timeout(time.Second * 30)) - mux.Use(middleware.Logger) + r.Use(middleware.Timeout(time.Second * 30)) + r.Use(middleware.Logger) - mux.Route("/api", func(r chi.Router) { + r.Route("/api", func(r chi.Router) { r.Group(func(r chi.Router) { - r.Post("/create-account", m.users.CreateAccount(m.c.PassSaltLen)) + r.Post("/create-account", m.users.CreateAccount()) r.Post("/login", m.users.Login()) r.Post("/logout", m.users.Logout()) }) @@ -106,7 +106,7 @@ func (m *Node) ServeHTTP(w http.ResponseWriter, r *http.Request) { r.Use(m.users.Authorize) r.Get("/user", m.users.UserInfo()) - r.Post("/change-password", m.users.ChangePassword(m.c.PassSaltLen)) + r.Post("/change-password", m.users.ChangePassword()) r.Get("/nodes", m.getNodes()) r.Get("/nodes/{pk}", m.getNode()) @@ -130,7 +130,7 @@ func (m *Node) ServeHTTP(w http.ResponseWriter, r *http.Request) { }) }) - mux.ServeHTTP(w, r) + r.ServeHTTP(w, req) } // provides summary of all nodes. diff --git a/pkg/manager/user.go b/pkg/manager/user.go index cf4d1f37dc..0a6791a8bf 100644 --- a/pkg/manager/user.go +++ b/pkg/manager/user.go @@ -16,18 +16,21 @@ import ( const ( boltTimeout = 10 * time.Second boltUserBucketName = "users" + passwordSaltLen = 16 ) func init() { gob.Register(User{}) } +// User represents a user of the manager. type User struct { Name string PwSalt []byte PwHash cipher.SHA256 } +// SetName checks the provided name, and sets the name if format is valid. func (u *User) SetName(name string) bool { if !UsernameFormatOkay(name) { return false @@ -36,19 +39,22 @@ func (u *User) SetName(name string) bool { return true } -func (u *User) SetPassword(saltLen int, password string) bool { +// SetPassword checks the provided password, and sets the password if format is valid. +func (u *User) SetPassword(password string) bool { if !PasswordFormatOkay(password) { return false } - u.PwSalt = cipher.RandByte(saltLen) + u.PwSalt = cipher.RandByte(passwordSaltLen) u.PwHash = cipher.SumSHA256(append([]byte(password), u.PwSalt...)) return true } +// VerifyPassword verifies the password input with hash and salt. func (u *User) VerifyPassword(password string) bool { return cipher.SumSHA256(append([]byte(password), u.PwSalt...)) == u.PwHash } +// Encode encodes the user to bytes. func (u *User) Encode() []byte { var buf bytes.Buffer if err := gob.NewEncoder(&buf).Encode(u); err != nil { @@ -57,6 +63,7 @@ func (u *User) Encode() []byte { return buf.Bytes() } +// DecodeUser decodes the user from bytes. func DecodeUser(raw []byte) User { var user User if err := gob.NewDecoder(bytes.NewReader(raw)).Decode(&user); err != nil { @@ -65,6 +72,7 @@ func DecodeUser(raw []byte) User { return user } +// UserStore stores users. type UserStore interface { User(name string) (User, bool) AddUser(user User) bool @@ -72,10 +80,12 @@ type UserStore interface { RemoveUser(name string) } +// BoltUserStore implements UserStore, storing users in a bbolt database file. type BoltUserStore struct { *bbolt.DB } +// NewBoltUserStore creates a new BoltUserStore. func NewBoltUserStore(path string) (*BoltUserStore, error) { if err := os.MkdirAll(filepath.Dir(path), os.FileMode(0700)); err != nil { return nil, err @@ -91,6 +101,7 @@ func NewBoltUserStore(path string) (*BoltUserStore, error) { return &BoltUserStore{DB: db}, err } +// User obtains a single user. Returns true if user exists. func (s *BoltUserStore) User(name string) (user User, ok bool) { catch(s.View(func(tx *bbolt.Tx) error { users := tx.Bucket([]byte(boltUserBucketName)) @@ -106,6 +117,7 @@ func (s *BoltUserStore) User(name string) (user User, ok bool) { return user, ok } +// AddUser adds a new user; ok is true when successful. func (s *BoltUserStore) AddUser(user User) (ok bool) { catch(s.Update(func(tx *bbolt.Tx) error { users := tx.Bucket([]byte(boltUserBucketName)) @@ -119,6 +131,7 @@ func (s *BoltUserStore) AddUser(user User) (ok bool) { return ok } +// SetUser changes an existing user. Returns true on success. func (s *BoltUserStore) SetUser(user User) (ok bool) { catch(s.Update(func(tx *bbolt.Tx) error { users := tx.Bucket([]byte(boltUserBucketName)) @@ -132,17 +145,20 @@ func (s *BoltUserStore) SetUser(user User) (ok bool) { return ok } +// RemoveUser removes a user of given username. func (s *BoltUserStore) RemoveUser(name string) { catch(s.Update(func(tx *bbolt.Tx) error { return tx.Bucket([]byte(boltUserBucketName)).Delete([]byte(name)) })) } +// SingleUserStore implements UserStore while enforcing only having a single user. type SingleUserStore struct { username string UserStore } +// NewSingleUserStore creates a new SingleUserStore with provided username and UserStore. func NewSingleUserStore(username string, users UserStore) *SingleUserStore { return &SingleUserStore{ username: username, @@ -150,6 +166,7 @@ func NewSingleUserStore(username string, users UserStore) *SingleUserStore { } } +// User gets a user. func (s *SingleUserStore) User(name string) (User, bool) { if s.allowName(name) { return s.UserStore.User(name) @@ -157,6 +174,7 @@ func (s *SingleUserStore) User(name string) (User, bool) { return User{}, false } +// AddUser adds a new user. func (s *SingleUserStore) AddUser(user User) bool { if s.allowName(user.Name) { return s.UserStore.AddUser(user) @@ -164,6 +182,7 @@ func (s *SingleUserStore) AddUser(user User) bool { return false } +// SetUser sets an existing user. func (s *SingleUserStore) SetUser(user User) bool { if s.allowName(user.Name) { return s.UserStore.SetUser(user) @@ -171,6 +190,7 @@ func (s *SingleUserStore) SetUser(user User) bool { return false } +// RemoveUser removes a user. func (s *SingleUserStore) RemoveUser(name string) { if s.allowName(name) { s.UserStore.RemoveUser(name) @@ -181,10 +201,12 @@ func (s *SingleUserStore) allowName(name string) bool { return name == s.username } +// UsernameFormatOkay checks if the username format is valid. func UsernameFormatOkay(name string) bool { return regexp.MustCompile(`^[a-z0-9_-]{4,21}$`).MatchString(name) } +// PasswordFormatOkay checks if the password format is valid. func PasswordFormatOkay(pass string) bool { if len(pass) < 6 || len(pass) > 64 { return false diff --git a/pkg/manager/user_manager.go b/pkg/manager/user_manager.go index f8d612f110..74a72c2a8f 100644 --- a/pkg/manager/user_manager.go +++ b/pkg/manager/user_manager.go @@ -17,6 +17,7 @@ const ( sessionCookieName = "swm-session" ) +// Errors associated with user management. var ( ErrBadBody = errors.New("ill-formatted request body") ErrNotLoggedOut = errors.New("not logged out") @@ -28,12 +29,22 @@ var ( ErrUserNotFound = errors.New("user is either deleted or not found") ) +// for use with context.Context +type ctxKey string + +const ( + userKey = ctxKey("user") + sessionKey = ctxKey("session") +) + +// Session represents a user session. type Session struct { SID uuid.UUID `json:"sid"` User string `json:"username"` Expiry time.Time `json:"expiry"` } +// UserManager manages the users and sessions. type UserManager struct { c CookieConfig db UserStore @@ -42,6 +53,7 @@ type UserManager struct { mu *sync.RWMutex } +// NewUserManager creates a new UserManager. func NewUserManager(users UserStore, config CookieConfig) *UserManager { return &UserManager{ db: users, @@ -52,6 +64,7 @@ func NewUserManager(users UserStore, config CookieConfig) *UserManager { } } +// Login returns a HandlerFunc for login operations. func (s *UserManager) Login() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if _, _, ok := s.session(r); ok { @@ -80,6 +93,7 @@ func (s *UserManager) Login() http.HandlerFunc { } } +// Logout returns a HandlerFunc of logout operations. func (s *UserManager) Logout() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if err := s.delSession(w, r); err != nil { @@ -90,6 +104,7 @@ func (s *UserManager) Logout() http.HandlerFunc { } } +// Authorize is an http middleware for authorizing requests. func (s *UserManager) Authorize(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user, session, ok := s.session(r) @@ -98,16 +113,17 @@ func (s *UserManager) Authorize(next http.Handler) http.Handler { return } ctx := r.Context() - ctx = context.WithValue(ctx, "user", user) - ctx = context.WithValue(ctx, "session", session) + ctx = context.WithValue(ctx, userKey, user) + ctx = context.WithValue(ctx, sessionKey, session) next.ServeHTTP(w, r.WithContext(ctx)) }) } -func (s *UserManager) ChangePassword(pwSaltLen int) http.HandlerFunc { +// ChangePassword returns a HandlerFunc for changing the user's password. +func (s *UserManager) ChangePassword() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var ( - user = r.Context().Value("user").(User) + user = r.Context().Value(userKey).(User) ) var rb struct { OldPassword string `json:"old_password"` @@ -121,7 +137,7 @@ func (s *UserManager) ChangePassword(pwSaltLen int) http.HandlerFunc { httputil.WriteJSON(w, r, http.StatusUnauthorized, ErrBadLogin) return } - if ok := user.SetPassword(pwSaltLen, rb.NewPassword); !ok { + if ok := user.SetPassword(rb.NewPassword); !ok { httputil.WriteJSON(w, r, http.StatusBadRequest, ErrBadPasswordFormat) return } @@ -133,7 +149,8 @@ func (s *UserManager) ChangePassword(pwSaltLen int) http.HandlerFunc { } } -func (s *UserManager) CreateAccount(pwSaltLen int) http.HandlerFunc { +// CreateAccount returns a HandlerFunc for account creation. +func (s *UserManager) CreateAccount() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var rb struct { Username string `json:"username"` @@ -148,7 +165,7 @@ func (s *UserManager) CreateAccount(pwSaltLen int) http.HandlerFunc { httputil.WriteJSON(w, r, http.StatusBadRequest, ErrBadUsernameFormat) return } - if ok := user.SetPassword(pwSaltLen, rb.Password); !ok { + if ok := user.SetPassword(rb.Password); !ok { httputil.WriteJSON(w, r, http.StatusBadRequest, ErrBadPasswordFormat) return } @@ -160,11 +177,12 @@ func (s *UserManager) CreateAccount(pwSaltLen int) http.HandlerFunc { } } +// UserInfo returns a HandlerFunc for obtaining user info. func (s *UserManager) UserInfo() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var ( - user = r.Context().Value("user").(User) - session = r.Context().Value("session").(Session) + user = r.Context().Value(userKey).(User) + session = r.Context().Value(sessionKey).(Session) ) var otherSessions []Session s.mu.RLock() @@ -199,7 +217,7 @@ func (s *UserManager) newSession(w http.ResponseWriter, session Session) { Domain: s.c.Domain, Expires: time.Now().Add(s.c.ExpiresDuration), Secure: s.c.Secure, - HttpOnly: s.c.HttpOnly, + HttpOnly: s.c.HTTPOnly, SameSite: s.c.SameSite, }) } @@ -221,7 +239,7 @@ func (s *UserManager) delSession(w http.ResponseWriter, r *http.Request) error { Domain: s.c.Domain, MaxAge: -1, Secure: s.c.Secure, - HttpOnly: s.c.HttpOnly, + HttpOnly: s.c.HTTPOnly, SameSite: s.c.SameSite, }) return nil From c1c6ee4a48f0065ab51df4136a44cf0dd8b7d032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E5=BF=97=E5=AE=87?= Date: Thu, 28 Mar 2019 21:43:12 +0800 Subject: [PATCH 15/18] manager http refactor --- pkg/manager/node.go | 237 ++++++++++++++++++++++---------------------- pkg/manager/user.go | 2 +- 2 files changed, 119 insertions(+), 120 deletions(-) diff --git a/pkg/manager/node.go b/pkg/manager/node.go index e48358873e..2763b9d54b 100644 --- a/pkg/manager/node.go +++ b/pkg/manager/node.go @@ -90,38 +90,28 @@ func (m *Node) AddMockData(config MockConfig) error { // ServeHTTP implements http.Handler func (m *Node) ServeHTTP(w http.ResponseWriter, req *http.Request) { r := chi.NewRouter() - r.Use(middleware.Timeout(time.Second * 30)) r.Use(middleware.Logger) - r.Route("/api", func(r chi.Router) { - r.Group(func(r chi.Router) { r.Post("/create-account", m.users.CreateAccount()) r.Post("/login", m.users.Login()) r.Post("/logout", m.users.Logout()) }) - r.Group(func(r chi.Router) { r.Use(m.users.Authorize) - r.Get("/user", m.users.UserInfo()) r.Post("/change-password", m.users.ChangePassword()) - r.Get("/nodes", m.getNodes()) r.Get("/nodes/{pk}", m.getNode()) - r.Get("/nodes/{pk}/apps", m.getApps()) r.Get("/nodes/{pk}/apps/{app}", m.getApp()) r.Put("/nodes/{pk}/apps/{app}", m.putApp()) - r.Get("/nodes/{pk}/transport-types", m.getTransportTypes()) - r.Get("/nodes/{pk}/transports", m.getTransports()) r.Post("/nodes/{pk}/transports", m.postTransport()) r.Get("/nodes/{pk}/transports/{tid}", m.getTransport()) r.Delete("/nodes/{pk}/transports/{tid}", m.deleteTransport()) - r.Get("/nodes/{pk}/routes", m.getRoutes()) r.Post("/nodes/{pk}/routes", m.postRoute()) r.Get("/nodes/{pk}/routes/{rid}", m.getRoute()) @@ -129,7 +119,6 @@ func (m *Node) ServeHTTP(w http.ResponseWriter, req *http.Request) { r.Delete("/nodes/{pk}/routes/{rid}", m.deleteRoute()) }) }) - r.ServeHTTP(w, req) } @@ -153,7 +142,7 @@ func (m *Node) getNodes() http.HandlerFunc { // provides summary of single node. func (m *Node) getNode() http.HandlerFunc { - return m.ctxNode(func(w http.ResponseWriter, r *http.Request, ctx nodeCtx) { + return m.withCtx(m.nodeCtx, func(w http.ResponseWriter, r *http.Request, ctx *httpCtx) { summary, err := ctx.RPC.Summary() if err != nil { httputil.WriteJSON(w, r, http.StatusInternalServerError, err) @@ -165,7 +154,7 @@ func (m *Node) getNode() http.HandlerFunc { // returns app summaries of a given node of pk func (m *Node) getApps() http.HandlerFunc { - return m.ctxNode(func(w http.ResponseWriter, r *http.Request, ctx nodeCtx) { + return m.withCtx(m.nodeCtx, func(w http.ResponseWriter, r *http.Request, ctx *httpCtx) { apps, err := ctx.RPC.Apps() if err != nil { httputil.WriteJSON(w, r, http.StatusInternalServerError, err) @@ -177,13 +166,13 @@ func (m *Node) getApps() http.HandlerFunc { // returns an app summary of a given node's pk and app name func (m *Node) getApp() http.HandlerFunc { - return m.ctxApp(func(w http.ResponseWriter, r *http.Request, ctx appCtx) { + return m.withCtx(m.appCtx, func(w http.ResponseWriter, r *http.Request, ctx *httpCtx) { httputil.WriteJSON(w, r, http.StatusOK, ctx.App) }) } func (m *Node) putApp() http.HandlerFunc { - return m.ctxApp(func(w http.ResponseWriter, r *http.Request, ctx appCtx) { + return m.withCtx(m.appCtx, func(w http.ResponseWriter, r *http.Request, ctx *httpCtx) { var reqBody struct { Autostart *bool `json:"autostart,omitempty"` Status *int `json:"status,omitempty"` @@ -222,7 +211,7 @@ func (m *Node) putApp() http.HandlerFunc { } func (m *Node) getTransportTypes() http.HandlerFunc { - return m.ctxNode(func(w http.ResponseWriter, r *http.Request, ctx nodeCtx) { + return m.withCtx(m.nodeCtx, func(w http.ResponseWriter, r *http.Request, ctx *httpCtx) { types, err := ctx.RPC.TransportTypes() if err != nil { httputil.WriteJSON(w, r, http.StatusInternalServerError, err) @@ -233,7 +222,7 @@ func (m *Node) getTransportTypes() http.HandlerFunc { } func (m *Node) getTransports() http.HandlerFunc { - return m.ctxNode(func(w http.ResponseWriter, r *http.Request, ctx nodeCtx) { + return m.withCtx(m.nodeCtx, func(w http.ResponseWriter, r *http.Request, ctx *httpCtx) { var ( qTypes []string qPKs []cipher.PubKey @@ -259,7 +248,7 @@ func (m *Node) getTransports() http.HandlerFunc { } func (m *Node) postTransport() http.HandlerFunc { - return m.ctxNode(func(w http.ResponseWriter, r *http.Request, ctx nodeCtx) { + return m.withCtx(m.nodeCtx, func(w http.ResponseWriter, r *http.Request, ctx *httpCtx) { var reqBody struct { Remote cipher.PubKey `json:"remote_pk"` TpType string `json:"transport_type"` @@ -279,13 +268,13 @@ func (m *Node) postTransport() http.HandlerFunc { } func (m *Node) getTransport() http.HandlerFunc { - return m.ctxTransport(func(w http.ResponseWriter, r *http.Request, ctx transportCtx) { + return m.withCtx(m.tpCtx, func(w http.ResponseWriter, r *http.Request, ctx *httpCtx) { httputil.WriteJSON(w, r, http.StatusOK, ctx.Tp) }) } func (m *Node) deleteTransport() http.HandlerFunc { - return m.ctxTransport(func(w http.ResponseWriter, r *http.Request, ctx transportCtx) { + return m.withCtx(m.tpCtx, func(w http.ResponseWriter, r *http.Request, ctx *httpCtx) { if err := ctx.RPC.RemoveTransport(ctx.Tp.ID); err != nil { httputil.WriteJSON(w, r, http.StatusInternalServerError, err) return @@ -312,7 +301,7 @@ func makeRoutingRuleResp(key routing.RouteID, rule routing.Rule, summary bool) r } func (m *Node) getRoutes() http.HandlerFunc { - return m.ctxNode(func(w http.ResponseWriter, r *http.Request, ctx nodeCtx) { + return m.withCtx(m.nodeCtx, func(w http.ResponseWriter, r *http.Request, ctx *httpCtx) { qSummary, err := httputil.BoolFromQuery(r, "summary", false) if err != nil { httputil.WriteJSON(w, r, http.StatusBadRequest, err) @@ -332,7 +321,7 @@ func (m *Node) getRoutes() http.HandlerFunc { } func (m *Node) postRoute() http.HandlerFunc { - return m.ctxNode(func(w http.ResponseWriter, r *http.Request, ctx nodeCtx) { + return m.withCtx(m.nodeCtx, func(w http.ResponseWriter, r *http.Request, ctx *httpCtx) { var summary routing.RuleSummary if err := httputil.ReadJSON(r, &summary); err != nil { httputil.WriteJSON(w, r, http.StatusBadRequest, err) @@ -353,7 +342,7 @@ func (m *Node) postRoute() http.HandlerFunc { } func (m *Node) getRoute() http.HandlerFunc { - return m.ctxRoute(func(w http.ResponseWriter, r *http.Request, ctx routeCtx) { + return m.withCtx(m.routeCtx, func(w http.ResponseWriter, r *http.Request, ctx *httpCtx) { qSummary, err := httputil.BoolFromQuery(r, "summary", true) if err != nil { httputil.WriteJSON(w, r, http.StatusBadRequest, err) @@ -369,7 +358,7 @@ func (m *Node) getRoute() http.HandlerFunc { } func (m *Node) putRoute() http.HandlerFunc { - return m.ctxRoute(func(w http.ResponseWriter, r *http.Request, ctx routeCtx) { + return m.withCtx(m.routeCtx, func(w http.ResponseWriter, r *http.Request, ctx *httpCtx) { var summary routing.RuleSummary if err := httputil.ReadJSON(r, &summary); err != nil { httputil.WriteJSON(w, r, http.StatusBadRequest, err) @@ -389,7 +378,7 @@ func (m *Node) putRoute() http.HandlerFunc { } func (m *Node) deleteRoute() http.HandlerFunc { - return m.ctxRoute(func(w http.ResponseWriter, r *http.Request, ctx routeCtx) { + return m.withCtx(m.routeCtx, func(w http.ResponseWriter, r *http.Request, ctx *httpCtx) { if err := ctx.RPC.RemoveRoutingRule(ctx.RtKey); err != nil { httputil.WriteJSON(w, r, http.StatusNotFound, err) return @@ -409,6 +398,110 @@ func (m *Node) client(pk cipher.PubKey) (node.RPCClient, bool) { return client, ok } +type httpCtx struct { + // Node + PK cipher.PubKey + RPC node.RPCClient + + // App + App *node.AppState + + // Transport + Tp *node.TransportSummary + + // Route + RtKey routing.RouteID +} + +type ( + valuesFunc func(w http.ResponseWriter, r *http.Request) (*httpCtx, bool) + handlerFunc func(w http.ResponseWriter, r *http.Request, ctx *httpCtx) +) + +func (m *Node) withCtx(vFunc valuesFunc, hFunc handlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if rv, ok := vFunc(w, r); ok { + hFunc(w, r, rv) + } + } +} + +func (m *Node) nodeCtx(w http.ResponseWriter, r *http.Request) (*httpCtx, bool) { + pk, err := pkFromParam(r, "pk") + if err != nil { + httputil.WriteJSON(w, r, http.StatusBadRequest, err) + return nil, false + } + client, ok := m.client(pk) + if !ok { + httputil.WriteJSON(w, r, http.StatusNotFound, fmt.Errorf("node of pk '%s' not found", pk)) + return nil, false + } + return &httpCtx{ + PK: pk, + RPC: client, + }, true +} + +func (m *Node) appCtx(w http.ResponseWriter, r *http.Request) (*httpCtx, bool) { + ctx, ok := m.nodeCtx(w, r) + if !ok { + return nil, false + } + appName := chi.URLParam(r, "app") + apps, err := ctx.RPC.Apps() + if err != nil { + httputil.WriteJSON(w, r, http.StatusInternalServerError, err) + return nil, false + } + for _, app := range apps { + if app.Name == appName { + ctx.App = app + return ctx, true + } + } + httputil.WriteJSON(w, r, http.StatusNotFound, + fmt.Errorf("can not find app of name %s from node %s", appName, ctx.PK)) + return nil, false +} + +func (m *Node) tpCtx(w http.ResponseWriter, r *http.Request) (*httpCtx, bool) { + ctx, ok := m.appCtx(w, r) + if !ok { + return nil, false + } + tid, err := uuidFromParam(r, "tid") + if err != nil { + httputil.WriteJSON(w, r, http.StatusBadRequest, err) + return nil, false + } + tp, err := ctx.RPC.Transport(tid) + if err != nil { + if err.Error() == node.ErrNotFound.Error() { + httputil.WriteJSON(w, r, http.StatusNotFound, + fmt.Errorf("transport of ID %s is not found", tid)) + return nil, false + } + httputil.WriteJSON(w, r, http.StatusInternalServerError, err) + return nil, false + } + ctx.Tp = tp + return ctx, true +} + +func (m *Node) routeCtx(w http.ResponseWriter, r *http.Request) (*httpCtx, bool) { + ctx, ok := m.tpCtx(w, r) + if !ok { + return nil, false + } + rid, err := ridFromParam(r, "key") + if err != nil { + httputil.WriteJSON(w, r, http.StatusBadRequest, err) + } + ctx.RtKey = rid + return ctx, true +} + func pkFromParam(r *http.Request, key string) (cipher.PubKey, error) { pk := cipher.PubKey{} err := pk.UnmarshalText([]byte(chi.URLParam(r, key))) @@ -451,100 +544,6 @@ func pkSliceFromQuery(r *http.Request, key string, defaultVal []cipher.PubKey) ( return pks, nil } -type nodeCtx struct { - PK cipher.PubKey - RPC node.RPCClient -} - -type nodeHandlerFunc func(w http.ResponseWriter, r *http.Request, ctx nodeCtx) - -func (m *Node) ctxNode(next nodeHandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - pk, err := pkFromParam(r, "pk") - if err != nil { - httputil.WriteJSON(w, r, http.StatusBadRequest, err) - return - } - client, ok := m.client(pk) - if !ok { - httputil.WriteJSON(w, r, http.StatusNotFound, fmt.Errorf("node of pk '%s' not found", pk)) - return - } - next(w, r, nodeCtx{PK: pk, RPC: client}) - } -} - -type appCtx struct { - nodeCtx - App *node.AppState -} - -type appHandlerFunc func(w http.ResponseWriter, r *http.Request, ctx appCtx) - -func (m *Node) ctxApp(next appHandlerFunc) http.HandlerFunc { - return m.ctxNode(func(w http.ResponseWriter, r *http.Request, ctx nodeCtx) { - appName := chi.URLParam(r, "app") - apps, err := ctx.RPC.Apps() - if err != nil { - httputil.WriteJSON(w, r, http.StatusInternalServerError, err) - return - } - for _, app := range apps { - if app.Name == appName { - next(w, r, appCtx{nodeCtx: ctx, App: app}) - return - } - } - httputil.WriteJSON(w, r, http.StatusNotFound, - fmt.Errorf("can not find app of name %s from node %s", appName, ctx.PK)) - }) -} - -type transportCtx struct { - nodeCtx - Tp *node.TransportSummary -} - -type transportHandlerFunc func(w http.ResponseWriter, r *http.Request, ctx transportCtx) - -func (m *Node) ctxTransport(next transportHandlerFunc) http.HandlerFunc { - return m.ctxNode(func(w http.ResponseWriter, r *http.Request, ctx nodeCtx) { - tid, err := uuidFromParam(r, "tid") - if err != nil { - httputil.WriteJSON(w, r, http.StatusBadRequest, err) - return - } - tp, err := ctx.RPC.Transport(tid) - if err != nil { - if err.Error() == node.ErrNotFound.Error() { - httputil.WriteJSON(w, r, http.StatusNotFound, - fmt.Errorf("transport of ID %s is not found", tid)) - return - } - httputil.WriteJSON(w, r, http.StatusInternalServerError, err) - return - } - next(w, r, transportCtx{nodeCtx: ctx, Tp: tp}) - }) -} - -type routeCtx struct { - nodeCtx - RtKey routing.RouteID -} - -type routeHandlerFunc func(w http.ResponseWriter, r *http.Request, ctx routeCtx) - -func (m *Node) ctxRoute(next routeHandlerFunc) http.HandlerFunc { - return m.ctxNode(func(w http.ResponseWriter, r *http.Request, ctx nodeCtx) { - rid, err := ridFromParam(r, "key") - if err != nil { - httputil.WriteJSON(w, r, http.StatusBadRequest, err) - } - next(w, r, routeCtx{nodeCtx: ctx, RtKey: rid}) - }) -} - func catch(err error, msgs ...string) { if err != nil { if len(msgs) > 0 { diff --git a/pkg/manager/user.go b/pkg/manager/user.go index 0a6791a8bf..d0654f27c0 100644 --- a/pkg/manager/user.go +++ b/pkg/manager/user.go @@ -103,7 +103,7 @@ func NewBoltUserStore(path string) (*BoltUserStore, error) { // User obtains a single user. Returns true if user exists. func (s *BoltUserStore) User(name string) (user User, ok bool) { - catch(s.View(func(tx *bbolt.Tx) error { + catch(s.View(func(tx *bbolt.Tx) error { //nolint:unparam users := tx.Bucket([]byte(boltUserBucketName)) rawUser := users.Get([]byte(name)) if rawUser == nil { From 3237345a8b96c97aaea84de932cb564794a2bcd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E5=BF=97=E5=AE=87?= Date: Fri, 29 Mar 2019 01:15:47 +0800 Subject: [PATCH 16/18] changed env --- cmd/skywire-node/commands/root.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cmd/skywire-node/commands/root.go b/cmd/skywire-node/commands/root.go index 7f97e78b07..1d2a6f8d9c 100644 --- a/cmd/skywire-node/commands/root.go +++ b/cmd/skywire-node/commands/root.go @@ -9,13 +9,14 @@ import ( "syscall" "time" - "github.com/skycoin/skywire/internal/pathutil" - "github.com/spf13/cobra" + "github.com/skycoin/skywire/internal/pathutil" "github.com/skycoin/skywire/pkg/node" ) +const configEnv = "SW_CONFIG" + var rootCmd = &cobra.Command{ Use: "skywire-node [skywire.json]", Short: "App Node for skywire", @@ -23,7 +24,7 @@ var rootCmd = &cobra.Command{ var configFile string if len(args) > 0 { configFile = args[0] - } else if conf, ok := os.LookupEnv("SKYWIRE_CONFIG"); ok { + } else if conf, ok := os.LookupEnv(configEnv); ok { configFile = conf } else { conf, err := pathutil.Find("skywire.json") @@ -33,7 +34,7 @@ var rootCmd = &cobra.Command{ configFile = conf } - log.Println("using conf file at: ", configFile) + log.Println("using conf file at:", configFile) file, err := os.Open(configFile) if err != nil { From 8871167707f2720c02cc9ae6e044e5c156a57be4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E5=BF=97=E5=AE=87?= Date: Sat, 30 Mar 2019 00:04:25 +0800 Subject: [PATCH 17/18] added reusable config file logic --- .gitignore | 1 + README.md | 88 +++++------ cmd/manager-node/commands/gen-config.go | 54 +++++++ cmd/manager-node/commands/init-config.go | 68 -------- cmd/manager-node/commands/root.go | 35 +---- .../commands/{config.go => gen-config.go} | 74 ++++----- cmd/skywire-cli/commands/pk.go | 1 - cmd/skywire-cli/commands/root.go | 4 +- cmd/skywire-node/commands/root.go | 24 +-- internal/pathutil/configpath.go | 148 ++++++++++++++++++ internal/pathutil/data.go | 42 ----- pkg/manager/config.go | 11 ++ 12 files changed, 305 insertions(+), 245 deletions(-) create mode 100644 cmd/manager-node/commands/gen-config.go delete mode 100644 cmd/manager-node/commands/init-config.go rename cmd/skywire-cli/commands/{config.go => gen-config.go} (66%) create mode 100644 internal/pathutil/configpath.go delete mode 100644 internal/pathutil/data.go diff --git a/.gitignore b/.gitignore index 3f69f2ac2a..297304b1d1 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ .idea/ /skywire.json +/skywire-config.json /apps/ /skywire/ /local/ diff --git a/README.md b/README.md index bf59ee97b8..d64786e8d7 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ $ make install # compiles and installs all binaries **Generate default json config** ```bash -skywire-cli config +skywire-cli gen-config ``` ### Run `skywire-node` @@ -71,8 +71,8 @@ skywire-cli config `skywire-node` hosts apps, proxies app's requests to remote nodes and exposes communication API that apps can use to implement communication protocols. App binaries are spawned by the node, communication between node and app is performed via unix pipes provided on app startup. ```bash -# Run skywire-node. It takes one argument; the path of a configuration file (`skywire.json` if unspecified). -$ skywire-node skywire.json +# Run skywire-node. It takes one argument; the path of a configuration file (`skywire-config.json` if unspecified). +$ skywire-node skywire-config.json ``` ### Run `skywire-node` in docker container @@ -88,42 +88,42 @@ The `skywire-cli` tool is used to control the `skywire-node`. Refer to the help ```bash $ skywire-cli -h -# Command Line Interface for skywire -# -# Usage: -# skywire-cli [command] -# -# Available Commands: -# add-rule adds a new routing rule -# add-transport adds a new transport -# apps lists apps running on the node -# config Generate default config file -# find-routes lists available routes between two nodes via route finder service -# find-transport finds and lists transport(s) of given transport ID or edge public key from transport discovery -# help Help about any command -# list-rules lists the local node's routing rules -# list-transports lists the available transports with optional filter flags -# messaging manage operations with messaging services -# rm-rule removes a routing rule via route ID key -# rm-transport removes transport with given id -# rule returns a routing rule via route ID key -# set-app-autostart sets the autostart flag for an app of given name -# start-app starts an app of given name -# stop-app stops an app of given name -# transport returns summary of given transport by id -# transport-types lists transport types used by the local node -# -# Flags: -# -h, --help help for skywire-cli -# --rpc string RPC server address (default "localhost:3435") -# -# Use "skywire-cli [command] --help" for more information about a command. - +# Command Line Interface for skywire +# +# Usage: +# skywire-cli [command] +# +# Available Commands: +# add-rule adds a new routing rule +# add-transport adds a new transport +# apps lists apps running on the node +# find-routes lists available routes between two nodes via route finder service +# find-transport finds and lists transport(s) of given transport ID or edge public key from transport discovery +# gen-config Generate default config file +# help Help about any command +# list-rules lists the local node's routing rules +# list-transports lists the available transports with optional filter flags +# messaging manage operations with messaging services +# pk get public key of node +# rm-rule removes a routing rule via route ID key +# rm-transport removes transport with given id +# rule returns a routing rule via route ID key +# set-app-autostart sets the autostart flag for an app of given name +# start-app starts an app of given name +# stop-app stops an app of given name +# transport returns summary of given transport by id +# transport-types lists transport types used by the local node +# +# Flags: +# -h, --help help for skywire-cli +# --rpc string RPC server address (default "localhost:3435") +# +# Use "skywire-cli [command] --help" for more information about a command. ``` ### Apps -After `skywire-node` is up and running with default environment, default apps are run with the configuration specified in `skywire.json`. Refer to the following for usage of the default apps: +After `skywire-node` is up and running with default environment, default apps are run with the configuration specified in `skywire-config.json`. Refer to the following for usage of the default apps: - [Chat](/cmd/apps/chat) - [Hello World](/cmd/apps/helloworld) @@ -193,7 +193,7 @@ This will: #### Structure of `./node` -```bash +``` ./node ├── apps # node `apps` compiled with DOCKER_OPTS │   ├── chat.v1.0 # @@ -203,14 +203,14 @@ This will: │   ├── therealssh-client.v1.0 # │   └── therealssh.v1.0 # ├── local # **Created inside docker** -│   ├── chat # according to "local_path" in skywire.json +│   ├── chat # according to "local_path" in skywire-config.json │   ├── therealproxy # │   └── therealssh # ├── PK # contains public key of node ├── skywire # db & logs. **Created inside docker** │   ├── routing.db # │   └── transport_logs # -├── skywire.json # config of node +├── skywire-config.json # config of node └── skywire-node # `skywire-node binary` compiled with DOCKER_OPTS ``` @@ -275,7 +275,7 @@ Default value: "GO111MODULE=on GOOS=linux" #### 1. Get Public Key of docker-node ```bash -$ cat ./node/skywire.json|grep static_public_key |cut -d ':' -f2 |tr -d '"'','' ' +$ cat ./node/skywire-config.json|grep static_public_key |cut -d ':' -f2 |tr -d '"'','' ' # 029be6fa68c13e9222553035cc1636d98fb36a888aa569d9ce8aa58caa2c651b45 ``` @@ -308,8 +308,8 @@ $ GO111MODULE=on GOOS=linux go build -o /tmp/SKYNODE/apps/helloworld.v1.0 ./cmd/ $ GO111MODULE=on GOOS=linux go build -o /tmp/SKYNODE/apps/therealproxy.v1.0 ./cmd/apps/therealproxy $ GO111MODULE=on GOOS=linux go build -o /tmp/SKYNODE/apps/therealssh.v1.0 ./cmd/apps/therealssh $ GO111MODULE=on GOOS=linux go build -o /tmp/SKYNODE/apps/therealssh-client.v1.0 ./cmd/apps/therealssh-client -# 4. Create skywire.json for node -$ skywire-cli config /tmp/SKYNODE/skywire.json +# 4. Create skywire-config.json for node +$ skywire-cli gen-config -o /tmp/SKYNODE/skywire-config.json # 2019/03/15 16:43:49 Done! $ tree /tmp/SKYNODE # /tmp/SKYNODE @@ -319,7 +319,7 @@ $ tree /tmp/SKYNODE # │   ├── therealproxy.v1.0 # │   ├── therealssh-client.v1.0 # │   └── therealssh.v1.0 -# ├── skywire.json +# ├── skywire-config.json # └── skywire-node # So far so good. We prepared docker volume. Now we can: $ docker run -it -v /tmp/SKYNODE:/sky --network=SKYNET --name=SKYNODE skywire-runner bash -c "cd /sky && ./skywire-node" @@ -353,9 +353,9 @@ Instead of skywire-runner you can use: ```bash export SW_NODE_A=127.0.0.1 -export SW_NODE_A_PK=$(cat ./skywire.json|grep static_public_key |cut -d ':' -f2 |tr -d '"'','' ') +export SW_NODE_A_PK=$(cat ./skywire-config.json|grep static_public_key |cut -d ':' -f2 |tr -d '"'','' ') export SW_NODE_B=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' SKY01) -export SW_NODE_B_PK=$(cat ./node/skywire.json|grep static_public_key |cut -d ':' -f2 |tr -d '"'','' ') +export SW_NODE_B_PK=$(cat ./node/skywire-config.json|grep static_public_key |cut -d ':' -f2 |tr -d '"'','' ') ``` #### 6. "Hello-Mike-Hello-Joe" test diff --git a/cmd/manager-node/commands/gen-config.go b/cmd/manager-node/commands/gen-config.go new file mode 100644 index 0000000000..bf5c167e72 --- /dev/null +++ b/cmd/manager-node/commands/gen-config.go @@ -0,0 +1,54 @@ +package commands + +import ( + "fmt" + "path/filepath" + + "github.com/skycoin/skywire/internal/pathutil" + + "github.com/spf13/cobra" + + "github.com/skycoin/skywire/pkg/manager" +) + +var ( + output string + replace bool + configLocType = pathutil.WorkingDirLoc +) + +func init() { + rootCmd.AddCommand(genConfigCmd) + genConfigCmd.Flags().StringVarP(&output, "output", "o", "", "path of output config file. Uses default of 'type' flag if unspecified.") + genConfigCmd.Flags().BoolVarP(&replace, "replace", "r", false, "whether to allow rewrite of a file that already exists.") + genConfigCmd.Flags().VarP(&configLocType, "type", "m", fmt.Sprintf("config generation mode. Valid values: %v", pathutil.AllConfigLocationTypes())) +} + +var genConfigCmd = &cobra.Command{ + Use: "gen-config", + Short: "generates a configuration file", + PreRun: func(_ *cobra.Command, _ []string) { + if output == "" { + output = pathutil.NodeDefaults().Get(configLocType) + log.Infof("no 'output,o' flag is empty, using default path: %s", output) + } + var err error + if output, err = filepath.Abs(output); err != nil { + log.WithError(err).Fatalln("invalid output provided") + } + }, + Run: func(_ *cobra.Command, _ []string) { + var conf manager.Config + switch configLocType { + case pathutil.WorkingDirLoc: + conf = manager.GenerateWorkDirConfig() + case pathutil.HomeLoc: + conf = manager.GenerateHomeConfig() + case pathutil.LocalLoc: + conf = manager.GenerateLocalConfig() + default: + log.Fatalln("invalid config type:", configLocType) + } + pathutil.WriteJSONConfig(conf, output, replace) + }, +} diff --git a/cmd/manager-node/commands/init-config.go b/cmd/manager-node/commands/init-config.go deleted file mode 100644 index 4a4743da46..0000000000 --- a/cmd/manager-node/commands/init-config.go +++ /dev/null @@ -1,68 +0,0 @@ -package commands - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "os" - "path/filepath" - - "github.com/spf13/cobra" - - "github.com/skycoin/skywire/pkg/manager" -) - -const ( - homeMode = "HOME" - localMode = "LOCAL" -) - -var initConfigModes = []string{homeMode, localMode} - -var ( - output string - replace bool - mode string -) - -func init() { - rootCmd.AddCommand(initConfigCmd) - - initConfigCmd.Flags().StringVarP(&output, "output", "o", defaultConfigPaths[0], "path of output config file.") - initConfigCmd.Flags().BoolVarP(&replace, "replace", "r", false, "whether to allow rewrite of a file that already exists.") - initConfigCmd.Flags().StringVarP(&mode, "mode", "m", homeMode, fmt.Sprintf("config generation mode. Valid values: %v", initConfigModes)) -} - -var initConfigCmd = &cobra.Command{ - Use: "init-config", - Short: "generates a configuration file", - Run: func(_ *cobra.Command, _ []string) { - output, err := filepath.Abs(output) - if err != nil { - log.WithError(err).Fatalln("invalid output provided") - } - var conf manager.Config - switch mode { - case homeMode: - conf = manager.GenerateHomeConfig() - case localMode: - conf = manager.GenerateLocalConfig() - default: - log.Fatalln("invalid mode:", mode) - } - raw, err := json.MarshalIndent(conf, "", " ") - if err != nil { - log.WithError(err).Fatal("unexpected error, report to dev") - } - if _, err := os.Stat(output); !replace && err == nil { - log.Fatalf("file %s already exists, stopping as 'replace,r' flag is not set", output) - } - if err := os.MkdirAll(filepath.Dir(output), 0750); err != nil { - log.WithError(err).Fatalln("failed to create output directory") - } - if err := ioutil.WriteFile(output, raw, 0744); err != nil { - log.WithError(err).Fatalln("failed to write file") - } - log.Infof("Wrote %d bytes to %s\n%s", len(raw), output, string(raw)) - }, -} diff --git a/cmd/manager-node/commands/root.go b/cmd/manager-node/commands/root.go index bdf5d9118a..bdb9891667 100644 --- a/cmd/manager-node/commands/root.go +++ b/cmd/manager-node/commands/root.go @@ -1,12 +1,10 @@ package commands import ( - "errors" "fmt" "net" "net/http" "os" - "path/filepath" "github.com/skycoin/skycoin/src/util/logging" "github.com/spf13/cobra" @@ -15,6 +13,8 @@ import ( "github.com/skycoin/skywire/pkg/manager" ) +const configEnv = "SW_MANAGER_CONFIG" + var ( log = logging.MustGetLogger("manager-node") @@ -22,26 +22,8 @@ var ( mockNodes int mockMaxTps int mockMaxRoutes int - - defaultConfigPaths = [2]string{ - filepath.Join(pathutil.HomeDir(), ".skycoin/skywire-manager/config.json"), - "/usr/local/skycoin/skywire-manager/config.json", - } ) -func findConfigPath() (string, error) { - log.Info("configuration file is not explicitly specified, attempting to find one in default paths ...") - for i, cPath := range defaultConfigPaths { - if _, err := os.Stat(cPath); err != nil { - log.Infof("- [%d/%d] '%s' does not exist", i+1, len(defaultConfigPaths), cPath) - } else { - log.Infof("- [%d/%d] '%s' exists (using this one)", i+1, len(defaultConfigPaths), cPath) - return cPath, nil - } - } - return "", errors.New("no configuration file found") -} - func init() { rootCmd.Flags().BoolVar(&mock, "mock", false, "whether to run manager node with mock data") rootCmd.Flags().IntVar(&mockNodes, "mock-nodes", 5, "number of app nodes to have in mock mode") @@ -53,23 +35,14 @@ var rootCmd = &cobra.Command{ Use: "manager-node [config-path]", Short: "Manages Skywire App Nodes", Run: func(_ *cobra.Command, args []string) { - - var configPath string - if len(args) == 0 { - var err error - if configPath, err = findConfigPath(); err != nil { - log.WithError(err).Fatal() - } - } else { - configPath = args[0] - } - log.Infof("config path: '%s'", configPath) + configPath := pathutil.FindConfigPath(args, 0, configEnv, pathutil.ManagerDefaults()) var config manager.Config config.FillDefaults() if err := config.Parse(configPath); err != nil { log.WithError(err).Fatalln("failed to parse config file") } + var ( httpAddr = config.Interfaces.HTTPAddr rpcAddr = config.Interfaces.RPCAddr diff --git a/cmd/skywire-cli/commands/config.go b/cmd/skywire-cli/commands/gen-config.go similarity index 66% rename from cmd/skywire-cli/commands/config.go rename to cmd/skywire-cli/commands/gen-config.go index 614d2810fc..b88829be2c 100644 --- a/cmd/skywire-cli/commands/config.go +++ b/cmd/skywire-cli/commands/gen-config.go @@ -2,63 +2,60 @@ package commands import ( "encoding/base64" - "encoding/json" - "log" - "os" + "fmt" "path/filepath" - "github.com/skycoin/skywire/internal/pathutil" - "github.com/spf13/cobra" + "github.com/skycoin/skywire/internal/pathutil" "github.com/skycoin/skywire/pkg/cipher" "github.com/skycoin/skywire/pkg/node" ) -func init() { - rootCmd.AddCommand(configCmd) -} - var ( - configMode string + output string + replace bool + configLocType = pathutil.WorkingDirLoc ) -var configCmd = &cobra.Command{ - Use: "config [skywire.json]", +func init() { + rootCmd.AddCommand(genConfigCmd) + genConfigCmd.Flags().StringVarP(&output, "output", "o", "", "path of output config file. Uses default of 'type' flag if unspecified.") + genConfigCmd.Flags().BoolVarP(&replace, "replace", "r", false, "whether to allow rewrite of a file that already exists.") + genConfigCmd.Flags().VarP(&configLocType, "type", "m", fmt.Sprintf("config generation mode. Valid values: %v", pathutil.AllConfigLocationTypes())) +} + +var genConfigCmd = &cobra.Command{ + Use: "gen-config", Short: "Generate default config file", - Run: func(_ *cobra.Command, args []string) { - configFile := "skywire.json" - if len(args) > 0 { - configFile = args[0] + PreRun: func(cmd *cobra.Command, args []string) { + if output == "" { + output = pathutil.ManagerDefaults().Get(configLocType) + log.Infof("no 'output,o' flag is empty, using default path: %s", output) } - + var err error + if output, err = filepath.Abs(output); err != nil { + log.WithError(err).Fatalln("invalid output provided") + } + }, + Run: func(_ *cobra.Command, args []string) { var conf *node.Config - switch configMode { - case "local": + switch configLocType { + case pathutil.WorkingDirLoc: conf = defaultConfig() - case "home": + case pathutil.HomeLoc: conf = homeConfig() - case "system": - conf = systemConfig() - } - confFile, err := os.OpenFile(configFile, os.O_RDWR|os.O_CREATE, 0600) - if err != nil { - log.Fatal("Failed to create config files: ", err) + case pathutil.LocalLoc: + conf = localConfig() + default: + log.Fatalln("invalid config type:", configLocType) } - - enc := json.NewEncoder(confFile) - enc.SetIndent("", " ") - if err := enc.Encode(conf); err != nil { - log.Fatal("Failed to encode json config: ", err) - } - - log.Println("Done!") + pathutil.WriteJSONConfig(conf, output, replace) }, } func homeConfig() *node.Config { c := defaultConfig() - c.AppsPath = filepath.Join(pathutil.HomeDir(), ".skycoin/skywire/apps") c.Transport.LogStore.Location = filepath.Join(pathutil.HomeDir(), ".skycoin/skywire/transport_logs") c.Routing.Table.Location = filepath.Join(pathutil.HomeDir(), ".skycoin/skywire/routing.db") @@ -66,9 +63,8 @@ func homeConfig() *node.Config { return c } -func systemConfig() *node.Config { +func localConfig() *node.Config { c := defaultConfig() - c.AppsPath = "/usr/local/skycoin/skywire/apps" c.Transport.LogStore.Location = "/usr/local/skycoin/skywire/transport_logs" c.Routing.Table.Location = "/usr/local/skycoin/skywire/routing.db" @@ -117,7 +113,3 @@ func defaultConfig() *node.Config { return conf } - -func init() { - configCmd.Flags().StringVar(&configMode, "mode", "home", "either home or local") -} diff --git a/cmd/skywire-cli/commands/pk.go b/cmd/skywire-cli/commands/pk.go index 7bd96fc1f9..91c2e5f346 100644 --- a/cmd/skywire-cli/commands/pk.go +++ b/cmd/skywire-cli/commands/pk.go @@ -2,7 +2,6 @@ package commands import ( "fmt" - "log" "github.com/spf13/cobra" ) diff --git a/cmd/skywire-cli/commands/root.go b/cmd/skywire-cli/commands/root.go index c1ab163fd2..e184bf05f3 100644 --- a/cmd/skywire-cli/commands/root.go +++ b/cmd/skywire-cli/commands/root.go @@ -2,17 +2,19 @@ package commands import ( "fmt" - "log" "net/rpc" "strconv" "github.com/google/uuid" + "github.com/skycoin/skycoin/src/util/logging" "github.com/spf13/cobra" "github.com/skycoin/skywire/pkg/cipher" "github.com/skycoin/skywire/pkg/node" ) +var log = logging.MustGetLogger("skywire-cli") + var rpcAddr string var rootCmd = &cobra.Command{ diff --git a/cmd/skywire-node/commands/root.go b/cmd/skywire-node/commands/root.go index 1d2a6f8d9c..0e7d8edc6f 100644 --- a/cmd/skywire-node/commands/root.go +++ b/cmd/skywire-node/commands/root.go @@ -2,13 +2,14 @@ package commands import ( "encoding/json" - "log" "os" "os/signal" "strings" "syscall" "time" + "github.com/skycoin/skycoin/src/util/logging" + "github.com/spf13/cobra" "github.com/skycoin/skywire/internal/pathutil" @@ -17,33 +18,22 @@ import ( const configEnv = "SW_CONFIG" +var log = logging.MustGetLogger("skywire-node") + var rootCmd = &cobra.Command{ Use: "skywire-node [skywire.json]", Short: "App Node for skywire", Run: func(_ *cobra.Command, args []string) { - var configFile string - if len(args) > 0 { - configFile = args[0] - } else if conf, ok := os.LookupEnv(configEnv); ok { - configFile = conf - } else { - conf, err := pathutil.Find("skywire.json") - if err != nil { - log.Fatalln(err) - } - configFile = conf - } - - log.Println("using conf file at:", configFile) + configPath := pathutil.FindConfigPath(args, 0, configEnv, pathutil.NodeDefaults()) - file, err := os.Open(configFile) + file, err := os.Open(configPath) if err != nil { log.Fatalf("Failed to open config: %s", err) } conf := &node.Config{} if err := json.NewDecoder(file).Decode(&conf); err != nil { - log.Fatalf("Failed to decode %s: %s", configFile, err) + log.Fatalf("Failed to decode %s: %s", configPath, err) } node, err := node.NewNode(conf) diff --git a/internal/pathutil/configpath.go b/internal/pathutil/configpath.go new file mode 100644 index 0000000000..a29c0c6207 --- /dev/null +++ b/internal/pathutil/configpath.go @@ -0,0 +1,148 @@ +package pathutil + +import ( + "encoding/json" + "io/ioutil" + "os" + "path/filepath" + + "github.com/skycoin/skycoin/src/util/logging" +) + +var log = logging.MustGetLogger("pathutil") + +// ConfigLocationType describes a config path's location type. +type ConfigLocationType string + +const ( + // WorkingDirLoc represents the default working directory location for a configuration file. + WorkingDirLoc = ConfigLocationType("WD") + + // HomeLoc represents the default home folder location for a configuration file. + HomeLoc = ConfigLocationType("HOME") + + // LocalLoc represents the default /usr/local location for a configuration file. + LocalLoc = ConfigLocationType("LOCAL") +) + +// String implements fmt.Stringer for ConfigLocationType. +func (t ConfigLocationType) String() string { + return string(t) +} + +// Set implements pflag.Value for ConfigLocationType. +func (t *ConfigLocationType) Set(s string) error { + *t = ConfigLocationType(s) + return nil +} + +// Type implements pflag.Value for ConfigLocationType. +func (t ConfigLocationType) Type() string { + return "pathutil.ConfigLocationType" +} + +// AllConfigLocationTypes returns all valid config location types. +func AllConfigLocationTypes() []ConfigLocationType { + return []ConfigLocationType{ + WorkingDirLoc, + HomeLoc, + LocalLoc, + } +} + +// ConfigPaths contains a map of configuration paths, based on ConfigLocationTypes. +type ConfigPaths map[ConfigLocationType]string + +// String implements fmt.Stringer for ConfigPaths. +func (dp ConfigPaths) String() string { + raw, err := json.MarshalIndent(dp, "", "\t") + if err != nil { + log.Fatalf("cannot marshal default paths: %s", err.Error()) + } + return string(raw) +} + +// Get obtains a path stored under given configuration location type. +func (dp ConfigPaths) Get(cpType ConfigLocationType) string { + if path, ok := dp[cpType]; ok { + return path + } + log.Fatalf("invalid config type '%s' provided. Valid types: %v", cpType, AllConfigLocationTypes()) + return "" +} + +// NodeDefaults returns the default config paths for skywire-node. +func NodeDefaults() ConfigPaths { + paths := make(ConfigPaths) + if wd, err := os.Getwd(); err == nil { + paths[WorkingDirLoc] = filepath.Join(wd, "skywire-config.json") + } + paths[HomeLoc] = filepath.Join(HomeDir(), ".skycoin/skywire/skywire-config.json") + paths[LocalLoc] = "/usr/local/skycoin/skywire/skywire-config.json" + return paths +} + +// ManagerDefaults returns the default config paths for manager-node. +func ManagerDefaults() ConfigPaths { + paths := make(ConfigPaths) + if wd, err := os.Getwd(); err == nil { + paths[WorkingDirLoc] = filepath.Join(wd, "manager-config.json") + } + paths[HomeLoc] = filepath.Join(HomeDir(), ".skycoin/skywire-manager/manager-config.json") + paths[LocalLoc] = "/usr/local/skycoin/skywire-manager/manager-config.json" + return paths +} + +// FindConfigPath is used by a service to find a config file path in the following order: +// - From CLI argument. +// - From ENV. +// - From a list of default paths. +func FindConfigPath(args []string, argsIndex int, env string, defaults ConfigPaths) string { + if len(args) > argsIndex { + path := args[argsIndex] + log.Infof("using args[%d] as config path: %s", argsIndex, path) + return path + } + if env != "" { + if path, ok := os.LookupEnv(env); ok { + log.Infof("using $%s as config path: %s", env, path) + return path + } + } + log.Debugf("config path is not explicitly specified, trying default paths...") + for i, cpType := range []ConfigLocationType{WorkingDirLoc, HomeLoc, LocalLoc} { + path, ok := defaults[cpType] + if !ok { + continue + } + if _, err := os.Stat(path); err != nil { + log.Debugf("- [%d/%d] '%s' cannot be accessed: %s", i+1, len(defaults), path, err.Error()) + } else { + log.Debugf("- [%d/%d] '%s' is found", i+1, len(defaults), path) + log.Printf("using fallback config path: %s", path) + return path + } + } + log.Fatalf("config not found in any of the following paths: %s", defaults.String()) + return "" +} + +// WriteJSONConfig is used by config file generators. +// 'output' specifies the path to save generated config files. +// 'replace' is true if replacing files is allowed. +func WriteJSONConfig(conf interface{}, output string, replace bool) { + raw, err := json.MarshalIndent(conf, "", "\t") + if err != nil { + log.WithError(err).Fatal("unexpected error, report to dev") + } + if _, err := os.Stat(output); !replace && err == nil { + log.Fatalf("file %s already exists, stopping as 'replace,r' flag is not set", output) + } + if err := os.MkdirAll(filepath.Dir(output), 0750); err != nil { + log.WithError(err).Fatalln("failed to create output directory") + } + if err := ioutil.WriteFile(output, raw, 0744); err != nil { + log.WithError(err).Fatalln("failed to write file") + } + log.Infof("Wrote %d bytes to %s\n%s", len(raw), output, string(raw)) +} diff --git a/internal/pathutil/data.go b/internal/pathutil/data.go deleted file mode 100644 index 94ecf10bfc..0000000000 --- a/internal/pathutil/data.go +++ /dev/null @@ -1,42 +0,0 @@ -package pathutil - -import ( - "fmt" - "os" - "path/filepath" - "strings" -) - -// pathutil errors -var ( - ErrDirectoryNotFound = fmt.Errorf( - "directory not found in any of the following paths: %s", strings.Join(paths(), ", ")) -) - -// paths return the paths in which skywire looks for data directories -func paths() []string { - // if we are unable to find the local directory try next option - localDir, _ := os.Getwd() // nolint: errcheck - - return []string{ - localDir, - filepath.Join(HomeDir(), ".skycoin/skywire"), - "/usr/local/skycoin/skywire", - } -} - -// Find tries to find given directory or file in: -// 1) local app directory -// 2) ${HOME}/.skycoin/skywire/{directory} -// 3) /usr/local/skycoin/skywire/{directory} -// It will return the first path, including given directory if found, by preference. -func Find(name string) (string, error) { - for _, path := range paths() { - _, err := os.Stat(filepath.Join(path, name)) - if err == nil { - return filepath.Join(path, name), nil - } - } - - return "", ErrDirectoryNotFound -} diff --git a/pkg/manager/config.go b/pkg/manager/config.go index 062b70f647..6ef5970495 100644 --- a/pkg/manager/config.go +++ b/pkg/manager/config.go @@ -53,6 +53,17 @@ func makeConfig() Config { return c } +// GenerateWorkDirConfig generates a config with default values and uses db from current working directory. +func GenerateWorkDirConfig() Config { + dir, err := os.Getwd() + if err != nil { + log.Fatalf("failed to generate WD config: %s", dir) + } + c := makeConfig() + c.DBPath = filepath.Join(dir, "users.db") + return c +} + // GenerateHomeConfig generates a config with default values and uses db from user's home folder. func GenerateHomeConfig() Config { c := makeConfig() From bc00998a1b9b22f57315776478629b8b92dc827c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97=E5=BF=97=E5=AE=87?= Date: Sat, 30 Mar 2019 00:32:59 +0800 Subject: [PATCH 18/18] fixed bug --- .gitignore | 1 + cmd/manager-node/README.md | 10 ++++++++++ cmd/manager-node/commands/gen-config.go | 2 +- cmd/manager-node/commands/root.go | 1 + cmd/skywire-cli/commands/gen-config.go | 8 ++++---- cmd/skywire-node/commands/root.go | 2 +- users.db | Bin 0 -> 32768 bytes 7 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 users.db diff --git a/.gitignore b/.gitignore index 297304b1d1..377188452a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ /skywire.json /skywire-config.json +/manager-config.json /apps/ /skywire/ /local/ diff --git a/cmd/manager-node/README.md b/cmd/manager-node/README.md index d114171568..10af50618d 100644 --- a/cmd/manager-node/README.md +++ b/cmd/manager-node/README.md @@ -2,9 +2,19 @@ Manager node exposes node management operations via web API. +**Generate config file:** + +```bash + +``` + **Run with mock data:** ```bash +# Generate config file. +$ manager-node gen-config + +# Run. $ manager-node --mock ``` diff --git a/cmd/manager-node/commands/gen-config.go b/cmd/manager-node/commands/gen-config.go index bf5c167e72..39ab4a1df6 100644 --- a/cmd/manager-node/commands/gen-config.go +++ b/cmd/manager-node/commands/gen-config.go @@ -29,7 +29,7 @@ var genConfigCmd = &cobra.Command{ Short: "generates a configuration file", PreRun: func(_ *cobra.Command, _ []string) { if output == "" { - output = pathutil.NodeDefaults().Get(configLocType) + output = pathutil.ManagerDefaults().Get(configLocType) log.Infof("no 'output,o' flag is empty, using default path: %s", output) } var err error diff --git a/cmd/manager-node/commands/root.go b/cmd/manager-node/commands/root.go index bdb9891667..58fd48530b 100644 --- a/cmd/manager-node/commands/root.go +++ b/cmd/manager-node/commands/root.go @@ -42,6 +42,7 @@ var rootCmd = &cobra.Command{ if err := config.Parse(configPath); err != nil { log.WithError(err).Fatalln("failed to parse config file") } + fmt.Println(config) var ( httpAddr = config.Interfaces.HTTPAddr diff --git a/cmd/skywire-cli/commands/gen-config.go b/cmd/skywire-cli/commands/gen-config.go index b88829be2c..c7e3868c0f 100644 --- a/cmd/skywire-cli/commands/gen-config.go +++ b/cmd/skywire-cli/commands/gen-config.go @@ -28,17 +28,17 @@ func init() { var genConfigCmd = &cobra.Command{ Use: "gen-config", Short: "Generate default config file", - PreRun: func(cmd *cobra.Command, args []string) { + PreRun: func(_ *cobra.Command, _ []string) { if output == "" { - output = pathutil.ManagerDefaults().Get(configLocType) - log.Infof("no 'output,o' flag is empty, using default path: %s", output) + output = pathutil.NodeDefaults().Get(configLocType) + log.Infof("No 'output' set; using default path: %s", output) } var err error if output, err = filepath.Abs(output); err != nil { log.WithError(err).Fatalln("invalid output provided") } }, - Run: func(_ *cobra.Command, args []string) { + Run: func(_ *cobra.Command, _ []string) { var conf *node.Config switch configLocType { case pathutil.WorkingDirLoc: diff --git a/cmd/skywire-node/commands/root.go b/cmd/skywire-node/commands/root.go index 0e7d8edc6f..6c00443052 100644 --- a/cmd/skywire-node/commands/root.go +++ b/cmd/skywire-node/commands/root.go @@ -21,7 +21,7 @@ const configEnv = "SW_CONFIG" var log = logging.MustGetLogger("skywire-node") var rootCmd = &cobra.Command{ - Use: "skywire-node [skywire.json]", + Use: "skywire-node [config-path]", Short: "App Node for skywire", Run: func(_ *cobra.Command, args []string) { configPath := pathutil.FindConfigPath(args, 0, configEnv, pathutil.NodeDefaults()) diff --git a/users.db b/users.db new file mode 100644 index 0000000000000000000000000000000000000000..27182d35c599779c2dab3d2f41200ba210a26b1f GIT binary patch literal 32768 zcmeI)t**i_6ae7zt4J;y3<`xKyu%gO@DNB45Cl9b?*JahcGn{y;7Dk~*JNwAbw(=hw>`_?>vMW`F`||_kVs5%hBR*`q14a0RjXF5FkK+009C72oNApfI#H)dAYxU zh=f3Z009C72oNAZfB*pk1PHuaAlCO|9^l>M8;<}10t5&UAV7cs0RjXF5cs@6?Bj2z zW+#7ruJ;1e-}z6fW4~K%cUAqgm^66G;wAwC1PBlyK!5-N0t5&UC`};#=Qm7EG9M{@ z>