package saltyim import ( "crypto/sha256" "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" "strings" "github.com/keys-pub/keys" log "github.com/sirupsen/logrus" ) var ( _ json.Marshaler = (*Addr)(nil) _ json.Unmarshaler = (*Addr)(nil) _ Lookuper = (*DirectLookup)(nil) _ Lookuper = (*ProxyLookup)(nil) ) type Lookuper interface { LookupAddr(user string) (*Addr, error) } func fetchConfig(addr string) (Config, Capabilities, error) { // Attempt using hash res, err := Request(http.MethodGet, addr, nil, nil) if err != nil { return Config{}, Capabilities{}, err } var config Config if err := json.NewDecoder(res.Body).Decode(&config); err != nil { return Config{}, Capabilities{}, err } log.Debug(res.Header) cap := Capabilities{ AcceptEncoding: res.Header.Get("Accept-Encoding"), } return config, cap, 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"` } type Capabilities struct { AcceptEncoding string } func (c Capabilities) String() string { return fmt.Sprint("accept-encoding: ", c.AcceptEncoding) } // Addr represents a Salty IM User's Address type Addr struct { User string Domain string key *keys.EdX25519PublicKey endpoint *url.URL discoveredDomain string avatar string capabilities Capabilities checkedAvatar bool } // IsZero returns true if the address is empty func (a *Addr) IsZero() bool { return a.User == "" && a.Domain == "" } func (a *Addr) MarshalJSON() ([]byte, error) { return json.Marshal(struct { Addr string User string Domain string Key string Endpoint string Avatar string }{ User: a.User, Domain: a.Domain, Addr: a.String(), Key: a.key.ID().String(), Endpoint: a.Endpoint().String(), Avatar: a.Avatar(), }) } func (a *Addr) UnmarshalJSON(data []byte) error { res := struct { Addr string User string Domain string Key string Endpoint string Avatar string }{} if err := json.Unmarshal(data, &res); err != nil { return err } a.User = res.User a.Domain = res.Domain u, err := url.Parse(res.Endpoint) if err != nil { return fmt.Errorf("error parsing endpoint %q: %w", res.Endpoint, err) } a.endpoint = u key, err := keys.NewEdX25519PublicKeyFromID(keys.ID(res.Key)) if err != nil { return fmt.Errorf("error parsing public key %q: %w", res.Key, err) } a.key = key a.avatar = res.Avatar return 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())))) } // Formatted returns a formatted user used in the Salty Message Format // ) \n func (a *Addr) Formatted() string { return fmt.Sprintf("(%s)", a) } // Key returns the Publib Kcy of this User (Salty Addr) as discovered func (a *Addr) Key() *keys.EdX25519PublicKey { return a.key } // Endpoint returns the discovered Endpoint func (a *Addr) Endpoint() *url.URL { return a.endpoint } // Cap returns the discovered Capabilities func (a *Addr) Cap() Capabilities { return a.capabilities } // 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 } // 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 { log.Debugf("Looking up SRV record for _salty._tcp.%s", a.Domain) if target, err := resolver.LookupSRV("salty", "tcp", a.Domain); err == nil { a.discoveredDomain = target log.Debugf("Discovered salty services %s", a.discoveredDomain) } else if err != nil { log.Debugf("error looking up SRV record for _salty._tcp.%s : %s", a.Domain, err) } config, cap, err := fetchConfig(a.HashURI()) if err != nil { // Fallback to plain user nick config, cap, err = fetchConfig(a.URI()) } if err != nil { return fmt.Errorf("error looking up user %s: %w", a, err) } 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 u, err := url.Parse(config.Endpoint) if err != nil { return fmt.Errorf("error parsing endpoint %s: %w", config.Endpoint, err) } a.endpoint = u a.capabilities = cap log.Debugf("Discovered endpoint: %v", a.endpoint) log.Debugf("Discovered capability: %v", a.capabilities) return nil } func (a *Addr) Avatar() string { if a.checkedAvatar { return a.avatar } log.Debugf("Looking up SRV record for _avatars._tcp.%s", a.Domain) if target, err := resolver.LookupSRV("avatars", "tcp", a.Domain); err == nil { a.avatar = fmt.Sprintf("https://%s/avatar/%s", target, a.Hash()) } a.checkedAvatar = true return a.avatar } // 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(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]}, nil } // LookupAddr 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/.json func LookupAddr(addr string) (*Addr, error) { a, err := ParseAddr(addr) if err != nil { return nil, err } if err := a.Refresh(); err != nil { return nil, err } return a, nil } // DirectLookup performs a direct lookup request type DirectLookup struct{} // LookupAddr performs a lookup of a Salty Addr directly func (l *DirectLookup) LookupAddr(user string) (*Addr, error) { return LookupAddr(user) } // ProxyLookup proxies lookup requests through a Salty Broker's /api/v1/lookup endpoint type ProxyLookup struct { // LookupEndpoint is the uri of the lookup endpoint of a broker LookupEndpoint string } // LookupAddr performs a lookup of a Salty Addr directly and if the request fails for // whatever reason (usuaully due to Cross-Orogin-Resource-Sharing policies / CORS) it // uses the Salty Broker the PWA was served from initially to perform the lookup by // proxying the lookup through the broker. Why? CORS sucks. func (l *ProxyLookup) LookupAddr(user string) (*Addr, error) { addr, err := LookupAddr(user) if err == nil { return addr, nil } // Fallback to proxying the lookup through the broker... res, err := Request(http.MethodGet, l.LookupEndpoint+user, nil, nil) if err != nil { return nil, err } defer res.Body.Close() data, err := ioutil.ReadAll(res.Body) if err != nil { return nil, err } if err := json.Unmarshal(data, &addr); err != nil { return nil, err } return addr, nil }