6
1
mirror of https://git.mills.io/saltyim/saltyim.git synced 2024-06-25 00:08:26 +00:00

Add make-user command (#9)

Co-authored-by: James Mills <prologic@shortcircuit.net.au>
Reviewed-on: https://git.mills.io/prologic/saltyim/pulls/9
This commit is contained in:
James Mills 2022-03-19 11:42:36 +00:00
parent 005a013753
commit 8f0914ef22
9 changed files with 225 additions and 30 deletions

BIN
.salty-chat.sh.swp Normal file

Binary file not shown.

@ -0,0 +1,98 @@
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.mills.io/saltyim"
)
const (
postSetupInstructions = `Create this file and place it on your web server
so that it is accessible at a top-level domain or sub-domain at the URL:
{{ .Addr.HashURI }}
{
"endpoint": "{{ .Config.Endpoint }}",
"key": "{{ .Config.Key }}"
}
To verify you have done this correctly:
$ salty-chat lookup {{ .Addr }}`
)
type setupCtx struct {
Config saltyim.Config
Addr saltyim.Addr
}
var makeuserCmd = &cobra.Command{
Use: "make-user nick@domain",
Aliases: []string{"mkuser", "setup"},
Short: "Creates a new Salty User",
Long: `This command creates a new Salty User and Key Pair storing the
Private Key in either $XDG_CONFIG_HOME/salty/ or $HOME/.salty/ as well as
information on how to setup the Well-Known Config and URI for Discovery by
others Salty IM Users.
A valid top-level domain or sub-domain is required and the <user> is in the form of:
username@domain`,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
uri := viper.GetString("uri")
user := args[0]
makeuser(uri, user)
},
}
func init() {
rootCmd.AddCommand(makeuserCmd)
}
func makeuser(uri, user string) {
if _, err := saltyim.ParseAddr(user); err != nil {
fmt.Fprintf(os.Stderr, "error parsing user %q: %s\n", user, err)
os.Exit(2)
}
config := saltyim.GetConfigDir()
if config == "" {
log.Fatal("unable to find a suitable configurtion directory")
}
if err := os.MkdirAll(config, 0700); err != nil {
fmt.Fprintf(os.Stderr, "error creating configuration directory %s: %s", config, err)
os.Exit(2)
}
identity := filepath.Join(config, fmt.Sprintf("%s.key", user))
if err := saltyim.CreateIdentity(identity, user); err != nil {
fmt.Fprintf(os.Stderr, "error creating identity %s for %s: %s", identity, user, err)
os.Exit(2)
}
key, me, err := saltyim.GetIdentity(identity)
if err != nil {
fmt.Fprintf(os.Stderr, "error reading identity %s for %s: %s", identity, user, err)
os.Exit(2)
}
ctx := setupCtx{
Config: saltyim.Config{
Endpoint: fmt.Sprintf("%s/%s", uri, me.User),
Key: key.PublicKey().ID().String(),
},
Addr: me,
}
fmt.Println(saltyim.MustRenderString(postSetupInstructions, ctx))
}

@ -1,31 +1,8 @@
package saltyim
import (
"bufio"
"fmt"
"io"
"strings"
)
// Config represents a Salty Config for a User which at a minimum is required
// to have an Endpoint and Key (Public Key)
type Config struct {
Endpoint string `json:"endpoint"`
Key string `json:"key"`
}
func ReadUser(fd io.Reader) (Addr, error) {
scan := bufio.NewScanner(fd)
var a Addr
for scan.Scan() {
if strings.HasPrefix(scan.Text(), "# user:") {
user := strings.Split(strings.TrimSpace(strings.TrimPrefix(scan.Text(), "# user:")), "@")
if len(user) != 2 {
return Addr{}, fmt.Errorf("user not found")
}
a.User, a.Domain = user[0], user[1]
}
}
return a, scan.Err()
}

1
go.mod

