libravatar.go 8.0 KB

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