| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 | // 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 libravatarimport (	"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 configurationtype 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 domainfunc (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 domainsfunc (v *Libravatar) SetSecureFallbackHost(host string) {	v.secureFallbackHost = host}// SetUseHTTPS sets flag requesting use of https for fetching avatarsfunc (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 OpenIDfunc (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 emailfunc (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 addersfunc 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 URLfunc FromURL(openid string) (string, error) {	return DefaultLibravatar.FromURL(openid)}
 |