@ -12,6 +12,7 @@ require (
require (
git.mills.io/prologic/msgbus v0.1.3-0.20220319044708-6df82dc484d7
github.com/Masterminds/sprig/v3 v3.2.2
github.com/buger/goterm v1.0.4
github.com/gen2brain/beeep v0.0.0-20210529141713-5586760f0cc1
github.com/gorilla/websocket v1.5.0 // indirect

20
go.sum

@ -50,6 +50,12 @@ git.mills.io/prologic/msgbus v0.1.3-0.20220319044708-6df82dc484d7/go.mod h1:mWGX
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8=
github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5 h1:VauE2GcJNZFun2Och6tIT2zJZK1v6jxALQDA9BIji/E=
github.com/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5/go.mod h1:gxOHeajFfvGQh/fxlC8oOKBe23xnnJTif00IFFbiT+o=
@ -223,6 +229,8 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
@ -267,9 +275,13 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs=
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
@ -332,6 +344,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@ -340,6 +354,8 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs=
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mmcloughlin/professor v0.0.0-20170922221822-6b97112ab8b3/go.mod h1:LQkXsHRSPIEklPCq8OMQAzYNS2NGtYStdNE/ej1oJU8=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -402,6 +418,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sagikazarmark/crypt v0.4.0/go.mod h1:ALv2SRj7GxYV4HO9elxH9nS6M9gW+xDNxqmyJ6RfDFM=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
@ -413,6 +431,7 @@ github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY52
github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
@ -487,6 +506,7 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200427165652-729f1e841bcc/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=

@ -1,21 +1,58 @@
package saltyim
import (
"bufio"
"fmt"
"log"
"io"
"os"
"path/filepath"
"strings"
"github.com/keys-pub/keys"
"go.mills.io/salty"
)
var (
defaultConfigPaths = []string{
"$HOME/.config/salty/",
"$HOME/.salty/",
}
defaultIdentityPaths = []string{
"$XDG_CONFIG_HOME/.salty/$USER.key",
"$HOME/.config/salty/$USER.key",
"$HOME/.salty/$USER.key",
}
)
func readUser(fd io.Reader) (Addr, error) {
scan := bufio.NewScanner(fd)
var a Addr
for scan.Scan() {
if strings.HasPrefix(scan.Text(), "# user:") {
user := strings.Split(strings.TrimSpace(strings.TrimPrefix(scan.Text(), "# user:")), "@")
if len(user) != 2 {
return Addr{}, fmt.Errorf("user not found")
}
a.User, a.Domain = user[0], user[1]
}
}
return a, scan.Err()
}
// GetConfigDir returns a suitable configuration directory
func GetConfigDir() string {
if configDir := os.Getenv("XDG_CONFIG_HOME"); configDir != "" {
return filepath.Join(configDir, "salty")
}
for _, p := range defaultConfigPaths {
if p := os.ExpandEnv(p); DirExists(p) || DirExists(filepath.Join(p, "..")) {
return p
}
}
return ""
}
// DefaultIdentity returns a default identity file (if one exists) otherwise
// returns an empty string
func DefaultIdentity() string {
@ -27,21 +64,44 @@ func DefaultIdentity() string {
return ""
}
// CreateIdentity ...
func CreateIdentity(fn, user string) error {
f, err := os.OpenFile(fn, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
if err != nil {
return fmt.Errorf("error opening identity %s for writing: %w", fn, err)
}
defer f.Close()
salty.GenerateKeys(f)
f.Write([]byte(fmt.Sprintf("# user: %s\n", user)))
if err := f.Sync(); err != nil {
return fmt.Errorf("error syncing identity %s for writing: %w", fn, err)
}
if err := f.Close(); err != nil {
return fmt.Errorf("error closing identity %s for writing: %w", fn, err)
}
return nil
}
// GetIdentity ...
func GetIdentity(fn string) (*keys.EdX25519Key, Addr, error) {
id, err := os.Open(fn)
if err != nil {
log.Fatalf("error opening identity file: %q", fn)
return nil, Addr{}, fmt.Errorf("error opening identity file: %q", fn)
}
defer id.Close()
key, err := salty.ParseIdentity(id)
if err != nil {
log.Fatalf("error reading private key: %q", fn)
return nil, Addr{}, fmt.Errorf("error reading private key: %q", fn)
}
id.Seek(0, 0)
me, err := ReadUser(id)
me, err := readUser(id)
if err != nil {
return key, Addr{}, fmt.Errorf("error reading user from keyfile: %q", fn)
}

@ -23,7 +23,7 @@ func (a Addr) IsZero() bool {
}
func (a Addr) String() string {
return fmt.Sprint(a.User, "@", a.Domain)
return fmt.Sprintf("%s@%s", a.User, a.Domain)
}
// Formatted returns a formatted user used in the Salty Message Format
@ -48,7 +48,7 @@ func (a Addr) HashURI() string {
func ParseAddr(addr string) (Addr, error) {
parts := strings.Split(addr, "@")
if len(parts) != 2 {
return Addr{}, fmt.Errorf("error parsing addr %q, expected nick@domain", addr)
return Addr{}, fmt.Errorf("expected nick@domain found %q", addr)
}
return Addr{parts[0], parts[1]}, nil

@ -1,12 +1,15 @@
package saltyim
import (
"bytes"
"fmt"
"html/template"
"io"
"net/http"
"os"
"time"
"github.com/Masterminds/sprig/v3"
log "github.com/sirupsen/logrus"
)
@ -14,6 +17,26 @@ const (
defaultRequestTimeout = time.Second * 5
)
func MustRenderString(s string, ctx interface{}) string {
out, err := RenderString(s, ctx)
if err != nil {
log.WithError(err).Fatal("error rendering string template")
}
return out
}
// DirExists returns true if the given directory exists
func DirExists(name string) bool {
stat, err := os.Stat(name)
if err != nil {
if os.IsNotExist(err) {
return false
}
return false
}
return stat.IsDir()
}
// FileExists returns true if the given file exists
func FileExists(name string) bool {
if _, err := os.Stat(name); err != nil {
@ -24,6 +47,22 @@ func FileExists(name string) bool {
return true
}
// RenderString renders the string `s` as a `text/template` using the provided
// context `ctx` as input into the template.
// Typically used to render the results into a Markdown document.
func RenderString(s string, ctx interface{}) (string, error) {
t := template.Must(
template.New("s").Funcs(sprig.FuncMap()).Parse(s),
)
buf := bytes.NewBuffer([]byte{})
err := t.Execute(buf, ctx)
if err != nil {
return "", err
}
return buf.String(), nil
}
// Request is a generic request handling function for making artbitrary HTPT
// requests to Salty endpoints for looking up Salty Addresses, Configs and
// publishing encrypted messages.