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.

307 lines
8.2 KiB

  1. // Copyright 2016 by Sandro Santilli <strk@kbt.io>
  2. // Use of this source code is governed by a MIT
  3. // license that can be found in the LICENSE file.
  4. // Implements support for federated avatars lookup.
  5. // See https://wiki.libravatar.org/api/
  6. package libravatar // import "strk.kbt.io/projects/go/libravatar"
  7. import (
  8. "crypto/md5"
  9. "crypto/sha256"
  10. "fmt"
  11. "math/rand"
  12. "net"
  13. "net/mail"
  14. "net/url"
  15. "strings"
  16. "sync"
  17. "time"
  18. )
  19. // Default images (to be used as defaultURL)
  20. const (
  21. // Do not load any image if none is associated with the email
  22. // hash, instead return an HTTP 404 (File Not Found) response
  23. HTTP404 = "404"
  24. // (mystery-man) a simple, cartoon-style silhouetted outline of
  25. // a person (does not vary by email hash)
  26. MysteryMan = "mm"
  27. // a geometric pattern based on an email hash
  28. IdentIcon = "identicon"
  29. // a generated 'monster' with different colors, faces, etc
  30. MonsterID = "monsterid"
  31. // generated faces with differing features and backgrounds
  32. Wavatar = "wavatar"
  33. // awesome generated, 8-bit arcade-style pixelated faces
  34. Retro = "retro"
  35. )
  36. var (
  37. // DefaultLibravatar is a default Libravatar object,
  38. // enabling object-less function calls
  39. DefaultLibravatar = New()
  40. )
  41. /* This should be moved in its own file */
  42. type cacheKey struct {
  43. service string
  44. domain string
  45. }
  46. type cacheValue struct {
  47. target string
  48. checkedAt time.Time
  49. }
  50. // Libravatar is an opaque structure holding service configuration
  51. type Libravatar struct {
  52. defURL string // default url
  53. picSize int // picture size
  54. fallbackHost string // default fallback URL
  55. secureFallbackHost string // default fallback URL for secure connections
  56. useHTTPS bool
  57. nameCache map[cacheKey]cacheValue
  58. nameCacheDuration time.Duration
  59. nameCacheMutex *sync.Mutex
  60. minSize uint // smallest image dimension allowed
  61. maxSize uint // largest image dimension allowed
  62. size uint // what dimension should be used
  63. serviceBase string // SRV record to be queried for federation
  64. secureServiceBase string // SRV record to be queried for federation with secure servers
  65. }
  66. // New instanciates a new Libravatar object (handle)
  67. func New() *Libravatar {
  68. // According to https://wiki.libravatar.org/running_your_own/
  69. // the time-to-live (cache expiry) should be set to at least 1 day.
  70. return &Libravatar{
  71. fallbackHost: `cdn.libravatar.org`,
  72. secureFallbackHost: `seccdn.libravatar.org`,
  73. minSize: 1,
  74. maxSize: 512,
  75. size: 0, // unset, defaults to 80
  76. serviceBase: `avatars`,
  77. secureServiceBase: `avatars-sec`,
  78. nameCache: make(map[cacheKey]cacheValue),
  79. nameCacheDuration: 24 * time.Hour,
  80. nameCacheMutex: &sync.Mutex{},
  81. }
  82. }
  83. // SetFallbackHost sets the hostname for fallbacks in case no avatar
  84. // service is defined for a domain
  85. func (v *Libravatar) SetFallbackHost(host string) {
  86. v.fallbackHost = host
  87. }
  88. // SetSecureFallbackHost sets the hostname for fallbacks in case no
  89. // avatar service is defined for a domain, when requiring secure domains
  90. func (v *Libravatar) SetSecureFallbackHost(host string) {
  91. v.secureFallbackHost = host
  92. }
  93. // SetUseHTTPS sets flag requesting use of https for fetching avatars
  94. func (v *Libravatar) SetUseHTTPS(use bool) {
  95. v.useHTTPS = use
  96. }
  97. // SetAvatarSize sets avatars image dimension (0 for default)
  98. func (v *Libravatar) SetAvatarSize(size uint) {
  99. v.size = size
  100. }
  101. // generate hash, either with email address or OpenID
  102. func (v *Libravatar) genHash(email *mail.Address, openid *url.URL) string {
  103. if email != nil {
  104. email.Address = strings.ToLower(strings.TrimSpace(email.Address))
  105. sum := md5.Sum([]byte(email.Address))
  106. return fmt.Sprintf("%x", sum)
  107. } else if openid != nil {
  108. openid.Scheme = strings.ToLower(openid.Scheme)
  109. openid.Host = strings.ToLower(openid.Host)
  110. sum := sha256.Sum256([]byte(openid.String()))
  111. return fmt.Sprintf("%x", sum)
  112. }
  113. // panic, because this should not be reachable
  114. panic("Neither Email or OpenID set")
  115. }
  116. // Gets domain out of email or openid (for openid to be parsed, email has to be nil)
  117. func (v *Libravatar) getDomain(email *mail.Address, openid *url.URL) string {
  118. if email != nil {
  119. u, err := url.Parse("//" + email.Address)
  120. if err != nil {
  121. if v.useHTTPS && v.secureFallbackHost != "" {
  122. return v.secureFallbackHost
  123. }
  124. return v.fallbackHost
  125. }
  126. return u.Host
  127. } else if openid != nil {
  128. return openid.Host
  129. }
  130. // panic, because this should not be reachable
  131. panic("Neither Email or OpenID set")
  132. }
  133. // Processes email or openid (for openid to be processed, email has to be nil)
  134. func (v *Libravatar) process(email *mail.Address, openid *url.URL) (string, error) {
  135. URL, err := v.baseURL(email, openid)
  136. if err != nil {
  137. return "", err
  138. }
  139. res := fmt.Sprintf("%s/avatar/%s", URL, v.genHash(email, openid))
  140. values := make(url.Values)
  141. if v.defURL != "" {
  142. values.Add("d", v.defURL)
  143. }
  144. if v.size > 0 {
  145. values.Add("s", fmt.Sprintf("%d", v.size))
  146. }
  147. if len(values) > 0 {
  148. return fmt.Sprintf("%s?%s", res, values.Encode()), nil
  149. }
  150. return res, nil
  151. }
  152. // Finds or defaults a URL for Federation (for openid to be used, email has to be nil)
  153. func (v *Libravatar) baseURL(email *mail.Address, openid *url.URL) (string, error) {
  154. var service, protocol, domain string
  155. if v.useHTTPS {
  156. protocol = "https://"
  157. service = v.secureServiceBase
  158. domain = v.secureFallbackHost
  159. } else {
  160. protocol = "http://"
  161. service = v.serviceBase
  162. domain = v.fallbackHost
  163. }
  164. host := v.getDomain(email, openid)
  165. key := cacheKey{service, host}
  166. now := time.Now()
  167. v.nameCacheMutex.Lock()
  168. val, found := v.nameCache[key]
  169. v.nameCacheMutex.Unlock()
  170. if found && now.Sub(val.checkedAt) <= v.nameCacheDuration {
  171. return protocol + val.target, nil
  172. }
  173. _, addrs, err := net.LookupSRV(service, "tcp", host)
  174. if err != nil && err.(*net.DNSError).IsTimeout {
  175. return "", err
  176. }
  177. if len(addrs) == 1 {
  178. // select only record, if only one is available
  179. domain = strings.TrimSuffix(addrs[0].Target, ".")
  180. } else if len(addrs) > 1 {
  181. // Select first record according to RFC2782 weight
  182. // ordering algorithm (page 3)
  183. type record struct {
  184. srv *net.SRV
  185. weight uint16
  186. }
  187. var (
  188. totalWeight uint16
  189. records []record
  190. topPriority = addrs[0].Priority
  191. topRecord *net.SRV
  192. )
  193. for _, rr := range addrs {
  194. if rr.Priority > topPriority {
  195. continue
  196. } else if rr.Priority < topPriority {
  197. // won't happen, because net sorts
  198. // by priority, but just in case
  199. totalWeight = 0
  200. records = nil
  201. topPriority = rr.Priority
  202. }
  203. totalWeight += rr.Weight
  204. if rr.Weight > 0 {
  205. records = append(records, record{rr, totalWeight})
  206. } else if rr.Weight == 0 {
  207. records = append([]record{record{srv: rr, weight: totalWeight}}, records...)
  208. }
  209. }
  210. if len(records) == 1 {
  211. topRecord = records[0].srv
  212. } else {
  213. randnum := uint16(rand.Intn(int(totalWeight)))
  214. for _, rr := range records {
  215. if rr.weight >= randnum {
  216. topRecord = rr.srv
  217. break
  218. }
  219. }
  220. }
  221. domain = fmt.Sprintf("%s:%d", topRecord.Target, topRecord.Port)
  222. }
  223. v.nameCacheMutex.Lock()
  224. v.nameCache[key] = cacheValue{checkedAt: now, target: domain}
  225. v.nameCacheMutex.Unlock()
  226. return protocol + domain, nil
  227. }
  228. // FromEmail returns the url of the avatar for the given email
  229. func (v *Libravatar) FromEmail(email string) (string, error) {
  230. addr, err := mail.ParseAddress(email)
  231. if err != nil {
  232. return "", err
  233. }
  234. link, err := v.process(addr, nil)
  235. if err != nil {
  236. return "", err
  237. }
  238. return link, nil
  239. }
  240. // FromEmail is the object-less call to DefaultLibravatar for an email adders
  241. func FromEmail(email string) (string, error) {
  242. return DefaultLibravatar.FromEmail(email)
  243. }
  244. // FromURL returns the url of the avatar for the given url (typically
  245. // for OpenID)
  246. func (v *Libravatar) FromURL(openid string) (string, error) {
  247. ourl, err := url.Parse(openid)
  248. if err != nil {
  249. return "", err
  250. }
  251. if !ourl.IsAbs() {
  252. return "", fmt.Errorf("Is not an absolute URL")
  253. } else if ourl.Scheme != "http" && ourl.Scheme != "https" {
  254. return "", fmt.Errorf("Invalid protocol: %s", ourl.Scheme)
  255. }
  256. link, err := v.process(nil, ourl)
  257. if err != nil {
  258. return "", err
  259. }
  260. return link, nil
  261. }
  262. // FromURL is the object-less call to DefaultLibravatar for a URL
  263. func FromURL(openid string) (string, error) {
  264. return DefaultLibravatar.FromURL(openid)
  265. }