6
1
mirror of https://git.mills.io/saltyim/saltyim.git synced 2024-06-27 09:18:22 +00:00

Add support for client and server (broker) registration (#43)

Co-authored-by: James Mills <prologic@shortcircuit.net.au>
Reviewed-on: https://git.mills.io/prologic/saltyim/pulls/43
This commit is contained in:
James Mills 2022-03-22 22:59:09 +00:00
parent 958f4ea4f6
commit 801d6b93bb
12 changed files with 271 additions and 7 deletions

@ -3,8 +3,10 @@ package saltyim
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"os" "os"
"time" "time"
@ -168,3 +170,34 @@ func (cli *Client) Send(user, msg string) error {
return nil 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
}

@ -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!")
}

@ -23,7 +23,9 @@ var (
version bool version bool
// Basic options // Basic options
store string data string
store string
baseURL string
) )
const ( const (
@ -47,7 +49,9 @@ func init() {
flag.BoolVarP(&version, "version", "v", false, "display version information") flag.BoolVarP(&version, "version", "v", false, "display version information")
// Basic options // Basic options
flag.StringVarP(&data, "data", "d", internal.DefaultData, "data directory")
flag.StringVarP(&store, "store", "s", internal.DefaultStore, "store to use") 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 { func flagNameFromEnvironmentName(s string) string {
@ -91,7 +95,9 @@ func main() {
internal.WithDebug(debug), internal.WithDebug(debug),
// Basic options // Basic options
internal.WithData(data),
internal.WithStore(store), internal.WithStore(store),
internal.WithBaseURL(baseURL),
) )
if err != nil { if err != nil {
log.WithError(err).Fatal("error creating server") log.WithError(err).Fatal("error creating server")

0
data/.gitkeep Normal file

@ -0,0 +1 @@
{"endpoint":"http://0.0.0.0:8000/inbox/01FYSBPFYJD0RGWFF41RMVYBY3","key":"kex1ekt5cru4vs42wnaxppkjn5pexmt2w6uxx9z2mz0fqeuc80e0g9gsggs8ah"}

4
go.mod

@ -8,7 +8,7 @@ require (
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.4.0 github.com/spf13/cobra v1.4.0
github.com/spf13/viper v1.10.1 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 ( require (
@ -58,7 +58,7 @@ require (
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.7 // indirect
google.golang.org/appengine v1.6.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/ini.v1 v1.66.4 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
) )

4
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.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 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-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.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 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.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 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.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/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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

@ -4,7 +4,10 @@ import (
"net/http" "net/http"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
log "github.com/sirupsen/logrus"
"github.com/unrolled/render" "github.com/unrolled/render"
"go.mills.io/saltyim"
) )
// API ... // API ...
@ -29,6 +32,7 @@ func (a *API) initRoutes() {
router := a.router.Group("/api/v1") router := a.router.Group("/api/v1")
router.GET("/ping", a.PingEndpoint()) router.GET("/ping", a.PingEndpoint())
router.POST("/register", a.RegisterEndpoint())
} }
// PingEndpoint ... // PingEndpoint ...
@ -38,3 +42,27 @@ func (a *API) PingEndpoint() httprouter.Handle {
_, _ = w.Write([]byte(`{}`)) _, _ = 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)
}
}

@ -1,14 +1,31 @@
package internal package internal
import (
"net/url"
log "github.com/sirupsen/logrus"
)
// Config contains the server configuration parameters // Config contains the server configuration parameters
type Config struct { type Config struct {
Debug bool Debug bool
Store string Data string
Store string
BaseURL string
baseURL *url.URL
} }
// Validate validates the configuration is valid which for the most part // Validate validates the configuration is valid which for the most part
// just ensures that default secrets are actually configured correctly // just ensures that default secrets are actually configured correctly
func (c *Config) Validate() error { 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 { if c.Debug {
return nil return nil
} }

@ -1,5 +1,7 @@
package internal package internal
import "net/url"
const ( const (
// InvalidConfigValue is the constant value for invalid config values // InvalidConfigValue is the constant value for invalid config values
// which must be changed for production configurations before successful // which must be changed for production configurations before successful
@ -9,14 +11,21 @@ const (
// DefaultDebug is the default debug mode // DefaultDebug is the default debug mode
DefaultDebug = false DefaultDebug = false
// DefaultData is the default data directory for storage
DefaultData = "./data"
// DefaultStore is the default data store used for accounts, sessions, etc // DefaultStore is the default data store used for accounts, sessions, etc
DefaultStore = "bitcask://saltyim.db" DefaultStore = "bitcask://saltyim.db"
// DefaultBaseURL is the default Base URL for the server
DefaultBaseURL = "http://0.0.0.0:8000"
) )
func NewConfig() *Config { func NewConfig() *Config {
return &Config{ return &Config{
Debug: DefaultDebug, Debug: DefaultDebug,
Store: DefaultStore, 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. // WithStore sets the store to use for accounts, sessions, etc.
func WithStore(store string) Option { func WithStore(store string) Option {
return func(cfg *Config) error { return func(cfg *Config) error {
@ -38,3 +55,16 @@ func WithStore(store string) Option {
return nil 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
}
}

47
internal/tasks.go Normal file

@ -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
}

32
types.go Normal file

@ -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
}