2016-11-03 23:16:01 +01:00
// 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/
2019-10-08 16:48:57 -03:00
package libravatar // import "strk.kbt.io/projects/go/libravatar"
2016-11-03 23:16:01 +01:00
import (
"crypto/md5"
"crypto/sha256"
"fmt"
"math/rand"
"net"
"net/mail"
"net/url"
"strings"
2019-10-08 16:48:57 -03:00
"sync"
2016-11-03 23:16:01 +01:00
"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 (
2019-10-08 16:48:57 -03:00
// DefaultLibravatar is a default Libravatar object,
// enabling object-less function calls
2016-11-03 23:16:01 +01:00
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
}
2019-10-08 16:48:57 -03:00
// Libravatar is an opaque structure holding service configuration
2016-11-03 23:16:01 +01:00
type Libravatar struct {
2019-10-08 16:48:57 -03:00
defURL string // default url
2016-11-03 23:16:01 +01:00
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
2019-10-08 16:48:57 -03:00
nameCacheMutex * sync . Mutex
2016-11-03 23:16:01 +01:00
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
}
2019-10-08 16:48:57 -03:00
// New instanciates a new Libravatar object (handle)
2016-11-03 23:16:01 +01:00
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 ,
2019-10-08 16:48:57 -03:00
nameCacheMutex : & sync . Mutex { } ,
2016-11-03 23:16:01 +01:00
}
}
2019-10-08 16:48:57 -03:00
// SetFallbackHost sets the hostname for fallbacks in case no avatar
// service is defined for a domain
2016-11-03 23:16:01 +01:00
func ( v * Libravatar ) SetFallbackHost ( host string ) {
v . fallbackHost = host
}
2019-10-08 16:48:57 -03:00
// SetSecureFallbackHost sets the hostname for fallbacks in case no
// avatar service is defined for a domain, when requiring secure domains
2016-11-03 23:16:01 +01:00
func ( v * Libravatar ) SetSecureFallbackHost ( host string ) {
v . secureFallbackHost = host
}
2019-10-08 16:48:57 -03:00
// SetUseHTTPS sets flag requesting use of https for fetching avatars
2016-11-03 23:16:01 +01:00
func ( v * Libravatar ) SetUseHTTPS ( use bool ) {
v . useHTTPS = use
}
2019-10-08 16:48:57 -03:00
// SetAvatarSize sets avatars image dimension (0 for default)
2016-11-03 23:16:01 +01:00
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 )
2019-10-08 16:48:57 -03:00
if v . defURL != "" {
values . Add ( "d" , v . defURL )
2016-11-03 23:16:01 +01:00
}
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 ( )
2019-10-08 16:48:57 -03:00
v . nameCacheMutex . Lock ( )
2016-11-03 23:16:01 +01:00
val , found := v . nameCache [ key ]
2019-10-08 16:48:57 -03:00
v . nameCacheMutex . Unlock ( )
2016-11-03 23:16:01 +01:00
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 (
2019-10-08 16:48:57 -03:00
totalWeight uint16
records [ ] record
topPriority = addrs [ 0 ] . Priority
topRecord * net . SRV
2016-11-03 23:16:01 +01:00
)
for _ , rr := range addrs {
2019-10-08 16:48:57 -03:00
if rr . Priority > topPriority {
2016-11-03 23:16:01 +01:00
continue
2019-10-08 16:48:57 -03:00
} else if rr . Priority < topPriority {
2016-11-03 23:16:01 +01:00
// won't happen, because net sorts
// by priority, but just in case
2019-10-08 16:48:57 -03:00
totalWeight = 0
2016-11-03 23:16:01 +01:00
records = nil
2019-10-08 16:48:57 -03:00
topPriority = rr . Priority
2016-11-03 23:16:01 +01:00
}
2019-10-08 16:48:57 -03:00
totalWeight += rr . Weight
2016-11-03 23:16:01 +01:00
if rr . Weight > 0 {
2019-10-08 16:48:57 -03:00
records = append ( records , record { rr , totalWeight } )
2016-11-03 23:16:01 +01:00
} else if rr . Weight == 0 {
2019-10-08 16:48:57 -03:00
records = append ( [ ] record { record { srv : rr , weight : totalWeight } } , records ... )
2016-11-03 23:16:01 +01:00
}
}
if len ( records ) == 1 {
2019-10-08 16:48:57 -03:00
topRecord = records [ 0 ] . srv
2016-11-03 23:16:01 +01:00
} else {
2019-10-08 16:48:57 -03:00
randnum := uint16 ( rand . Intn ( int ( totalWeight ) ) )
2016-11-03 23:16:01 +01:00
for _ , rr := range records {
if rr . weight >= randnum {
2019-10-08 16:48:57 -03:00
topRecord = rr . srv
2016-11-03 23:16:01 +01:00
break
}
}
}
2019-10-08 16:48:57 -03:00
domain = fmt . Sprintf ( "%s:%d" , topRecord . Target , topRecord . Port )
2016-11-03 23:16:01 +01:00
}
2019-10-08 16:48:57 -03:00
v . nameCacheMutex . Lock ( )
2016-11-03 23:16:01 +01:00
v . nameCache [ key ] = cacheValue { checkedAt : now , target : domain }
2019-10-08 16:48:57 -03:00
v . nameCacheMutex . Unlock ( )
2016-11-03 23:16:01 +01:00
return protocol + domain , nil
}
2019-10-08 16:48:57 -03:00
// FromEmail returns the url of the avatar for the given email
2016-11-03 23:16:01 +01:00
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
}
2019-10-08 16:48:57 -03:00
// FromEmail is the object-less call to DefaultLibravatar for an email adders
2016-11-03 23:16:01 +01:00
func FromEmail ( email string ) ( string , error ) {
return DefaultLibravatar . FromEmail ( email )
}
2019-10-08 16:48:57 -03:00
// FromURL returns the url of the avatar for the given url (typically
// for OpenID)
2016-11-03 23:16:01 +01:00
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
}
2019-10-08 16:48:57 -03:00
// FromURL is the object-less call to DefaultLibravatar for a URL
2016-11-03 23:16:01 +01:00
func FromURL ( openid string ) ( string , error ) {
return DefaultLibravatar . FromURL ( openid )
}