The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
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.
grafana/pkg/api/avatar/avatar.go

328 lines
8.0 KiB

// 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"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"strconv"
"sync"
"time"
gocache "github.com/patrickmn/go-cache"
"github.com/grafana/grafana/pkg/infra/log"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
)
const (
gravatarSource = "https://secure.gravatar.com/avatar/"
)
// Avatar represents the avatar object.
type Avatar struct {
hash string
data *bytes.Buffer
notFound bool
isCustom bool
timestamp time.Time
}
var (
alog = log.New("avatar")
// Represents a singleton AvatarCacheServer instance
csi *AvatarCacheServer
// Paremeters needed to fetch Gravatar with a retro fallback
gravatarReqParams = url.Values{
"d": {"retro"},
"size": {"200"},
"r": {"pg"},
}.Encode()
// Parameters needed to see if a Gravatar is custom
hasCustomReqParams = url.Values{
"d": {"404"},
}.Encode()
cacheInitOnce sync.Once
)
func New(hash string) *Avatar {
return &Avatar{hash: hash}
}
func (a *Avatar) Expired() bool {
return time.Since(a.timestamp) > (time.Minute * 10)
}
func (a *Avatar) Encode(wr io.Writer) error {
_, err := wr.Write(a.data.Bytes())
return err
}
func (a *Avatar) update(baseUrl string) (err error) {
customUrl := baseUrl + a.hash + "?"
select {
case <-time.After(time.Second * 3):
err = fmt.Errorf("get gravatar image %s timeout", a.hash)
case err = <-thunder.GoFetch(customUrl, a):
}
return err
}
func (a *Avatar) GetIsCustom() bool {
return a.isCustom
}
// Quick error handler to avoid multiple copy pastes
func (a *Avatar) setAvatarNotFound() {
a.notFound = true
a.isCustom = false
}
type AvatarCacheServer struct {
cfg *setting.Cfg
notFound *Avatar
cache *gocache.Cache
}
var validMD5 = regexp.MustCompile("^[a-fA-F0-9]{32}$")
func (a *AvatarCacheServer) Handler(ctx *contextmodel.ReqContext) {
hash := web.Params(ctx.Req)[":hash"]
if len(hash) != 32 || !validMD5.MatchString(hash) {
ctx.JsonApiErr(404, "Avatar not found", nil)
return
}
avatar := a.GetAvatarForHash(a.cfg, hash)
ctx.Resp.Header().Set("Content-Type", "image/jpeg")
if !a.cfg.EnableGzip {
ctx.Resp.Header().Set("Content-Length", strconv.Itoa(len(avatar.data.Bytes())))
}
ctx.Resp.Header().Set("Cache-Control", "private, max-age=3600")
if err := avatar.Encode(ctx.Resp); err != nil {
ctx.Logger.Warn("avatar encode error:", "err", err)
ctx.Resp.WriteHeader(http.StatusInternalServerError)
}
}
func (a *AvatarCacheServer) GetAvatarForHash(cfg *setting.Cfg, hash string) *Avatar {
if cfg.DisableGravatar {
alog.Warn("'GetGravatarForHash' called despite gravatars being disabled; returning default profile image")
return a.notFound
}
return a.getAvatarForHash(hash, gravatarSource)
}
func (a *AvatarCacheServer) getAvatarForHash(hash string, baseUrl string) *Avatar {
var avatar *Avatar
obj, exists := a.cache.Get(hash)
if exists {
avatar = obj.(*Avatar)
} else {
avatar = New(hash)
}
if avatar.Expired() {
// The cache item is either expired or newly created, update it from the server
if err := avatar.update(baseUrl); err != nil {
alog.Debug("avatar update", "err", err)
avatar = a.notFound
}
}
if avatar.notFound {
avatar = a.notFound
} else if !exists {
if err := a.cache.Add(hash, avatar, gocache.DefaultExpiration); err != nil {
alog.Debug("add avatar to cache", "err", err)
}
}
return avatar
}
// Access cache server singleton instance
func ProvideAvatarCacheServer(cfg *setting.Cfg) *AvatarCacheServer {
cacheInitOnce.Do(func() {
csi = newCacheServer(cfg)
})
return csi
}
func newCacheServer(cfg *setting.Cfg) *AvatarCacheServer {
return &AvatarCacheServer{
cfg: cfg,
notFound: newNotFound(cfg),
cache: gocache.New(time.Hour, time.Hour*2),
}
}
func newNotFound(cfg *setting.Cfg) *Avatar {
avatar := &Avatar{
notFound: true,
isCustom: false,
}
// load user_profile png into buffer
Security: Add gosec G304 auditing annotations (#29578) * Security: Add gosec G304 auditing annotations Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * add G304 auditing comment Signed-off-by: bergquist <carl.bergquist@gmail.com> * add G304 auditing comment Signed-off-by: bergquist <carl.bergquist@gmail.com> * add G304 auditing comment Signed-off-by: bergquist <carl.bergquist@gmail.com> * add G304 auditing comment Signed-off-by: bergquist <carl.bergquist@gmail.com> * add G304 auditing comment Signed-off-by: bergquist <carl.bergquist@gmail.com> * add G304 auditing comment Signed-off-by: bergquist <carl.bergquist@gmail.com> * add G304 auditing comment Signed-off-by: bergquist <carl.bergquist@gmail.com> * add G304 auditing comment Signed-off-by: bergquist <carl.bergquist@gmail.com> * Add gosec annotations Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * add G304 auditing comment Signed-off-by: bergquist <carl.bergquist@gmail.com> * add G304 auditing comment Signed-off-by: bergquist <carl.bergquist@gmail.com> * Add gosec annotations Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * add G304 auditing comment Signed-off-by: bergquist <carl.bergquist@gmail.com> * add G304 auditing comment Signed-off-by: bergquist <carl.bergquist@gmail.com> * add G304 auditing comment Signed-off-by: bergquist <carl.bergquist@gmail.com> * Add gosec annotations Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * space Signed-off-by: bergquist <carl.bergquist@gmail.com> * Add gosec annotations Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> Co-authored-by: bergquist <carl.bergquist@gmail.com>
5 years ago
// It's safe to ignore gosec warning G304 since the variable part of the file path comes from a configuration
// variable.
// nolint:gosec
path := filepath.Join(cfg.StaticRootPath, "img", "user_profile.png")
// It's safe to ignore gosec warning G304 since the variable part of the file path comes from a configuration
// variable.
// nolint:gosec
if data, err := os.ReadFile(path); err != nil {
alog.Error("Failed to read user_profile.png", "path", 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(baseUrl string, avatar *Avatar) error {
t.once.Do(t.init)
task := &thunderTask{
BaseUrl: baseUrl,
Avatar: avatar,
}
task.Add(1)
t.q <- task
task.Wait()
return task.err
}
func (t *Thunder) GoFetch(baseUrl string, avatar *Avatar) chan error {
c := make(chan error)
go func() {
c <- t.Fetch(baseUrl, avatar)
}()
return c
}
// thunder download
type thunderTask struct {
BaseUrl string
Avatar *Avatar
sync.WaitGroup
err error
}
func (a *thunderTask) Fetch() {
a.err = a.fetch()
a.Done()
}
Remove redundancy in variable declarations (golint) This commit fixes the following golint warnings: pkg/api/avatar/avatar.go:229:12: should omit type *http.Client from declaration of var client; it will be inferred from the right-hand side pkg/login/brute_force_login_protection.go:13:26: should omit type time.Duration from declaration of var loginAttemptsWindow; it will be inferred from the right-hand side pkg/metrics/graphitebridge/graphite.go:58:26: should omit type []string from declaration of var metricCategoryPrefix; it will be inferred from the right-hand side pkg/metrics/graphitebridge/graphite.go:69:22: should omit type []string from declaration of var trimMetricPrefix; it will be inferred from the right-hand side pkg/models/alert.go:37:36: should omit type error from declaration of var ErrCannotChangeStateOnPausedAlert; it will be inferred from the right-hand side pkg/models/alert.go:38:36: should omit type error from declaration of var ErrRequiresNewState; it will be inferred from the right-hand side pkg/models/datasource.go:61:28: should omit type map[string]bool from declaration of var knownDatasourcePlugins; it will be inferred from the right-hand side pkg/plugins/update_checker.go:16:13: should omit type http.Client from declaration of var httpClient; it will be inferred from the right-hand side pkg/services/alerting/engine.go:103:24: should omit type time.Duration from declaration of var unfinishedWorkTimeout; it will be inferred from the right-hand side pkg/services/alerting/engine.go:105:19: should omit type time.Duration from declaration of var alertTimeout; it will be inferred from the right-hand side pkg/services/alerting/engine.go:106:19: should omit type int from declaration of var alertMaxAttempts; it will be inferred from the right-hand side pkg/services/alerting/notifier.go:143:23: should omit type map[string]*NotifierPlugin from declaration of var notifierFactories; it will be inferred from the right-hand side pkg/services/alerting/rule.go:136:24: should omit type map[string]ConditionFactory from declaration of var conditionFactories; it will be inferred from the right-hand side pkg/services/alerting/conditions/evaluator.go:12:15: should omit type []string from declaration of var defaultTypes; it will be inferred from the right-hand side pkg/services/alerting/conditions/evaluator.go:13:15: should omit type []string from declaration of var rangedTypes; it will be inferred from the right-hand side pkg/services/alerting/notifiers/opsgenie.go:44:19: should omit type string from declaration of var opsgenieAlertURL; it will be inferred from the right-hand side pkg/services/alerting/notifiers/pagerduty.go:43:23: should omit type string from declaration of var pagerdutyEventApiUrl; it will be inferred from the right-hand side pkg/services/alerting/notifiers/telegram.go:21:17: should omit type string from declaration of var telegramApiUrl; it will be inferred from the right-hand side pkg/services/provisioning/dashboards/config_reader_test.go:11:24: should omit type string from declaration of var simpleDashboardConfig; it will be inferred from the right-hand side pkg/services/provisioning/dashboards/config_reader_test.go:12:24: should omit type string from declaration of var oldVersion; it will be inferred from the right-hand side pkg/services/provisioning/dashboards/config_reader_test.go:13:24: should omit type string from declaration of var brokenConfigs; it will be inferred from the right-hand side pkg/services/provisioning/dashboards/file_reader.go:22:30: should omit type time.Duration from declaration of var checkDiskForChangesInterval; it will be inferred from the right-hand side pkg/services/provisioning/dashboards/file_reader.go:24:23: should omit type error from declaration of var ErrFolderNameMissing; it will be inferred from the right-hand side pkg/services/provisioning/datasources/config_reader_test.go:15:34: should omit type string from declaration of var twoDatasourcesConfig; it will be inferred from the right-hand side pkg/services/provisioning/datasources/config_reader_test.go:16:34: should omit type string from declaration of var twoDatasourcesConfigPurgeOthers; it will be inferred from the right-hand side pkg/services/provisioning/datasources/config_reader_test.go:17:34: should omit type string from declaration of var doubleDatasourcesConfig; it will be inferred from the right-hand side pkg/services/provisioning/datasources/config_reader_test.go:18:34: should omit type string from declaration of var allProperties; it will be inferred from the right-hand side pkg/services/provisioning/datasources/config_reader_test.go:19:34: should omit type string from declaration of var versionZero; it will be inferred from the right-hand side pkg/services/provisioning/datasources/config_reader_test.go:20:34: should omit type string from declaration of var brokenYaml; it will be inferred from the right-hand side pkg/services/sqlstore/stats.go:16:25: should omit type time.Duration from declaration of var activeUserTimeLimit; it will be inferred from the right-hand side pkg/services/sqlstore/migrator/mysql_dialect.go:69:14: should omit type bool from declaration of var hasLen1; it will be inferred from the right-hand side pkg/services/sqlstore/migrator/mysql_dialect.go:70:14: should omit type bool from declaration of var hasLen2; it will be inferred from the right-hand side pkg/services/sqlstore/migrator/postgres_dialect.go:95:14: should omit type bool from declaration of var hasLen1; it will be inferred from the right-hand side pkg/services/sqlstore/migrator/postgres_dialect.go:96:14: should omit type bool from declaration of var hasLen2; it will be inferred from the right-hand side pkg/setting/setting.go:42:15: should omit type string from declaration of var Env; it will be inferred from the right-hand side pkg/setting/setting.go:161:18: should omit type bool from declaration of var LdapAllowSignup; it will be inferred from the right-hand side pkg/setting/setting.go:473:30: should omit type bool from declaration of var skipStaticRootValidation; it will be inferred from the right-hand side pkg/tsdb/interval.go:14:21: should omit type time.Duration from declaration of var defaultMinInterval; it will be inferred from the right-hand side pkg/tsdb/interval.go:15:21: should omit type time.Duration from declaration of var year; it will be inferred from the right-hand side pkg/tsdb/interval.go:16:21: should omit type time.Duration from declaration of var day; it will be inferred from the right-hand side pkg/tsdb/cloudwatch/credentials.go:26:24: should omit type map[string]cache from declaration of var awsCredentialCache; it will be inferred from the right-hand side pkg/tsdb/influxdb/query.go:15:27: should omit type *regexp.Regexp from declaration of var regexpOperatorPattern; it will be inferred from the right-hand side pkg/tsdb/influxdb/query.go:16:27: should omit type *regexp.Regexp from declaration of var regexpMeasurementPattern; it will be inferred from the right-hand side pkg/tsdb/mssql/mssql_test.go:25:14: should omit type string from declaration of var serverIP; it will be inferred from the right-hand side
7 years ago
var client = &http.Client{
Timeout: time.Second * 2,
Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
}
// We fetch the same url with param tweaks twice in a row
// Break out the fetch function in a way that makes each
// Portion highly reusable
func (a *thunderTask) fetch() error {
a.Avatar.timestamp = time.Now()
alog.Debug("avatar.fetch(fetch new avatar)", "url", a.BaseUrl)
// First do the fetch to get the Gravatar with a retro icon fallback
err := performGet(a.BaseUrl+gravatarReqParams, a.Avatar, getGravatarHandler)
if err == nil {
// Next do a fetch with a 404 fallback to see if it's a custom gravatar
return performGet(a.BaseUrl+hasCustomReqParams, a.Avatar, checkIsCustomHandler)
}
return err
}
type ResponseHandler func(av *Avatar, resp *http.Response) error
// Verifies the Gravatar response code was 200, then stores the image byte slice
func getGravatarHandler(av *Avatar, resp *http.Response) error {
if resp.StatusCode != http.StatusOK {
av.setAvatarNotFound()
return fmt.Errorf("status code: %d", resp.StatusCode)
}
av.data = &bytes.Buffer{}
writer := bufio.NewWriter(av.data)
_, err := io.Copy(writer, resp.Body)
return err
}
// Uses the d=404 fallback to see if the gravatar we got back is custom
func checkIsCustomHandler(av *Avatar, resp *http.Response) error {
av.isCustom = resp.StatusCode != http.StatusNotFound
return nil
}
// Reusable Get helper that allows us to pass in custom handling depending on the endpoint
func performGet(url string, av *Avatar, handler ResponseHandler) error {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}
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")
alog.Debug("Fetching avatar url with parameters", "url", url)
resp, err := client.Do(req)
if err != nil {
av.setAvatarNotFound()
return fmt.Errorf("gravatar unreachable: %w", err)
}
defer func() {
if err := resp.Body.Close(); err != nil {
alog.Warn("Failed to close response body", "err", err)
}
}()
err = handler(av, resp)
Simplify error returns (gosimple) This fixes: pkg/api/avatar/avatar.go:261:2: 'if err != nil { return err }; return nil' can be simplified to 'return err' (S1013) pkg/log/file.go:102:2: 'if err != nil { return err }; return nil' can be simplified to 'return err' (S1013) pkg/metrics/graphitebridge/graphite.go:298:2: 'if err != nil { return err }; return nil' can be simplified to 'return err' (S1013) pkg/services/provisioning/provisioning.go:23:2: 'if err != nil { return err }; return nil' can be simplified to 'return err' (S1013) pkg/services/sqlstore/alert_notification.go:27:3: 'if err != nil { return err }; return nil' can be simplified to 'return err' (S1013) pkg/services/sqlstore/alert_notification.go:27:3: 'if err != nil { return err }; return nil' can be simplified to 'return err' (S1013) pkg/services/sqlstore/annotation.go:105:3: 'if err != nil { return err }; return nil' can be simplified to 'return err' (S1013) pkg/services/sqlstore/annotation.go:105:3: 'if err != nil { return err }; return nil' can be simplified to 'return err' (S1013) pkg/services/sqlstore/dashboard.go:351:2: 'if err != nil { return err }; return nil' can be simplified to 'return err' (S1013) pkg/services/sqlstore/dashboard.go:435:2: 'if err != nil { return err }; return nil' can be simplified to 'return err' (S1013) pkg/services/sqlstore/dashboard_acl.go:38:3: 'if err != nil { return err }; return nil' can be simplified to 'return err' (S1013) pkg/services/sqlstore/dashboard_acl.go:38:3: 'if err != nil { return err }; return nil' can be simplified to 'return err' (S1013) pkg/services/sqlstore/stats.go:22:2: 'if err != nil { return err }; return err' can be simplified to 'return err' (S1013) pkg/services/sqlstore/team.go:213:2: 'if err != nil { return err }; return nil' can be simplified to 'return err' (S1013) pkg/services/sqlstore/user.go:256:3: 'if err != nil { return err }; return nil' can be simplified to 'return err' (S1013) pkg/services/sqlstore/user.go:256:3: 'if err != nil { return err }; return nil' can be simplified to 'return err' (S1013) pkg/services/sqlstore/user.go:274:3: 'if err != nil { return err }; return nil' can be simplified to 'return err' (S1013) pkg/services/sqlstore/user.go:274:3: 'if err != nil { return err }; return nil' can be simplified to 'return err' (S1013) pkg/services/sqlstore/user.go:482:3: 'if err != nil { return err }; return nil' can be simplified to 'return err' (S1013) pkg/services/sqlstore/user.go:482:3: 'if err != nil { return err }; return nil' can be simplified to 'return err' (S1013)
7 years ago
return err
}