6
1
mirror of https://git.mills.io/saltyim/saltyim.git synced 2024-06-25 00:08:26 +00:00
prologic-saltyim/lookup.go
2022-03-23 12:39:31 +00:00

150 lines
3.3 KiB
Go

package saltyim
import (
"crypto/sha256"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"strings"
sync "github.com/sasha-s/go-deadlock"
)
// Addr represents a Salty IM User's Address
type Addr struct {
mu sync.RWMutex
User string
Domain string
endpoint *url.URL
discoveredDomain string
}
// IsZero returns true if the address is empty
func (a *Addr) IsZero() bool {
a.mu.RLock()
defer a.mu.RUnlock()
return a.User == "" && a.Domain == ""
}
func (a *Addr) String() string {
return fmt.Sprintf("%s@%s", a.User, a.Domain)
}
// Hash returns the Hex(SHA256Sum()) of the Address
func (a *Addr) Hash() string {
return fmt.Sprintf("%x", sha256.Sum256([]byte(a.String())))
}
// Formatted returns a formatted user used in the Salty Message Format
// <timestamp\t(<user>) <message>\n
func (a *Addr) Formatted() string {
return fmt.Sprintf("(%s)", a)
}
// Endpoint returns the discovered Endpoint
func (a *Addr) Endpoint() *url.URL {
a.mu.RLock()
defer a.mu.RUnlock()
return a.endpoint
}
// DiscoveredDomain returns the discovered domain (if any) of fallbacks to the Domain
func (a *Addr) DiscoveredDomain() string {
a.mu.RLock()
defer a.mu.RUnlock()
if a.discoveredDomain != "" {
return a.discoveredDomain
}
return a.Domain
}
// URI returns the Well-Known URI for this Addr
func (a *Addr) URI() string {
return fmt.Sprintf("https://%s/.well-known/salty/%s.json", a.DiscoveredDomain(), a.User)
}
// HashURI returns the Well-Known HashURI for this Addr
func (a *Addr) HashURI() string {
return fmt.Sprintf("https://%s/.well-known/salty/%s.json", a.DiscoveredDomain(), a.Hash())
}
func (a *Addr) Refresh() error {
a.mu.Lock()
defer a.mu.Unlock()
_, records, err := net.LookupSRV("salty", "tcp", a.Domain)
if err == nil && len(records) > 0 {
a.discoveredDomain = records[0].Target
}
config, err := Lookup(a.String())
if err != nil {
return fmt.Errorf("error looking up endpoint for %s: %w", a, err)
}
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
}
// 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
func ParseAddr(addr string) (*Addr, error) {
parts := strings.Split(addr, "@")
if len(parts) != 2 {
return nil, fmt.Errorf("expected nick@domain found %q", addr)
}
return &Addr{User: parts[0], Domain: parts[1]}, nil
}
// Lookup looks up a Salty Address for a User by parsing the user's domain and
// making a request to the user's Well-Known URI expected to be located at
// https://domain/.well-known/salty/<user>.json
func Lookup(addr string) (Config, error) {
a, err := ParseAddr(addr)
if err != nil {
return Config{}, err
}
config, err := fetchConfig(a.HashURI())
if err != nil {
// Fallback to plain user nick
config, err = fetchConfig(a.URI())
}
return config, err
}
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
}