6
1
mirror of https://git.mills.io/saltyim/saltyim.git synced 2024-06-16 03:48:24 +00:00
Co-authored-by: Jon Lundy <jon@xuu.cc>
Co-authored-by: James Mills <prologic@shortcircuit.net.au>
Reviewed-on: https://git.mills.io/saltyim/saltyim/pulls/64
Co-authored-by: xuu <xuu@noreply@mills.io>
Co-committed-by: xuu <xuu@noreply@mills.io>
This commit is contained in:
xuu 2022-03-26 14:43:05 +00:00 committed by James Mills
parent af290700b4
commit 38a6d71644
17 changed files with 317 additions and 58 deletions

3
.gitignore vendored

@ -15,3 +15,6 @@
/salty-chat
/cmd/saltyd/saltyd
/cmd/salty-chat/salty-chat
/data/*.key
/data/.wellt-known

@ -147,16 +147,14 @@ lookup () {
readmsgs () {
topic="$1"
if [ -z "$topic" ]; then
me="$(get_user)"
topic="$(echo "$me" | awk -F@ '{ print $1 }')"
fi
salty_json="$(mktemp /tmp/salty.XXXXXX)"
lookup "$user" > "$salty_json"
endpoint="$(jq -r '.endpoint' < "$salty_json")"
key="$(jq -r '.key' < "$salty_json")"
rm "$salty_json"
export SALTY_IDENTITY="$data_path/$topic.key"
if [ ! -f "$SALTY_IDENTITY" ]; then
echo "identity file missing for user $topic" >&2
return 1
fi
export MSGBUS_URI=$(dirname "$endpoint")
topic=$(basename "$endpoint")
msgbus sub "$topic" "$0"
}
@ -188,12 +186,6 @@ sendmsg () {
endpoint="$(jq -r '.endpoint' < "$salty_json")"
key="$(jq -r '.key' < "$salty_json")"
# XXX: Deprecated as of Spec v1.1
# XXX: There is only an Endpoint
if topic="$(jq -e -r '.topic' < "$salty_json")"; then
endpoint="$endpoint/$topic"
fi
rm "$salty_json"
me="$(get_user)"
@ -224,12 +216,6 @@ chatwith() {
endpoint="$(jq -r '.endpoint' < "$salty_json")"
key="$(jq -r '.key' < "$salty_json")"
# XXX: Deprecated as of Spec v1.1
# XXX: There is only an Endpoint
if topic="$(jq -e -r '.topic' < "$salty_json")"; then
endpoint="$endpoint/$topic"
fi
rm "$salty_json"
me="$(get_user)"
@ -240,8 +226,8 @@ chatwith() {
export SALTY_CHATKEY="$key"
echo chat with "$SALTY_CHATWITH" via "$endpoint" key "$key"
readmsgs "$nick" &
READ_PID=$$
readmsgs &
READ_PID=$!
trap 'kill $READ_PID' EXIT
while true; do
@ -302,6 +288,51 @@ Create this file in your webserver well-known folder. https://hostname.tld/.well
EOF
}
register () {
mkdir -p "$data_path"
if [ $# -lt 1 ]; then
printf "usage: %s register nick@domain\n" "$0"
exit 1
else
user=$1
fi
nick="$(echo "$user" | awk -F@ '{ print $1 }')"
domain="$(echo "$user" | awk -F@ '{ print $2 }')"
discovery_host="$(dig +short SRV _salty._tcp."$domain" | cut -f 4 -d' ')"
if [ -z "$discovery_host" ]; then
discovery_host="$domain"
fi
identity_file="$data_path/$nick.key"
if [ -f "$identity_file" ]; then
printf "user key already exists!"
return 1
fi
# Check for msgbus env.. probably can make it fallback to looking for a config file?
if [ -z "$MSGBUS_URI" ]; then
printf "missing MSGBUS_URI in environment"
return 1
fi
salty-keygen -o "$identity_file"
echo "# user: $user" >> "$identity_file"
pubkey=$(grep key: "$identity_file" | awk '{print $4}')
export SALTY_IDENTITY="$identity_file"
msgbus sub "$pubkey" "$0" &
pid="$!"
sendmsg "salty@$discovery_host" REGISTER
sleep 1
kill "$pid"
}
show_help() {
printf "Usage: %s [options] <command> [arguments]\n" "$(basename "$0")"
printf "\n"
@ -317,6 +348,7 @@ show_help() {
printf " make-user -- Generate a new user key pair\n"
printf " read -- Reads your messages\n"
printf " send -- Sends a message to nick@domain\n"
printf " register -- Sends a register message to a broker bot\n"
}
# check if streaming
@ -357,4 +389,7 @@ case $CMD in
make-user)
make_user "$@"
;;
register)
register "$@"
;;
esac

@ -3,6 +3,7 @@ package saltyim
import (
"bytes"
"context"
"errors"
"fmt"
"net/http"
"os"
@ -45,7 +46,15 @@ type Client struct {
cache addrCache
}
// NewClient returns a new Salty IM client for sending and receiving
func (c *Client) String() string {
b := &bytes.Buffer{}
fmt.Fprintln(b, "Me: ", c.me)
fmt.Fprintln(b, "Endpoint: ", c.me.Endpoint())
fmt.Fprintln(b, "Key: ", c.key)
return b.String()
}
// NewClient reeturns a new Salty IM client for sending and receiving
// encrypted messages to other Salty IM users as well as decrypting
// and displaying messages of the user's own inbox.
func NewClient(me *Addr, options ...IdentityOption) (*Client, error) {
@ -76,6 +85,27 @@ func NewClient(me *Addr, options ...IdentityOption) (*Client, error) {
}, nil
}
// CreateOrLoadClient creates a Client by creating or loading an existing identity
// from the given identity file and name of the client's user address
func CreateOrLoadBotClient(fn, name string) (*Client, error) {
me, err := ParseAddr(name)
if err != nil {
return nil, err
}
if _, err := os.Stat(fn); err == nil {
return NewClient(me, WithIdentityPath(fn))
} else if !errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("failed to load client: %w", err)
}
identity, err := CreateIdentity(WithIdentityPath(fn), WithIdentityAddr(me))
if err != nil {
return nil, fmt.Errorf("failed to create identity: %w", err)
}
return NewClient(me, WithIdentityBytes(identity.Contents()))
}
func (cli *Client) getAddr(user string) (*Addr, error) {
addr, ok := cli.cache[user]
if ok {
@ -92,7 +122,12 @@ func (cli *Client) getAddr(user string) (*Addr, error) {
return addr, nil
}
func (cli *Client) handleMessage(prehook, posthook string, msgs chan string) msgbus.HandlerFunc {
type Message struct {
Text string
Key *keys.EdX25519PublicKey
}
func (cli *Client) handleMessage(prehook, posthook string, msgs chan Message) msgbus.HandlerFunc {
return func(msg *msgbus.Message) error {
if prehook != "" {
out, err := exec.RunCmd(exec.DefaultRunCmdTimeout, prehook, bytes.NewBuffer(msg.Payload))
@ -102,13 +137,13 @@ func (cli *Client) handleMessage(prehook, posthook string, msgs chan string) msg
log.Debugf("pre-hook: %q", out)
}
data, _, err := salty.Decrypt(cli.key, msg.Payload)
data, senderKey, err := salty.Decrypt(cli.key, msg.Payload)
if err != nil {
fmt.Fprintf(os.Stderr, "error decrypting message")
return err
}
msgs <- string(data)
msgs <- Message{Text: string(data), Key: senderKey}
if posthook != "" {
out, err := exec.RunCmd(exec.DefaultRunCmdTimeout, posthook, bytes.NewBuffer(data))
@ -126,11 +161,11 @@ func (cli *Client) Me() *Addr { return cli.me }
func (cli *Client) Key() *keys.EdX25519PublicKey { return cli.key.PublicKey() }
// Read subscribers to this user's inbox for new messages
func (cli *Client) Read(ctx context.Context, prehook, posthook string) chan string {
func (cli *Client) Read(ctx context.Context, prehook, posthook string) chan Message {
uri, inbox := SplitInbox(cli.me.Endpoint().String())
bus := msgbus_client.NewClient(uri, nil)
msgs := make(chan string)
msgs := make(chan Message)
s := bus.Subscribe(inbox, cli.handleMessage(prehook, posthook, msgs))
s.Start()
@ -152,12 +187,17 @@ func (cli *Client) Send(user, msg string) error {
return fmt.Errorf("error looking up user %s: %w", user, err)
}
b, err := salty.Encrypt(cli.key, PackMessage(cli.me, msg), []string{addr.Key().ID().String()})
return cli.SendWithConfig(user, &Config{Endpoint: addr.Endpoint().String(), Key: addr.key.String()}, msg)
}
func (cli *Client) SendWithConfig(user string, config *Config, msg string) error {
b, err := salty.Encrypt(cli.key, PackMessage(cli.me, msg), []string{config.Key})
if err != nil {
return fmt.Errorf("error encrypting message to %s: %w", user, err)
}
if err := Send(addr.Endpoint().String(), string(b)); err != nil {
log.Println("SEND: ", config.Endpoint, user, msg)
if err := Send(config.Endpoint, string(b)); err != nil {
return fmt.Errorf("error sending message to %s: %w", user, err)
}

@ -50,7 +50,7 @@ others Salty IM Users.
A valid top-level domain or sub-domain is required and the <user> is in the form of:
username@domain
If the -u/--endpoint flag or <endpoint> argument is passed this is used as the broker
endpoint for constructing the Well-Known Config, if neither is used the broker is
assumed to be the domain part of the nick@domain <user> argument.

@ -93,7 +93,7 @@ func read(me *saltyim.Addr, identity string, prehook, posthook string, args ...s
for msg := range cli.Read(ctx, prehook, posthook) {
if term.IsTerminal(syscall.Stdin) {
fmt.Println(saltyim.FormatMessage(msg))
fmt.Println(saltyim.FormatMessage(msg.Text))
} else {
fmt.Println(msg)
}

@ -29,9 +29,10 @@ var (
tlsCert string
// Basic options
data string
store string
baseURL string
data string
store string
baseURL string
servicesUser string
// Oeprator
adminUser string
@ -67,6 +68,7 @@ func init() {
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")
flag.StringVarP(&servicesUser, "services-user", "S", internal.DefaultServicesUser, "internal services user")
// Oeprator
flag.StringVarP(&adminUser, "admin-user", "A", internal.DefaultAdminUser, "default admin user to use")
@ -125,6 +127,7 @@ func main() {
internal.WithData(data),
internal.WithStore(store),
internal.WithBaseURL(baseURL),
internal.WithServicesUser(servicesUser),
// Oeprator
internal.WithAdminUser(adminUser),

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

@ -101,19 +101,19 @@ func GetIdentity(options ...IdentityOption) (*Identity, error) {
id, err := os.Open(fn)
if err != nil {
return ident, fmt.Errorf("error opening identity %q: %s", ident.Source(), err)
return ident, fmt.Errorf("error opening identity %q: %w", ident.Source(), err)
}
defer id.Close()
identityBytes, err := ioutil.ReadAll(id)
if err != nil {
return ident, fmt.Errorf("error opening identity %q: %s", ident.Source(), err)
return ident, fmt.Errorf("error opening identity %q: %w", ident.Source(), err)
}
ident.contents = identityBytes
}
key, err := salty.ParseIdentity(bytes.NewBuffer(ident.contents))
if err != nil {
return ident, fmt.Errorf("error reading identity %q: %s", ident.Source(), err)
return ident, fmt.Errorf("error reading identity %q: %w", ident.Source(), err)
}
ident.key = key
@ -142,7 +142,7 @@ type Identity struct {
}
func (i *Identity) Source() string {
if i.path != "" {
if i != nil && i.path != "" {
return i.path
}
return "[]byte"

@ -57,7 +57,7 @@ func (a *API) RegisterEndpoint() httprouter.Handle {
return
}
if err := CreateConfig(a.config, req.Addr, req.Key); err != nil {
if err := CreateConfig(a.config, req.Addr.Hash(), req.Key); err != nil {
log.WithError(err).Errorf("error creating config for %s", req.Addr.String())
http.Error(w, "Error", http.StatusInternalServerError)
return

@ -14,9 +14,10 @@ type Config struct {
TLSKey string `json:"-"`
TLSCert string `json:"-"`
Data string `json:"-"`
Store string `json:"-"`
BaseURL string
Data string `json:"-"`
Store string `json:"-"`
BaseURL string
ServicesUser string
AdminUser string `json:"-"`
AdminEmail string `json:"-"`

@ -1,6 +1,8 @@
package internal
import "net/url"
import (
"net/url"
)
const (
// InvalidConfigValue is the constant value for invalid config values
@ -29,6 +31,9 @@ const (
// DefaultBaseURL is the default Base URL for the server
DefaultBaseURL = "http://0.0.0.0:8000"
// DefaultServicesUser is the default user for internal services registrations and other operations
DefaultServicesUser = "salty@localhost"
// DefaultAdminUser is the default publickye to grant admin privileges to
DefaultAdminUser = ""
@ -125,3 +130,11 @@ func WithAdminEmail(adminEmail string) Option {
return nil
}
}
// WithServicesUser sets the internal services user
func WithServicesUser(user string) Option {
return func(c *Config) error {
c.ServicesUser = user
return nil
}
}

@ -91,7 +91,7 @@ func (h *SaltyChat) connect(ctx app.Context) {
for msg := range client.Read(context.Background(), "", "") {
log.Println("incoming message", msg)
ctx.Dispatch(func(ctx app.Context) {
h.incomingMessage(ctx, msg)
h.incomingMessage(ctx, msg.Text)
})
}
})

@ -5,8 +5,8 @@ import (
"fmt"
"net"
"net/http"
"os"
"os/signal"
"path"
"path/filepath"
"syscall"
"time"
@ -16,18 +16,21 @@ import (
"github.com/NYTimes/gziphandler"
"github.com/julienschmidt/httprouter"
"github.com/justinas/nosurf"
"github.com/keys-pub/keys"
"github.com/maxence-charriere/go-app/v9/pkg/app"
"github.com/robfig/cron"
log "github.com/sirupsen/logrus"
"github.com/unrolled/logger"
"go.mills.io/saltyim/internal/pwa/routes"
"go.yarn.social/lextwt"
"golang.org/x/crypto/acme/autocert"
"go.mills.io/saltyim"
"go.mills.io/saltyim/internal/pwa/routes"
)
const (
acmeDir = "acme"
acmeDir = "acme"
servicesIdentity = "svc.key"
)
var (
@ -52,6 +55,9 @@ type Server struct {
// Message Bus
bus *msgbus.MessageBus
// Services User
svc *saltyim.Service
// Data Store
db Store
@ -100,10 +106,11 @@ func (s *Server) Run() (err error) {
}
}()
sigch := make(chan os.Signal, 1)
signal.Notify(sigch, syscall.SIGINT, syscall.SIGTERM)
sig := <-sigch
log.Infof("Received signal %s", sig)
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()
<-ctx.Done()
log.Infof("Received signal %s", ctx.Err())
log.Info("Shutting down...")
@ -214,6 +221,49 @@ func (s *Server) setupCronJobs() error {
return nil
}
func (s *Server) setupServices() {
time.Sleep(time.Second * 5)
log.Infof("starting services %s", s.config.ServicesUser)
// create or load client for services user
identity := filepath.Join(s.config.Data, servicesIdentity)
cli, err := saltyim.CreateOrLoadBotClient(identity, s.config.ServicesUser)
if err != nil {
log.WithError(err).Errorf("error creating or loading service user")
return
}
svc, err := saltyim.NewService(cli)
if err != nil {
log.WithError(err).Errorf("error creating service")
return
}
s.svc = svc
err = CreateConfig(s.config, cli.Me().Hash(), cli.Me().Key().ID().String())
if err != nil {
log.WithError(err).Error("error creating service config")
return
}
svc.TextFunc("register", func(ctx context.Context, bot *saltyim.Service, key *keys.EdX25519PublicKey, msg *lextwt.SaltyText) error {
addr, err := saltyim.ParseAddr(msg.User.String())
if err != nil {
return err
}
err = CreateConfig(s.config, addr.Hash(), key.String())
if err != nil {
return err
}
return bot.SendWithConfig(msg.User.String(), &saltyim.Config{
Endpoint: s.config.BaseURL + "/" + path.Join("inbox", key.String()),
Key: key.String(),
}, "OK")
})
}
func (s *Server) runStartupJobs() {
time.Sleep(time.Second * 5)
@ -361,11 +411,16 @@ func NewServer(bind string, options ...Option) (*Server, error) {
// Log interesting configuration options
log.Infof("Debug: %t", server.config.Debug)
log.Infof("Debug: %t", server.config.Debug)
log.Infof("Admin User: %s", server.config.AdminUser)
log.Infof("Admin Email: %s", server.config.AdminEmail)
log.Infof("Service Users; %s", server.config.ServicesUser)
api.initRoutes()
server.initRoutes()
go server.runStartupJobs()
go server.setupServices()
return server, nil
}

@ -13,9 +13,9 @@ const (
wellknownPath = ".well-known/salty"
)
func CreateConfig(conf *Config, addr saltyim.Addr, key string) error {
func CreateConfig(conf *Config, hash string, key string) error {
p := filepath.Join(conf.Data, wellknownPath)
fn := filepath.Join(p, fmt.Sprintf("%s.json", addr.Hash()))
fn := filepath.Join(p, fmt.Sprintf("%s.json", hash))
if err := os.MkdirAll(p, 0755); err != nil {
return fmt.Errorf("error creating config paths %s: %w", p, err)

@ -33,6 +33,7 @@ type ChatTUI struct {
cli *saltyim.Client
user string
addr *saltyim.Addr
config *saltyim.Config
// Configurations.
palette map[string]string
@ -158,7 +159,7 @@ func (c *ChatTUI) RunChat(inCh chan<- string, outCh <-chan string) {
// Receives incoming messages on a separate goroutine to be non-blocking.
go func() {
for msg := range c.cli.Read(ctx, "", "") {
inCh <- msg
inCh <- msg.Text
}
}()

109
service.go Normal file

@ -0,0 +1,109 @@
package saltyim
import (
"bytes"
"context"
"fmt"
"strings"
"sync"
"github.com/keys-pub/keys"
log "github.com/sirupsen/logrus"
"go.yarn.social/lextwt"
)
type Service struct {
*Client
mu sync.RWMutex
textFns map[string]MessageTextHandlerFunc
eventFns map[string]MessageEventHandlerFunc
}
type MessageTextHandlerFunc func(context.Context, *Service, *keys.EdX25519PublicKey, *lextwt.SaltyText) error
type MessageEventHandlerFunc func(context.Context, *Service, *keys.EdX25519PublicKey, *lextwt.SaltyEvent) error
func NewService(client *Client) (*Service, error) {
svc := &Service{
Client: client,
textFns: make(map[string]MessageTextHandlerFunc),
eventFns: make(map[string]MessageEventHandlerFunc),
}
svc.TextFunc("ping", func(ctx context.Context, svc *Service, key *keys.EdX25519PublicKey, msg *lextwt.SaltyText) error {
return svc.Send(msg.User.String(), "Pong!")
})
return svc, nil
}
func (svc *Service) String() string {
buf := &bytes.Buffer{}
fmt.Fprintln(buf, "Bot: ", svc.Client.me)
svc.mu.RLock()
defer svc.mu.RUnlock()
for k := range svc.textFns {
fmt.Fprintln(buf, " - TextCmd: ", k)
}
for k := range svc.eventFns {
fmt.Fprintln(buf, " - EventCmd: ", k)
}
return buf.String()
}
func (svc *Service) Run(ctx context.Context) {
log.Println("listining for bot: ", svc.Me().Endpoint())
msgch := svc.Read(ctx, "", "")
for {
select {
case <-ctx.Done():
return
case msg := <-msgch:
if err := svc.handle(ctx, msg); err != nil {
log.WithError(err).Println("failed to handle message")
}
}
}
}
func (svc *Service) handle(ctx context.Context, msg Message) error {
decoded, err := lextwt.ParseSalty(msg.Text)
if err != nil {
return err
}
switch m := decoded.(type) {
case *lextwt.SaltyText:
fields := strings.Fields(m.LiteralText())
svc.mu.RLock()
defer svc.mu.RUnlock()
if fn, ok := svc.textFns[strings.ToUpper(fields[0])]; ok {
err = fn(ctx, svc, msg.Key, m)
}
case *lextwt.SaltyEvent:
svc.mu.RLock()
defer svc.mu.RUnlock()
if fn, ok := svc.eventFns[strings.ToUpper(m.Command)]; ok {
err = fn(ctx, svc, msg.Key, m)
}
}
return err
}
func (svc *Service) TextFunc(name string, fn MessageTextHandlerFunc) {
svc.mu.Lock()
defer svc.mu.Unlock()
svc.textFns[strings.ToUpper(name)] = fn
}
func (svc *Service) EventFunc(name string, fn MessageEventHandlerFunc) {
svc.mu.Lock()
defer svc.mu.Unlock()
svc.eventFns[strings.ToUpper(name)] = fn
}

@ -10,7 +10,7 @@ import (
// RegisterRequest is the request used by clients to register to a broker
type RegisterRequest struct {
Addr Addr
Addr *Addr
Key string
}