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.

libravatar.go 7.7KB

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