Commit 08f7ccff by Torkel Ödegaard

feat(avatar): added server side proxy and cache of gravatar requests

parent bf4a00b6
......@@ -2,6 +2,7 @@ package api
import (
"github.com/go-macaron/binding"
"github.com/grafana/grafana/pkg/api/avatar"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
......@@ -224,6 +225,10 @@ func Register(r *macaron.Macaron) {
// rendering
r.Get("/render/*", reqSignedIn, RenderToPng)
// Gravatar service.
avt := avatar.CacheServer()
r.Get("/avatar/:hash", avt.ServeHTTP)
InitAppPluginRoutes(r)
r.NotFound(NotFoundHandler)
......
// Copyright 2014 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// Code from https://github.com/gogits/gogs/blob/v0.7.0/modules/avatar/avatar.go
package avatar
import (
"bufio"
"bytes"
"crypto/md5"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/setting"
)
var gravatarSource string
func UpdateGravatarSource() {
srcCfg := "//secure.gravatar.com/avatar/"
gravatarSource = srcCfg
if strings.HasPrefix(gravatarSource, "//") {
gravatarSource = "http:" + gravatarSource
} else if !strings.HasPrefix(gravatarSource, "http://") &&
!strings.HasPrefix(gravatarSource, "https://") {
gravatarSource = "http://" + gravatarSource
}
}
// hash email to md5 string
// keep this func in order to make this package independent
func HashEmail(email string) string {
// https://en.gravatar.com/site/implement/hash/
email = strings.TrimSpace(email)
email = strings.ToLower(email)
h := md5.New()
h.Write([]byte(email))
return hex.EncodeToString(h.Sum(nil))
}
// Avatar represents the avatar object.
type Avatar struct {
hash string
reqParams string
data *bytes.Buffer
notFound bool
timestamp time.Time
}
func New(hash string) *Avatar {
return &Avatar{
hash: hash,
reqParams: url.Values{
"d": {"404"},
"size": {"200"},
"r": {"pg"}}.Encode(),
}
}
func (this *Avatar) Expired() bool {
return time.Since(this.timestamp) > (time.Minute * 10)
}
func (this *Avatar) Encode(wr io.Writer) error {
_, err := wr.Write(this.data.Bytes())
return err
}
func (this *Avatar) Update() (err error) {
select {
case <-time.After(time.Second * 3):
err = fmt.Errorf("get gravatar image %s timeout", this.hash)
case err = <-thunder.GoFetch(gravatarSource+this.hash+"?"+this.reqParams, this):
}
return err
}
type service struct {
notFound *Avatar
cache map[string]*Avatar
}
func (this *service) mustInt(r *http.Request, defaultValue int, keys ...string) (v int) {
for _, k := range keys {
if _, err := fmt.Sscanf(r.FormValue(k), "%d", &v); err == nil {
defaultValue = v
}
}
return defaultValue
}
func (this *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
urlPath := r.URL.Path
hash := urlPath[strings.LastIndex(urlPath, "/")+1:]
var avatar *Avatar
if avatar, _ = this.cache[hash]; avatar == nil {
avatar = New(hash)
}
if avatar.Expired() {
if err := avatar.Update(); err != nil {
log.Trace("avatar update error: %v", err)
}
}
if avatar.notFound {
avatar = this.notFound
} else {
this.cache[hash] = avatar
}
w.Header().Set("Content-Type", "image/jpeg")
w.Header().Set("Content-Length", strconv.Itoa(len(avatar.data.Bytes())))
w.Header().Set("Cache-Control", "private, max-age=3600")
if err := avatar.Encode(w); err != nil {
log.Warn("avatar encode error: %v", err)
w.WriteHeader(500)
}
}
func CacheServer() http.Handler {
UpdateGravatarSource()
return &service{
notFound: newNotFound(),
cache: make(map[string]*Avatar),
}
}
func newNotFound() *Avatar {
avatar := &Avatar{}
// load transparent png into buffer
path := filepath.Join(setting.StaticRootPath, "img", "transparent.png")
if data, err := ioutil.ReadFile(path); err != nil {
log.Error(3, "Failed to read transparent.png, %v", path)
} else {
avatar.data = bytes.NewBuffer(data)
}
return avatar
}
// thunder downloader
var thunder = &Thunder{QueueSize: 10}
type Thunder struct {
QueueSize int // download queue size
q chan *thunderTask
once sync.Once
}
func (t *Thunder) init() {
if t.QueueSize < 1 {
t.QueueSize = 1
}
t.q = make(chan *thunderTask, t.QueueSize)
for i := 0; i < t.QueueSize; i++ {
go func() {
for {
task := <-t.q
task.Fetch()
}
}()
}
}
func (t *Thunder) Fetch(url string, avatar *Avatar) error {
t.once.Do(t.init)
task := &thunderTask{
Url: url,
Avatar: avatar,
}
task.Add(1)
t.q <- task
task.Wait()
return task.err
}
func (t *Thunder) GoFetch(url string, avatar *Avatar) chan error {
c := make(chan error)
go func() {
c <- t.Fetch(url, avatar)
}()
return c
}
// thunder download
type thunderTask struct {
Url string
Avatar *Avatar
sync.WaitGroup
err error
}
func (this *thunderTask) Fetch() {
this.err = this.fetch()
this.Done()
}
var client = &http.Client{}
func (this *thunderTask) fetch() error {
this.Avatar.timestamp = time.Now()
log.Debug("avatar.fetch(fetch new avatar): %s", this.Url)
req, _ := http.NewRequest("GET", this.Url, nil)
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/jpeg,image/png,*/*;q=0.8")
req.Header.Set("Accept-Encoding", "deflate,sdch")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.8")
req.Header.Set("Cache-Control", "no-cache")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.154 Safari/537.36")
resp, err := client.Do(req)
if err != nil {
this.Avatar.notFound = true
return fmt.Errorf("gravatar unreachable, %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
this.Avatar.notFound = true
return fmt.Errorf("status code: %d", resp.StatusCode)
}
this.Avatar.data = &bytes.Buffer{}
writer := bufio.NewWriter(this.Avatar.data)
if _, err = io.Copy(writer, resp.Body); err != nil {
return err
}
return nil
}
......@@ -7,6 +7,7 @@ import (
"time"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
type LoginCommand struct {
......@@ -89,5 +90,5 @@ func GetGravatarUrl(text string) string {
hasher := md5.New()
hasher.Write([]byte(strings.ToLower(text)))
return fmt.Sprintf("https://secure.gravatar.com/avatar/%x?s=90&default=mm", hasher.Sum(nil))
return fmt.Sprintf(setting.AppSubUrl+"/avatar/%x", hasher.Sum(nil))
}
......@@ -36,7 +36,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
}
if setting.DisableGravatar {
data.User.GravatarUrl = setting.AppSubUrl + "/public/img/user_profile.png"
data.User.GravatarUrl = setting.AppSubUrl + "/public/img/transparent.png"
}
if len(data.User.Name) == 0 {
......
......@@ -4,7 +4,7 @@
<div class="sidemenu-org">
<div class="sidemenu-org-avatar">
<img ng-if="ctrl.user.gravatarUrl" ng-src="{{ctrl.user.gravatarUrl}}">
<span class="sidemenu-org-avatar--missing" ng-if="!ctrl.user.gravatarUrl">
<span class="sidemenu-org-avatar--missing">
<i class="fa fa-fw fa-user"></i>
</span>
</div>
......
......@@ -210,9 +210,11 @@
text-align: center;
>img {
position: absolute;
width: 40px;
height: 40px;
border-radius: 50%;
left: 14px;
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment