From 08f7ccff38cedc5ae0a0456b5aabb160fdeab3da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 20 Feb 2016 23:51:22 +0100 Subject: [PATCH] feat(avatar): added server side proxy and cache of gravatar requests --- pkg/api/api.go | 5 + pkg/api/avatar/avatar.go | 253 ++++++++++++++++++ pkg/api/dtos/models.go | 3 +- pkg/api/index.go | 2 +- .../core/components/sidemenu/sidemenu.html | 2 +- public/img/transparent.png | Bin 0 -> 95 bytes public/sass/components/_sidemenu.scss | 2 + 7 files changed, 264 insertions(+), 3 deletions(-) create mode 100644 pkg/api/avatar/avatar.go create mode 100644 public/img/transparent.png diff --git a/pkg/api/api.go b/pkg/api/api.go index 6e7015ed889..c21deae5a59 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -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) diff --git a/pkg/api/avatar/avatar.go b/pkg/api/avatar/avatar.go new file mode 100644 index 00000000000..79aebece349 --- /dev/null +++ b/pkg/api/avatar/avatar.go @@ -0,0 +1,253 @@ +// 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 +} diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index 83176e836e5..b810701233e 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -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)) } diff --git a/pkg/api/index.go b/pkg/api/index.go index b53050c9833..72277fe301b 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -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 { diff --git a/public/app/core/components/sidemenu/sidemenu.html b/public/app/core/components/sidemenu/sidemenu.html index 61785b3cfe3..6e460e813ce 100644 --- a/public/app/core/components/sidemenu/sidemenu.html +++ b/public/app/core/components/sidemenu/sidemenu.html @@ -4,7 +4,7 @@
- +
diff --git a/public/img/transparent.png b/public/img/transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..e4e8dcaa3b3cd01a1287359902561166d7f942ee GIT binary patch literal 95 zcmeAS@N?(olHy`uVBq!ia0vp^j3CU&3?x-=hn)ga%mF?juK#@*VoWXSL2@NQe!&b5 m&u*jvIb5DDjv*Cul9PaJHU?%h^O_Yv7K5j&pUXO@geCw~dlW4I literal 0 HcmV?d00001 diff --git a/public/sass/components/_sidemenu.scss b/public/sass/components/_sidemenu.scss index fb380d8ec50..ee3c3f35363 100644 --- a/public/sass/components/_sidemenu.scss +++ b/public/sass/components/_sidemenu.scss @@ -210,9 +210,11 @@ text-align: center; >img { + position: absolute; width: 40px; height: 40px; border-radius: 50%; + left: 14px; } }