123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301 |
- // Copyright 2016 by Sandro Santilli <strk@kbt.io>
- // Use of this source code is governed by a MIT
- // license that can be found in the LICENSE file.
- // Implements support for federated avatars lookup.
- // See https://wiki.libravatar.org/api/
- package libravatar
- import (
- "crypto/md5"
- "crypto/sha256"
- "fmt"
- "math/rand"
- "net"
- "net/mail"
- "net/url"
- "strings"
- "time"
- )
- // Default images (to be used as defaultURL)
- const (
- // Do not load any image if none is associated with the email
- // hash, instead return an HTTP 404 (File Not Found) response
- HTTP404 = "404"
- // (mystery-man) a simple, cartoon-style silhouetted outline of
- // a person (does not vary by email hash)
- MysteryMan = "mm"
- // a geometric pattern based on an email hash
- IdentIcon = "identicon"
- // a generated 'monster' with different colors, faces, etc
- MonsterID = "monsterid"
- // generated faces with differing features and backgrounds
- Wavatar = "wavatar"
- // awesome generated, 8-bit arcade-style pixelated faces
- Retro = "retro"
- )
- var (
- // DefaultLibravatar is a default Libravatar object,
- // enabling object-less function calls
- DefaultLibravatar = New()
- )
- /* This should be moved in its own file */
- type cacheKey struct {
- service string
- domain string
- }
- type cacheValue struct {
- target string
- checkedAt time.Time
- }
- // Libravatar is an opaque structure holding service configuration
- type Libravatar struct {
- defURL string // default url
- picSize int // picture size
- fallbackHost string // default fallback URL
- secureFallbackHost string // default fallback URL for secure connections
- useHTTPS bool
- nameCache map[cacheKey]cacheValue
- nameCacheDuration time.Duration
- minSize uint // smallest image dimension allowed
- maxSize uint // largest image dimension allowed
- size uint // what dimension should be used
- serviceBase string // SRV record to be queried for federation
- secureServiceBase string // SRV record to be queried for federation with secure servers
- }
- // New instanciates a new Libravatar object (handle)
- func New() *Libravatar {
- // According to https://wiki.libravatar.org/running_your_own/
- // the time-to-live (cache expiry) should be set to at least 1 day.
- return &Libravatar{
- fallbackHost: `cdn.libravatar.org`,
- secureFallbackHost: `seccdn.libravatar.org`,
- minSize: 1,
- maxSize: 512,
- size: 0, // unset, defaults to 80
- serviceBase: `avatars`,
- secureServiceBase: `avatars-sec`,
- nameCache: make(map[cacheKey]cacheValue),
- nameCacheDuration: 24 * time.Hour,
- }
- }
- // SetFallbackHost sets the hostname for fallbacks in case no avatar
- // service is defined for a domain
- func (v *Libravatar) SetFallbackHost(host string) {
- v.fallbackHost = host
- }
- // SetSecureFallbackHost sets the hostname for fallbacks in case no
- // avatar service is defined for a domain, when requiring secure domains
- func (v *Libravatar) SetSecureFallbackHost(host string) {
- v.secureFallbackHost = host
- }
- // SetUseHTTPS sets flag requesting use of https for fetching avatars
- func (v *Libravatar) SetUseHTTPS(use bool) {
- v.useHTTPS = use
- }
- // SetAvatarSize sets avatars image dimension (0 for default)
- func (v *Libravatar) SetAvatarSize(size uint) {
- v.size = size
- }
- // generate hash, either with email address or OpenID
- func (v *Libravatar) genHash(email *mail.Address, openid *url.URL) string {
- if email != nil {
- email.Address = strings.ToLower(strings.TrimSpace(email.Address))
- sum := md5.Sum([]byte(email.Address))
- return fmt.Sprintf("%x", sum)
- } else if openid != nil {
- openid.Scheme = strings.ToLower(openid.Scheme)
- openid.Host = strings.ToLower(openid.Host)
- sum := sha256.Sum256([]byte(openid.String()))
- return fmt.Sprintf("%x", sum)
- }
- // panic, because this should not be reachable
- panic("Neither Email or OpenID set")
- }
- // Gets domain out of email or openid (for openid to be parsed, email has to be nil)
- func (v *Libravatar) getDomain(email *mail.Address, openid *url.URL) string {
- if email != nil {
- u, err := url.Parse("//" + email.Address)
- if err != nil {
- if v.useHTTPS && v.secureFallbackHost != "" {
- return v.secureFallbackHost
- }
- return v.fallbackHost
- }
- return u.Host
- } else if openid != nil {
- return openid.Host
- }
- // panic, because this should not be reachable
- panic("Neither Email or OpenID set")
- }
- // Processes email or openid (for openid to be processed, email has to be nil)
- func (v *Libravatar) process(email *mail.Address, openid *url.URL) (string, error) {
- URL, err := v.baseURL(email, openid)
- if err != nil {
- return "", err
- }
- res := fmt.Sprintf("%s/avatar/%s", URL, v.genHash(email, openid))
- values := make(url.Values)
- if v.defURL != "" {
- values.Add("d", v.defURL)
- }
- if v.size > 0 {
- values.Add("s", fmt.Sprintf("%d", v.size))
- }
- if len(values) > 0 {
- return fmt.Sprintf("%s?%s", res, values.Encode()), nil
- }
- return res, nil
- }
- // Finds or defaults a URL for Federation (for openid to be used, email has to be nil)
- func (v *Libravatar) baseURL(email *mail.Address, openid *url.URL) (string, error) {
- var service, protocol, domain string
- if v.useHTTPS {
- protocol = "https://"
- service = v.secureServiceBase
- domain = v.secureFallbackHost
- } else {
- protocol = "http://"
- service = v.serviceBase
- domain = v.fallbackHost
- }
- host := v.getDomain(email, openid)
- key := cacheKey{service, host}
- now := time.Now()
- val, found := v.nameCache[key]
- if found && now.Sub(val.checkedAt) <= v.nameCacheDuration {
- return protocol + val.target, nil
- }
- _, addrs, err := net.LookupSRV(service, "tcp", host)
- if err != nil && err.(*net.DNSError).IsTimeout {
- return "", err
- }
- if len(addrs) == 1 {
- // select only record, if only one is available
- domain = strings.TrimSuffix(addrs[0].Target, ".")
- } else if len(addrs) > 1 {
- // Select first record according to RFC2782 weight
- // ordering algorithm (page 3)
- type record struct {
- srv *net.SRV
- weight uint16
- }
- var (
- totalWeight uint16
- records []record
- topPriority = addrs[0].Priority
- topRecord *net.SRV
- )
- for _, rr := range addrs {
- if rr.Priority > topPriority {
- continue
- } else if rr.Priority < topPriority {
- // won't happen, because net sorts
- // by priority, but just in case
- totalWeight = 0
- records = nil
- topPriority = rr.Priority
- }
- totalWeight += rr.Weight
- if rr.Weight > 0 {
- records = append(records, record{rr, totalWeight})
- } else if rr.Weight == 0 {
- records = append([]record{record{srv: rr, weight: totalWeight}}, records...)
- }
- }
- if len(records) == 1 {
- topRecord = records[0].srv
- } else {
- randnum := uint16(rand.Intn(int(totalWeight)))
- for _, rr := range records {
- if rr.weight >= randnum {
- topRecord = rr.srv
- break
- }
- }
- }
- domain = fmt.Sprintf("%s:%d", topRecord.Target, topRecord.Port)
- }
- v.nameCache[key] = cacheValue{checkedAt: now, target: domain}
- return protocol + domain, nil
- }
- // FromEmail returns the url of the avatar for the given email
- func (v *Libravatar) FromEmail(email string) (string, error) {
- addr, err := mail.ParseAddress(email)
- if err != nil {
- return "", err
- }
- link, err := v.process(addr, nil)
- if err != nil {
- return "", err
- }
- return link, nil
- }
- // FromEmail is the object-less call to DefaultLibravatar for an email adders
- func FromEmail(email string) (string, error) {
- return DefaultLibravatar.FromEmail(email)
- }
- // FromURL returns the url of the avatar for the given url (typically
- // for OpenID)
- func (v *Libravatar) FromURL(openid string) (string, error) {
- ourl, err := url.Parse(openid)
- if err != nil {
- return "", err
- }
- if !ourl.IsAbs() {
- return "", fmt.Errorf("Is not an absolute URL")
- } else if ourl.Scheme != "http" && ourl.Scheme != "https" {
- return "", fmt.Errorf("Invalid protocol: %s", ourl.Scheme)
- }
- link, err := v.process(nil, ourl)
- if err != nil {
- return "", err
- }
- return link, nil
- }
- // FromURL is the object-less call to DefaultLibravatar for a URL
- func FromURL(openid string) (string, error) {
- return DefaultLibravatar.FromURL(openid)
- }
|