6
1
mirror of https://git.mills.io/saltyim/saltyim.git synced 2024-06-29 18:21:06 +00:00
prologic-saltyim/lookup.go
xuu 754fcc7323 feat: add compression negotiation for sent messages (#91)
feat: add compression negotiation for sent messages
fix: unix homedir handling

the service will negotiate a compression algo for sending messages
when a user chats someone during the auto discovery, the service returns an `Accept-Encoding: br, gzip, deflate`

the client saves that response and so when it makes POSTs of messages adds the best `Content-Encoding` and compresses the message

example:
```
>> GET /.well-known/salty/c765c69040d98f3af2181237f47ec01398d80f8ab2690fe929e4311ab05dec01.json

<< Accept-Encoding: br, gzip, deflate
<<
<< {"endpoint":"https://salty.home.arpa/inbox/01FZBR8Y2E6TH949JA3925WF71","key":"kex1wurry09ftqjuxgjl0jxmqypv4axqvzqljkgeadxjcpwtfuhcedcslck52d"}

>> POST /inbox/01FZBR8Y2E6TH949JA3925WF71
>> Content-Encoding: br
>>
>> [Brotli Compressed data]
```

this PR depends on https://git.mills.io/prologic/msgbus/pulls/24

Co-authored-by: Jon Lundy <jon@xuu.cc>
Reviewed-on: https://git.mills.io/saltyim/saltyim/pulls/91
Co-authored-by: xuu <xuu@noreply@mills.io>
Co-committed-by: xuu <xuu@noreply@mills.io>
2022-03-29 22:23:16 +00:00

187 lines
4.6 KiB
Go

package saltyim
import (
"crypto/sha256"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/keys-pub/keys"
log "github.com/sirupsen/logrus"
)
var (
_ json.Marshaler = (*Addr)(nil)
)
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
}
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
}
// Addr represents a Salty IM User's Address
type Addr struct {
User string
Domain string
key *keys.EdX25519PublicKey
endpoint *url.URL
discoveredDomain string
capabilities Capabilities
}
// 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
}{
User: a.User,
Domain: a.Domain,
Addr: a.String(),
Key: a.key.ID().String(),
Endpoint: a.Endpoint().String(),
})
}
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)
}
// 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 = strings.TrimSuffix(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
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
}
// 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/<user>.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
}