Adding upstream version 0.0~git20191008.06d1c00.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
358bcd43bc
commit
6483251b09
6 changed files with 429 additions and 0 deletions
8
.editorconfig
Normal file
8
.editorconfig
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# http://editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_style = tab
|
19
LICENSE
Normal file
19
LICENSE
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2016 Sandro Santilli <strk@kbt.io>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
12
Makefile
Normal file
12
Makefile
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
PACKAGES ?= $(shell go list ./...)
|
||||||
|
|
||||||
|
.PHONY: check
|
||||||
|
check: lint
|
||||||
|
go test
|
||||||
|
|
||||||
|
.PHONY: lint
|
||||||
|
lint:
|
||||||
|
@which golint > /dev/null; if [ $$? -ne 0 ]; then \
|
||||||
|
go get -u github.com/golang/lint/golint; \
|
||||||
|
fi
|
||||||
|
@for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done;
|
27
README.md
Normal file
27
README.md
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
Simple [golang](https://www.golang.org) library for serving
|
||||||
|
[federated avatars](https://www.libravatar.org)
|
||||||
|
|
||||||
|
[]
|
||||||
|
(https://goreportcard.com/report/strk.kbt.io/projects/go/libravatar)
|
||||||
|
|
||||||
|
# Use
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get strk.kbt.io/projects/go/libravatar
|
||||||
|
go doc strk.kbt.io/projects/go/libravatar
|
||||||
|
```
|
||||||
|
|
||||||
|
# Contribute
|
||||||
|
|
||||||
|
A clone of the code repository would be downloaded by `go get`.
|
||||||
|
You can send patches or pull requests to strk@kbt.io.
|
||||||
|
|
||||||
|
If you need a place to publish your contribution branches,
|
||||||
|
you could start from a fork of the gitlab mirror:
|
||||||
|
https://gitlab.com/strk/go-libravatar
|
||||||
|
|
||||||
|
# Contacts
|
||||||
|
|
||||||
|
* Project homepage: http://strk.kbt.io/projects/go/libravatar
|
||||||
|
* Maintainer: Sandro Santilli <strk@kbt.io>
|
||||||
|
|
307
libravatar.go
Normal file
307
libravatar.go
Normal file
|
@ -0,0 +1,307 @@
|
||||||
|
// 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 "strk.kbt.io/projects/go/libravatar"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"net/mail"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"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
|
||||||
|
nameCacheMutex *sync.Mutex
|
||||||
|
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,
|
||||||
|
nameCacheMutex: &sync.Mutex{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
v.nameCacheMutex.Lock()
|
||||||
|
val, found := v.nameCache[key]
|
||||||
|
v.nameCacheMutex.Unlock()
|
||||||
|
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.nameCacheMutex.Lock()
|
||||||
|
v.nameCache[key] = cacheValue{checkedAt: now, target: domain}
|
||||||
|
v.nameCacheMutex.Unlock()
|
||||||
|
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)
|
||||||
|
}
|
56
libravatar_test.go
Normal file
56
libravatar_test.go
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
package libravatar
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestFromEmail(t *testing.T) {
|
||||||
|
|
||||||
|
avt := New()
|
||||||
|
|
||||||
|
// Email tests
|
||||||
|
|
||||||
|
cases := []struct{ in, want string }{
|
||||||
|
{"strk@kbt.io", "http://avatars.kbt.io/avatar/fe2a9e759730ee64c44bf8901bf4ccc3"},
|
||||||
|
{"strk@keybit.net", "http://cdn.libravatar.org/avatar/34bafd290f6f39380f5f87e0122daf83"},
|
||||||
|
{"strk@nonexistent.domain", "http://cdn.libravatar.org/avatar/3f30177111597990b15f8421eaf736c7"},
|
||||||
|
{"invalid", "mail: missing phrase"},
|
||||||
|
{"invalid@", "mail: no angle-addr"},
|
||||||
|
{"@invalid", "mail: missing word in phrase: mail: invalid string"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
got, err := avt.FromEmail(c.in)
|
||||||
|
if err != nil {
|
||||||
|
got = err.Error()
|
||||||
|
}
|
||||||
|
if got != c.want {
|
||||||
|
t.Errorf("fromEmail(%q) == %q, expected %q", c.in, got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: test https with email
|
||||||
|
|
||||||
|
// OpenID tests
|
||||||
|
|
||||||
|
cases = []struct{ in, want string }{
|
||||||
|
{"https://strk.kbt.io/openid/", "http://cdn.libravatar.org/avatar/1eaf3174c95d0df02f177f7f6a1df5125ad3d6603fbd062defecd30810a0463c"},
|
||||||
|
{"invalid", "Is not an absolute URL"},
|
||||||
|
{"ssh://user@nothttp/", "Invalid protocol: ssh"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
got, err := avt.FromURL(c.in)
|
||||||
|
if err != nil {
|
||||||
|
got = err.Error()
|
||||||
|
}
|
||||||
|
if got != c.want {
|
||||||
|
t.Errorf("fromURL(%q) == %q, expected %q", c.in, got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: test parameters
|
||||||
|
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue