2022-09-04 08:34:22 -06:00
|
|
|
package salty
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"crypto/sha256"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/keys-pub/keys"
|
2022-09-06 10:35:14 -06:00
|
|
|
"github.com/sour-is/ev/internal/lg"
|
2022-09-04 08:34:22 -06:00
|
|
|
)
|
|
|
|
|
|
|
|
// 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"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type Capabilities struct {
|
|
|
|
AcceptEncoding string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c Capabilities) String() string {
|
2022-09-07 16:00:10 -06:00
|
|
|
if c.AcceptEncoding == "" {
|
|
|
|
return "<nil>"
|
|
|
|
}
|
2022-09-04 08:34:22 -06:00
|
|
|
return fmt.Sprint("accept-encoding: ", c.AcceptEncoding)
|
|
|
|
}
|
|
|
|
|
|
|
|
type Addr struct {
|
|
|
|
User string
|
|
|
|
Domain string
|
|
|
|
|
|
|
|
capabilities Capabilities
|
|
|
|
discoveredDomain string
|
|
|
|
dns DNSResolver
|
|
|
|
endpoint *url.URL
|
|
|
|
key *keys.EdX25519PublicKey
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 (s *service) ParseAddr(addr string) (*Addr, error) {
|
|
|
|
parts := strings.Split(strings.ToLower(addr), "@")
|
|
|
|
if len(parts) != 2 {
|
|
|
|
return nil, fmt.Errorf("expected nick@domain found %q", addr)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &Addr{User: parts[0], Domain: parts[1], dns: s.dns}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
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(strings.ToLower(a.String()))))
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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())
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *Addr) Refresh(ctx context.Context) error {
|
2022-09-06 10:35:14 -06:00
|
|
|
ctx, span := lg.Span(ctx)
|
2022-09-04 08:34:22 -06:00
|
|
|
defer span.End()
|
|
|
|
|
|
|
|
span.AddEvent(fmt.Sprintf("Looking up SRV record for _salty._tcp.%s", a.Domain))
|
2022-09-07 16:00:10 -06:00
|
|
|
if _, srv, err := a.dns.LookupSRV(ctx, "salty", "tcp", a.Domain); err == nil {
|
|
|
|
if len(srv) > 0 {
|
|
|
|
a.discoveredDomain = strings.TrimSuffix(srv[0].Target, ".")
|
|
|
|
}
|
2022-09-04 08:34:22 -06:00
|
|
|
span.AddEvent(fmt.Sprintf("Discovered salty services %s", a.discoveredDomain))
|
|
|
|
} else if err != nil {
|
2022-09-07 16:00:10 -06:00
|
|
|
span.RecordError(fmt.Errorf("error looking up SRV record for _salty._tcp.%s : %s", a.Domain, err))
|
2022-09-04 08:34:22 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
config, cap, err := fetchConfig(ctx, a.HashURI())
|
|
|
|
if err != nil {
|
|
|
|
// Fallback to plain user nick
|
2022-09-07 16:00:10 -06:00
|
|
|
span.RecordError(err)
|
|
|
|
|
2022-09-04 08:34:22 -06:00
|
|
|
config, cap, err = fetchConfig(ctx, a.URI())
|
|
|
|
}
|
|
|
|
if err != nil {
|
2022-09-07 16:00:10 -06:00
|
|
|
err = fmt.Errorf("error looking up user %s: %w", a, err)
|
|
|
|
span.RecordError(err)
|
|
|
|
return err
|
2022-09-04 08:34:22 -06:00
|
|
|
}
|
|
|
|
key, err := keys.NewEdX25519PublicKeyFromID(keys.ID(config.Key))
|
|
|
|
if err != nil {
|
2022-09-07 16:00:10 -06:00
|
|
|
err = fmt.Errorf("error parsing public key %s: %w", config.Key, err)
|
|
|
|
span.RecordError(err)
|
|
|
|
return err
|
2022-09-04 08:34:22 -06:00
|
|
|
}
|
|
|
|
a.key = key
|
|
|
|
|
|
|
|
u, err := url.Parse(config.Endpoint)
|
|
|
|
if err != nil {
|
2022-09-07 16:00:10 -06:00
|
|
|
err = fmt.Errorf("error parsing endpoint %s: %w", config.Endpoint, err)
|
|
|
|
span.RecordError(err)
|
|
|
|
return err
|
2022-09-04 08:34:22 -06:00
|
|
|
}
|
|
|
|
a.endpoint = u
|
|
|
|
a.capabilities = cap
|
|
|
|
|
|
|
|
span.AddEvent(fmt.Sprintf("Discovered endpoint: %v", a.endpoint))
|
|
|
|
span.AddEvent(fmt.Sprintf("Discovered capability: %v", a.capabilities))
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func fetchConfig(ctx context.Context, addr string) (config Config, cap Capabilities, err error) {
|
2022-09-06 10:35:14 -06:00
|
|
|
ctx, span := lg.Span(ctx)
|
2022-09-04 08:34:22 -06:00
|
|
|
defer span.End()
|
|
|
|
|
|
|
|
var req *http.Request
|
|
|
|
req, err = http.NewRequestWithContext(ctx, http.MethodGet, addr, nil)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
res, err := http.DefaultClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = json.NewDecoder(res.Body).Decode(&config); err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
cap.AcceptEncoding = res.Header.Get("Accept-Encoding")
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|