mirror of https://github.com/gogits/gogs.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
300 lines
8.0 KiB
300 lines
8.0 KiB
// 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) |
|
}
|
|
|