gitroast/vendor/strk.kbt.io/projects/go/libravatar/libravatar.go

298 lines
7.7 KiB
Go

// 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 (
// Default 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
}
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
}
// Instanciate a library 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,
}
}
// Set the hostname for fallbacks in case no avatar service is defined
// for a domain
func (v *Libravatar) SetFallbackHost(host string) {
v.fallbackHost = host
}
// Set 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
}
// Set useHTTPS flag
func (v *Libravatar) SetUseHTTPS(use bool) {
v.useHTTPS = use
}
// Set 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 (
total_weight uint16
records []record
top_priority = addrs[0].Priority
top_record *net.SRV
)
for _, rr := range addrs {
if rr.Priority > top_priority {
continue
} else if rr.Priority < top_priority {
// won't happen, because net sorts
// by priority, but just in case
total_weight = 0
records = nil
top_priority = rr.Priority
}
total_weight += rr.Weight
if rr.Weight > 0 {
records = append(records, record{rr, total_weight})
} else if rr.Weight == 0 {
records = append([]record{record{srv: rr, weight: total_weight}}, records...)
}
}
if len(records) == 1 {
top_record = records[0].srv
} else {
randnum := uint16(rand.Intn(int(total_weight)))
for _, rr := range records {
if rr.weight >= randnum {
top_record = rr.srv
break
}
}
}
domain = fmt.Sprintf("%s:%d", top_record.Target, top_record.Port)
}
v.nameCache[key] = cacheValue{checkedAt: now, target: domain}
return protocol + domain, nil
}
// Return 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
}
// Object-less call to DefaultLibravatar for an email adders
func FromEmail(email string) (string, error) {
return DefaultLibravatar.FromEmail(email)
}
// Return 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
}
// Object-less call to DefaultLibravatar for a URL
func FromURL(openid string) (string, error) {
return DefaultLibravatar.FromURL(openid)
}