2022-03-18 14:48:59 +00:00
|
|
|
package saltyim
|
|
|
|
|
|
|
|
import (
|
2022-03-18 22:50:04 +00:00
|
|
|
"crypto/sha256"
|
2022-03-18 14:48:59 +00:00
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
2022-03-22 22:59:45 +00:00
|
|
|
"net"
|
2022-03-18 14:48:59 +00:00
|
|
|
"net/http"
|
2022-03-23 12:39:31 +00:00
|
|
|
"net/url"
|
2022-03-18 14:48:59 +00:00
|
|
|
"strings"
|
2022-03-23 12:39:31 +00:00
|
|
|
|
2022-03-25 23:49:44 +00:00
|
|
|
"github.com/keys-pub/keys"
|
|
|
|
log "github.com/sirupsen/logrus"
|
2022-03-18 14:48:59 +00:00
|
|
|
)
|
|
|
|
|
2022-03-25 23:49:44 +00:00
|
|
|
var (
|
|
|
|
_ json.Marshaler = (*Addr)(nil)
|
|
|
|
)
|
|
|
|
|
|
|
|
func fetchConfig(addr string) (Config, error) {
|
|
|
|
// Attempt using hash
|
|
|
|
res, err := Request(http.MethodGet, addr, nil, nil)
|
|
|
|
if err != nil {
|
|
|
|
return Config{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
data, err := ioutil.ReadAll(res.Body)
|
|
|
|
if err != nil {
|
|
|
|
return Config{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var config Config
|
|
|
|
if err := json.Unmarshal(data, &config); err != nil {
|
|
|
|
return Config{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return config, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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"`
|
|
|
|
}
|
|
|
|
|
2022-03-18 15:13:31 +00:00
|
|
|
// Addr represents a Salty IM User's Address
|
|
|
|
type Addr struct {
|
2022-03-18 14:48:59 +00:00
|
|
|
User string
|
|
|
|
Domain string
|
2022-03-23 12:39:31 +00:00
|
|
|
|
2022-03-25 23:49:44 +00:00
|
|
|
key *keys.EdX25519PublicKey
|
2022-03-23 12:39:31 +00:00
|
|
|
endpoint *url.URL
|
|
|
|
discoveredDomain string
|
2022-03-18 14:48:59 +00:00
|
|
|
}
|
|
|
|
|
2022-03-19 06:02:40 +00:00
|
|
|
// IsZero returns true if the address is empty
|
2022-03-23 12:39:31 +00:00
|
|
|
func (a *Addr) IsZero() bool {
|
2022-03-19 06:02:40 +00:00
|
|
|
return a.User == "" && a.Domain == ""
|
|
|
|
}
|
|
|
|
|
2022-03-25 23:49:44 +00:00
|
|
|
func (a *Addr) MarshalJSON() ([]byte, error) {
|
|
|
|
return json.Marshal(struct {
|
|
|
|
Addr string
|
|
|
|
User string
|
|
|
|
Domain string
|
|
|
|
Key string
|
|
|
|
Endpoint string
|
|
|
|
}{
|
|
|
|
User: a.User,
|
|
|
|
Domain: a.Domain,
|
|
|
|
Addr: a.String(),
|
|
|
|
Key: a.key.ID().String(),
|
|
|
|
Endpoint: a.Endpoint().String(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-03-23 12:39:31 +00:00
|
|
|
func (a *Addr) String() string {
|
2022-03-19 11:42:36 +00:00
|
|
|
return fmt.Sprintf("%s@%s", a.User, a.Domain)
|
2022-03-18 22:50:04 +00:00
|
|
|
}
|
2022-03-19 01:24:07 +00:00
|
|
|
|
2022-03-20 13:54:54 +00:00
|
|
|
// Hash returns the Hex(SHA256Sum()) of the Address
|
2022-03-23 12:39:31 +00:00
|
|
|
func (a *Addr) Hash() string {
|
2022-03-20 13:54:54 +00:00
|
|
|
return fmt.Sprintf("%x", sha256.Sum256([]byte(a.String())))
|
|
|
|
}
|
|
|
|
|
2022-03-19 01:24:07 +00:00
|
|
|
// Formatted returns a formatted user used in the Salty Message Format
|
|
|
|
// <timestamp\t(<user>) <message>\n
|
2022-03-23 12:39:31 +00:00
|
|
|
func (a *Addr) Formatted() string {
|
2022-03-18 22:50:04 +00:00
|
|
|
return fmt.Sprintf("(%s)", a)
|
|
|
|
}
|
|
|
|
|
2022-03-25 23:49:44 +00:00
|
|
|
// Key returns the Publib Kcy of this User (Salty Addr) as discovered
|
|
|
|
func (a *Addr) Key() *keys.EdX25519PublicKey {
|
|
|
|
return a.key
|
|
|
|
}
|
|
|
|
|
2022-03-23 12:39:31 +00:00
|
|
|
// Endpoint returns the discovered Endpoint
|
|
|
|
func (a *Addr) Endpoint() *url.URL {
|
|
|
|
return a.endpoint
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// DiscoveredDomain returns the discovered domain (if any) of fallbacks to the Domain
|
|
|
|
func (a *Addr) DiscoveredDomain() string {
|
|
|
|
if a.discoveredDomain != "" {
|
|
|
|
return a.discoveredDomain
|
|
|
|
}
|
|
|
|
return a.Domain
|
|
|
|
}
|
|
|
|
|
2022-03-18 15:13:31 +00:00
|
|
|
// URI returns the Well-Known URI for this Addr
|
2022-03-23 12:39:31 +00:00
|
|
|
func (a *Addr) URI() string {
|
|
|
|
return fmt.Sprintf("https://%s/.well-known/salty/%s.json", a.DiscoveredDomain(), a.User)
|
2022-03-18 14:48:59 +00:00
|
|
|
}
|
|
|
|
|
2022-03-18 22:50:04 +00:00
|
|
|
// HashURI returns the Well-Known HashURI for this Addr
|
2022-03-23 12:39:31 +00:00
|
|
|
func (a *Addr) HashURI() string {
|
|
|
|
return fmt.Sprintf("https://%s/.well-known/salty/%s.json", a.DiscoveredDomain(), a.Hash())
|
2022-03-22 22:59:45 +00:00
|
|
|
}
|
|
|
|
|
2022-03-23 12:39:31 +00:00
|
|
|
func (a *Addr) Refresh() error {
|
2022-03-25 23:49:44 +00:00
|
|
|
log.Debugf("Looking up SRV record for _salty._tcp.%s", a.Domain)
|
2022-03-22 22:59:45 +00:00
|
|
|
_, records, err := net.LookupSRV("salty", "tcp", a.Domain)
|
2022-03-23 12:39:31 +00:00
|
|
|
if err == nil && len(records) > 0 {
|
|
|
|
a.discoveredDomain = records[0].Target
|
2022-03-25 23:49:44 +00:00
|
|
|
log.Debugf("Discovered salty services %s", a.discoveredDomain)
|
2022-03-23 12:39:31 +00:00
|
|
|
}
|
|
|
|
|
2022-03-25 23:49:44 +00:00
|
|
|
config, err := fetchConfig(a.HashURI())
|
|
|
|
if err != nil {
|
|
|
|
// Fallback to plain user nick
|
|
|
|
config, err = fetchConfig(a.URI())
|
|
|
|
}
|
2022-03-23 12:39:31 +00:00
|
|
|
if err != nil {
|
2022-03-25 23:49:44 +00:00
|
|
|
return fmt.Errorf("error looking up user %s: %w", a, err)
|
2022-03-22 22:59:45 +00:00
|
|
|
}
|
2022-03-25 23:49:44 +00:00
|
|
|
key, err := keys.NewEdX25519PublicKeyFromID(keys.ID(config.Key))
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error parsing public key %s: %w", config.Key, err)
|
|
|
|
}
|
|
|
|
a.key = key
|
2022-03-23 12:39:31 +00:00
|
|
|
|
|
|
|
u, err := url.Parse(config.Endpoint)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error parsing endpoint %s: %w", config.Endpoint, err)
|
|
|
|
}
|
|
|
|
a.endpoint = u
|
|
|
|
|
|
|
|
return nil
|
2022-03-18 22:50:04 +00:00
|
|
|
}
|
|
|
|
|
2022-03-18 15:13:31 +00:00
|
|
|
// ParseAddr parsers a Salty Address for a user into it's user and domain
|
|
|
|
// parts and returns an Addr object with the User and Domain and a method
|
|
|
|
// for returning the expected User's Well-Known URI
|
2022-03-23 12:39:31 +00:00
|
|
|
func ParseAddr(addr string) (*Addr, error) {
|
2022-03-18 15:13:31 +00:00
|
|
|
parts := strings.Split(addr, "@")
|
2022-03-18 14:48:59 +00:00
|
|
|
if len(parts) != 2 {
|
2022-03-23 12:39:31 +00:00
|
|
|
return nil, fmt.Errorf("expected nick@domain found %q", addr)
|
2022-03-18 14:48:59 +00:00
|
|
|
}
|
|
|
|
|
2022-03-23 12:39:31 +00:00
|
|
|
return &Addr{User: parts[0], Domain: parts[1]}, nil
|
2022-03-18 14:48:59 +00:00
|
|
|
}
|
|
|
|
|
2022-03-25 23:49:44 +00:00
|
|
|
// LookupAddr looks up a Salty Address for a User by parsing the user's domain and
|
2022-03-18 15:13:31 +00:00
|
|
|
// making a request to the user's Well-Known URI expected to be located at
|
2022-03-18 14:48:59 +00:00
|
|
|
// https://domain/.well-known/salty/<user>.json
|
2022-03-25 23:49:44 +00:00
|
|
|
func LookupAddr(addr string) (*Addr, error) {
|
2022-03-18 15:13:31 +00:00
|
|
|
a, err := ParseAddr(addr)
|
2022-03-18 14:48:59 +00:00
|
|
|
if err != nil {
|
2022-03-25 23:49:44 +00:00
|
|
|
return nil, err
|
2022-03-18 22:50:04 +00:00
|
|
|
}
|
2022-03-25 23:49:44 +00:00
|
|
|
if err := a.Refresh(); err != nil {
|
|
|
|
return nil, err
|
2022-03-18 14:48:59 +00:00
|
|
|
}
|
2022-03-25 23:49:44 +00:00
|
|
|
return a, nil
|
2022-03-18 22:50:04 +00:00
|
|
|
}
|