From 801d6b93bbd6a228869901a65b8e6d9bb055bfd5 Mon Sep 17 00:00:00 2001 From: James Mills Date: Tue, 22 Mar 2022 22:59:09 +0000 Subject: [PATCH] Add support for client and server (broker) registration (#43) Co-authored-by: James Mills Reviewed-on: https://git.mills.io/prologic/saltyim/pulls/43 --- client.go | 33 ++++++++++ cmd/salty-chat/register.go | 66 +++++++++++++++++++ cmd/saltyd/main.go | 8 ++- data/.gitkeep | 0 ...631c181f4156f0edcde5cffa25b347c7ceda8.json | 1 + go.mod | 4 +- go.sum | 4 ++ internal/api.go | 28 ++++++++ internal/config.go | 21 +++++- internal/options.go | 34 +++++++++- internal/tasks.go | 47 +++++++++++++ types.go | 32 +++++++++ 12 files changed, 271 insertions(+), 7 deletions(-) create mode 100644 cmd/salty-chat/register.go create mode 100644 data/.gitkeep create mode 100644 data/.well-known/salty/d3d52221e8da5a8ae012f4e2db0631c181f4156f0edcde5cffa25b347c7ceda8.json create mode 100644 internal/tasks.go create mode 100644 types.go diff --git a/client.go b/client.go index 96b6ea4..c77e5b4 100644 --- a/client.go +++ b/client.go @@ -3,8 +3,10 @@ package saltyim import ( "bytes" "context" + "encoding/json" "fmt" "net/http" + "net/url" "os" "time" @@ -168,3 +170,34 @@ func (cli *Client) Send(user, msg string) error { return nil } + +// Register sends a registration requestn to a broker +func (cli *Client) Register() error { + req := RegisterRequest{ + Addr: cli.me, + Key: cli.key.ID().String(), + } + data, err := json.Marshal(req) + if err != nil { + return fmt.Errorf("error serializing register request: %w", err) + } + signed, err := salty.Sign(cli.key, data) + if err != nil { + return fmt.Errorf("error signing registration request: %w", err) + } + body := bytes.NewBuffer(signed) + + endpointURL, err := url.Parse(cli.endpoint) + if err != nil { + return fmt.Errorf("error parsing endpoint %s: %w", cli.endpoint, err) + } + endpointURL.Path = "/api/v1/register" + + res, err := Request(http.MethodPost, endpointURL.String(), nil, body) + if err != nil { + return fmt.Errorf("error registering to broker %s: %w", endpointURL, err) + } + defer res.Body.Close() + + return nil +} diff --git a/cmd/salty-chat/register.go b/cmd/salty-chat/register.go new file mode 100644 index 0000000..b0bc466 --- /dev/null +++ b/cmd/salty-chat/register.go @@ -0,0 +1,66 @@ +package main + +import ( + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "go.mills.io/saltyim" +) + +var registerCmd = &cobra.Command{ + Use: "register", + Aliases: []string{"auth", "reg"}, + Short: "Registers a new account with a broker", + Long: `This command registers a new account with a broker. + +A request is sent to the broker to the registration endpoint with the contents +of the user's public key and salty address, signed with the user's private key. + +If the broker can verify the request was signed correctly by the user a new +account is created and a Well-Known COnfing and Inbox created.`, + Args: cobra.ExactArgs(0), + Run: func(cmd *cobra.Command, args []string) { + user := viper.GetString("user") + endpoint := viper.GetString("endpoint") + identity := viper.GetString("identity") + + var profiles []profile + viper.UnmarshalKey("profiles", &profiles) + for _, p := range profiles { + if user == p.User { + endpoint = p.Endpoint + identity = p.Identity + } + } + + var me saltyim.Addr + if sp := strings.Split(user, "@"); len(sp) > 1 { + me.User = sp[0] + me.Domain = sp[1] + } + + register(me, identity, endpoint) + }, +} + +func init() { + rootCmd.AddCommand(registerCmd) +} + +func register(me saltyim.Addr, identity, endpoint string) { + cli, err := saltyim.NewClient(me, identity, endpoint) + if err != nil { + fmt.Fprintf(os.Stderr, "error initializing client: %s\n", err) + os.Exit(2) + } + + if err := cli.Register(); err != nil { + fmt.Fprintf(os.Stderr, "error registering to %s: %s\n", endpoint, err) + os.Exit(2) + } + fmt.Println("Success!") +} diff --git a/cmd/saltyd/main.go b/cmd/saltyd/main.go index e2c950b..8c13687 100644 --- a/cmd/saltyd/main.go +++ b/cmd/saltyd/main.go @@ -23,7 +23,9 @@ var ( version bool // Basic options - store string + data string + store string + baseURL string ) const ( @@ -47,7 +49,9 @@ func init() { flag.BoolVarP(&version, "version", "v", false, "display version information") // Basic options + flag.StringVarP(&data, "data", "d", internal.DefaultData, "data directory") flag.StringVarP(&store, "store", "s", internal.DefaultStore, "store to use") + flag.StringVarP(&baseURL, "base-url", "u", internal.DefaultBaseURL, "base url to use") } func flagNameFromEnvironmentName(s string) string { @@ -91,7 +95,9 @@ func main() { internal.WithDebug(debug), // Basic options + internal.WithData(data), internal.WithStore(store), + internal.WithBaseURL(baseURL), ) if err != nil { log.WithError(err).Fatal("error creating server") diff --git a/data/.gitkeep b/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/data/.well-known/salty/d3d52221e8da5a8ae012f4e2db0631c181f4156f0edcde5cffa25b347c7ceda8.json b/data/.well-known/salty/d3d52221e8da5a8ae012f4e2db0631c181f4156f0edcde5cffa25b347c7ceda8.json new file mode 100644 index 0000000..87115aa --- /dev/null +++ b/data/.well-known/salty/d3d52221e8da5a8ae012f4e2db0631c181f4156f0edcde5cffa25b347c7ceda8.json @@ -0,0 +1 @@ +{"endpoint":"http://0.0.0.0:8000/inbox/01FYSBPFYJD0RGWFF41RMVYBY3","key":"kex1ekt5cru4vs42wnaxppkjn5pexmt2w6uxx9z2mz0fqeuc80e0g9gsggs8ah"} \ No newline at end of file diff --git a/go.mod b/go.mod index cec39d5..aa52c70 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/sirupsen/logrus v1.8.1 github.com/spf13/cobra v1.4.0 github.com/spf13/viper v1.10.1 - go.mills.io/salty v0.0.0-20220318125419-fb3d6fc9e870 + go.mills.io/salty v0.0.0-20220322161301-ce2b9f6573fa ) require ( @@ -58,7 +58,7 @@ require ( golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/text v0.3.7 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.27.1 // indirect + google.golang.org/protobuf v1.28.0 // indirect gopkg.in/ini.v1 v1.66.4 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 06d199b..1ee3c8d 100644 --- a/go.sum +++ b/go.sum @@ -567,6 +567,8 @@ go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsX go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= go.mills.io/salty v0.0.0-20220318125419-fb3d6fc9e870 h1:fH4ftkY8i0Y2ycstDXmVmqxKyY+l4Gx4OvgxBm/wk8Q= go.mills.io/salty v0.0.0-20220318125419-fb3d6fc9e870/go.mod h1:bQ9yvK7wwThD4tzoioJq/YAuwYOB2XA9tAUHIYtjre8= +go.mills.io/salty v0.0.0-20220322161301-ce2b9f6573fa h1:KBxzYJMWP7MXd72RgqsMCGOSEqV6aaDDSdSb8usJCzQ= +go.mills.io/salty v0.0.0-20220322161301-ce2b9f6573fa/go.mod h1:bQ9yvK7wwThD4tzoioJq/YAuwYOB2XA9tAUHIYtjre8= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -1043,6 +1045,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/api.go b/internal/api.go index 3521395..564f614 100644 --- a/internal/api.go +++ b/internal/api.go @@ -4,7 +4,10 @@ import ( "net/http" "github.com/julienschmidt/httprouter" + log "github.com/sirupsen/logrus" "github.com/unrolled/render" + + "go.mills.io/saltyim" ) // API ... @@ -29,6 +32,7 @@ func (a *API) initRoutes() { router := a.router.Group("/api/v1") router.GET("/ping", a.PingEndpoint()) + router.POST("/register", a.RegisterEndpoint()) } // PingEndpoint ... @@ -38,3 +42,27 @@ func (a *API) PingEndpoint() httprouter.Handle { _, _ = w.Write([]byte(`{}`)) } } + +// RegisterEndpoint ... +func (a *API) RegisterEndpoint() httprouter.Handle { + return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + req, signer, err := saltyim.NewRegisterRequest(r.Body) + if err != nil { + log.WithError(err).Error("error parsing register request") + http.Error(w, "Bad Request", http.StatusBadRequest) + return + } + if signer != req.Key { + http.Error(w, "Bad Request", http.StatusBadRequest) + return + } + + if err := CreateConfig(a.config, req.Addr, req.Key); err != nil { + log.WithError(err).Errorf("error creating config for %s", req.Addr) + http.Error(w, "Error", http.StatusInternalServerError) + return + } + + http.Error(w, "Account Created", http.StatusCreated) + } +} diff --git a/internal/config.go b/internal/config.go index 9af32a5..92a73a1 100644 --- a/internal/config.go +++ b/internal/config.go @@ -1,14 +1,31 @@ package internal +import ( + "net/url" + + log "github.com/sirupsen/logrus" +) + // Config contains the server configuration parameters type Config struct { - Debug bool - Store string + Debug bool + Data string + Store string + BaseURL string + + baseURL *url.URL } // Validate validates the configuration is valid which for the most part // just ensures that default secrets are actually configured correctly func (c *Config) Validate() error { + // Automatically correct missing Scheme in Pod Base URL + if c.baseURL.Scheme == "" { + log.Warnf("base url (-u/--base-url) %s is missing the scheme", c.BaseURL) + c.baseURL.Scheme = "http" + c.BaseURL = c.baseURL.String() + } + if c.Debug { return nil } diff --git a/internal/options.go b/internal/options.go index 3b45e04..b4b5475 100644 --- a/internal/options.go +++ b/internal/options.go @@ -1,5 +1,7 @@ package internal +import "net/url" + const ( // InvalidConfigValue is the constant value for invalid config values // which must be changed for production configurations before successful @@ -9,14 +11,21 @@ const ( // DefaultDebug is the default debug mode DefaultDebug = false + // DefaultData is the default data directory for storage + DefaultData = "./data" + // DefaultStore is the default data store used for accounts, sessions, etc DefaultStore = "bitcask://saltyim.db" + + // DefaultBaseURL is the default Base URL for the server + DefaultBaseURL = "http://0.0.0.0:8000" ) func NewConfig() *Config { return &Config{ - Debug: DefaultDebug, - Store: DefaultStore, + Debug: DefaultDebug, + Store: DefaultStore, + BaseURL: DefaultBaseURL, } } @@ -31,6 +40,14 @@ func WithDebug(debug bool) Option { } } +// WithData sets the data directory to use for storage +func WithData(data string) Option { + return func(cfg *Config) error { + cfg.Data = data + return nil + } +} + // WithStore sets the store to use for accounts, sessions, etc. func WithStore(store string) Option { return func(cfg *Config) error { @@ -38,3 +55,16 @@ func WithStore(store string) Option { return nil } } + +// WithBaseURL sets the Base URL used for constructing feed URLs +func WithBaseURL(baseURL string) Option { + return func(cfg *Config) error { + u, err := url.Parse(baseURL) + if err != nil { + return err + } + cfg.BaseURL = baseURL + cfg.baseURL = u + return nil + } +} diff --git a/internal/tasks.go b/internal/tasks.go new file mode 100644 index 0000000..2116cb0 --- /dev/null +++ b/internal/tasks.go @@ -0,0 +1,47 @@ +package internal + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "go.mills.io/saltyim" +) + +const ( + wellknownPath = ".well-known/salty" +) + +func CreateConfig(conf *Config, addr saltyim.Addr, key string) error { + p := filepath.Join(conf.Data, wellknownPath) + fn := filepath.Join(p, fmt.Sprintf("%s.json", addr.Hash())) + + if err := os.MkdirAll(p, 0755); err != nil { + return fmt.Errorf("error creating config paths %s: %w", p, err) + } + + ulid, err := saltyim.GenerateULID() + if err != nil { + return fmt.Errorf("error generating ulid") + } + + endpointURL := *conf.baseURL + endpointURL.Path = fmt.Sprintf("/inbox/%s", ulid) + + config := saltyim.Config{ + Endpoint: endpointURL.String(), + Key: key, + } + + data, err := json.Marshal(config) + if err != nil { + return fmt.Errorf("error serializing config") + } + + if err := os.WriteFile(fn, data, 0644); err != nil { + return fmt.Errorf("error writing config to %s: %w", fn, err) + } + + return nil +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..4fc6aff --- /dev/null +++ b/types.go @@ -0,0 +1,32 @@ +package saltyim + +import ( + "encoding/json" + "io" + "io/ioutil" + + "go.mills.io/salty" +) + +// RegisterRequest is the request used by clients to register to a broker +type RegisterRequest struct { + Addr Addr + Key string +} + +// NewRegisterRequest reads the signed request body from a client, verifies its signature +// and returns the resulting `RegisterRequest` and key used to sign the request on success +// otherwise an empty object and en error on failure. +func NewRegisterRequest(r io.Reader) (req RegisterRequest, signer string, err error) { + body, err := ioutil.ReadAll(r) + if err != nil { + return + } + out, key, err := salty.Verify(body) + if err != nil { + return + } + signer = key.ID().String() + err = json.Unmarshal(out, &req) + return +}