mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Currently all API requests set Cache-control: no-cache to avoid browsers caching sensitive data. This fixes so that all responses returned from backend not are cached using http headers. The exception is the data proxy where we don't add these http headers in case datasource backend needs to control whether data can be cached or not. Fixes #16845
324 lines
9.2 KiB
Go
324 lines
9.2 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"time"
|
|
|
|
"github.com/grafana/grafana/pkg/api/live"
|
|
"github.com/grafana/grafana/pkg/api/routing"
|
|
httpstatic "github.com/grafana/grafana/pkg/api/static"
|
|
"github.com/grafana/grafana/pkg/bus"
|
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
|
"github.com/grafana/grafana/pkg/infra/remotecache"
|
|
"github.com/grafana/grafana/pkg/log"
|
|
"github.com/grafana/grafana/pkg/middleware"
|
|
"github.com/grafana/grafana/pkg/models"
|
|
"github.com/grafana/grafana/pkg/plugins"
|
|
"github.com/grafana/grafana/pkg/registry"
|
|
"github.com/grafana/grafana/pkg/services/cache"
|
|
"github.com/grafana/grafana/pkg/services/datasources"
|
|
"github.com/grafana/grafana/pkg/services/hooks"
|
|
"github.com/grafana/grafana/pkg/services/quota"
|
|
"github.com/grafana/grafana/pkg/services/rendering"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
"gopkg.in/macaron.v1"
|
|
)
|
|
|
|
func init() {
|
|
registry.Register(®istry.Descriptor{
|
|
Name: "HTTPServer",
|
|
Instance: &HTTPServer{},
|
|
InitPriority: registry.High,
|
|
})
|
|
}
|
|
|
|
type ProvisioningService interface {
|
|
ProvisionDatasources() error
|
|
ProvisionNotifications() error
|
|
ProvisionDashboards() error
|
|
GetDashboardProvisionerResolvedPath(name string) string
|
|
}
|
|
|
|
type HTTPServer struct {
|
|
log log.Logger
|
|
macaron *macaron.Macaron
|
|
context context.Context
|
|
streamManager *live.StreamManager
|
|
httpSrv *http.Server
|
|
|
|
RouteRegister routing.RouteRegister `inject:""`
|
|
Bus bus.Bus `inject:""`
|
|
RenderService rendering.Service `inject:""`
|
|
Cfg *setting.Cfg `inject:""`
|
|
HooksService *hooks.HooksService `inject:""`
|
|
CacheService *cache.CacheService `inject:""`
|
|
DatasourceCache datasources.CacheService `inject:""`
|
|
AuthTokenService models.UserTokenService `inject:""`
|
|
QuotaService *quota.QuotaService `inject:""`
|
|
RemoteCacheService *remotecache.RemoteCache `inject:""`
|
|
ProvisioningService ProvisioningService `inject:""`
|
|
}
|
|
|
|
func (hs *HTTPServer) Init() error {
|
|
hs.log = log.New("http.server")
|
|
|
|
hs.streamManager = live.NewStreamManager()
|
|
hs.macaron = hs.newMacaron()
|
|
hs.registerRoutes()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (hs *HTTPServer) Run(ctx context.Context) error {
|
|
var err error
|
|
|
|
hs.context = ctx
|
|
|
|
hs.applyRoutes()
|
|
hs.streamManager.Run(ctx)
|
|
|
|
listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort)
|
|
hs.log.Info("HTTP Server Listen", "address", listenAddr, "protocol", setting.Protocol, "subUrl", setting.AppSubUrl, "socket", setting.SocketPath)
|
|
|
|
hs.httpSrv = &http.Server{Addr: listenAddr, Handler: hs.macaron}
|
|
|
|
// handle http shutdown on server context done
|
|
go func() {
|
|
<-ctx.Done()
|
|
// Hacky fix for race condition between ListenAndServe and Shutdown
|
|
time.Sleep(time.Millisecond * 100)
|
|
if err := hs.httpSrv.Shutdown(context.Background()); err != nil {
|
|
hs.log.Error("Failed to shutdown server", "error", err)
|
|
}
|
|
}()
|
|
|
|
switch setting.Protocol {
|
|
case setting.HTTP:
|
|
err = hs.httpSrv.ListenAndServe()
|
|
if err == http.ErrServerClosed {
|
|
hs.log.Debug("server was shutdown gracefully")
|
|
return nil
|
|
}
|
|
case setting.HTTPS:
|
|
err = hs.listenAndServeTLS(setting.CertFile, setting.KeyFile)
|
|
if err == http.ErrServerClosed {
|
|
hs.log.Debug("server was shutdown gracefully")
|
|
return nil
|
|
}
|
|
case setting.SOCKET:
|
|
ln, err := net.ListenUnix("unix", &net.UnixAddr{Name: setting.SocketPath, Net: "unix"})
|
|
if err != nil {
|
|
hs.log.Debug("server was shutdown gracefully")
|
|
return nil
|
|
}
|
|
|
|
// Make socket writable by group
|
|
os.Chmod(setting.SocketPath, 0660)
|
|
|
|
err = hs.httpSrv.Serve(ln)
|
|
if err != nil {
|
|
hs.log.Debug("server was shutdown gracefully")
|
|
return nil
|
|
}
|
|
default:
|
|
hs.log.Error("Invalid protocol", "protocol", setting.Protocol)
|
|
err = errors.New("Invalid Protocol")
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (hs *HTTPServer) listenAndServeTLS(certfile, keyfile string) error {
|
|
if certfile == "" {
|
|
return fmt.Errorf("cert_file cannot be empty when using HTTPS")
|
|
}
|
|
|
|
if keyfile == "" {
|
|
return fmt.Errorf("cert_key cannot be empty when using HTTPS")
|
|
}
|
|
|
|
if _, err := os.Stat(setting.CertFile); os.IsNotExist(err) {
|
|
return fmt.Errorf(`Cannot find SSL cert_file at %v`, setting.CertFile)
|
|
}
|
|
|
|
if _, err := os.Stat(setting.KeyFile); os.IsNotExist(err) {
|
|
return fmt.Errorf(`Cannot find SSL key_file at %v`, setting.KeyFile)
|
|
}
|
|
|
|
tlsCfg := &tls.Config{
|
|
MinVersion: tls.VersionTLS12,
|
|
PreferServerCipherSuites: true,
|
|
CipherSuites: []uint16{
|
|
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
|
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
|
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
|
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
|
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
|
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
|
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
|
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
|
},
|
|
}
|
|
|
|
hs.httpSrv.TLSConfig = tlsCfg
|
|
hs.httpSrv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
|
|
|
|
return hs.httpSrv.ListenAndServeTLS(setting.CertFile, setting.KeyFile)
|
|
}
|
|
|
|
func (hs *HTTPServer) newMacaron() *macaron.Macaron {
|
|
macaron.Env = setting.Env
|
|
m := macaron.New()
|
|
|
|
// automatically set HEAD for every GET
|
|
m.SetAutoHead(true)
|
|
|
|
return m
|
|
}
|
|
|
|
func (hs *HTTPServer) applyRoutes() {
|
|
// start with middlewares & static routes
|
|
hs.addMiddlewaresAndStaticRoutes()
|
|
// then add view routes & api routes
|
|
hs.RouteRegister.Register(hs.macaron)
|
|
// then custom app proxy routes
|
|
hs.initAppPluginRoutes(hs.macaron)
|
|
// lastly not found route
|
|
hs.macaron.NotFound(hs.NotFoundHandler)
|
|
}
|
|
|
|
func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
|
|
m := hs.macaron
|
|
|
|
m.Use(middleware.Logger())
|
|
|
|
if setting.EnableGzip {
|
|
m.Use(middleware.Gziper())
|
|
}
|
|
|
|
m.Use(middleware.Recovery())
|
|
|
|
for _, route := range plugins.StaticRoutes {
|
|
pluginRoute := path.Join("/public/plugins/", route.PluginId)
|
|
hs.log.Debug("Plugins: Adding route", "route", pluginRoute, "dir", route.Directory)
|
|
hs.mapStatic(hs.macaron, route.Directory, "", pluginRoute)
|
|
}
|
|
|
|
hs.mapStatic(m, setting.StaticRootPath, "build", "public/build")
|
|
hs.mapStatic(m, setting.StaticRootPath, "", "public")
|
|
hs.mapStatic(m, setting.StaticRootPath, "robots.txt", "robots.txt")
|
|
|
|
if setting.ImageUploadProvider == "local" {
|
|
hs.mapStatic(m, hs.Cfg.ImagesDir, "", "/public/img/attachments")
|
|
}
|
|
|
|
m.Use(middleware.AddDefaultResponseHeaders())
|
|
|
|
m.Use(macaron.Renderer(macaron.RenderOptions{
|
|
Directory: path.Join(setting.StaticRootPath, "views"),
|
|
IndentJSON: macaron.Env != macaron.PROD,
|
|
Delims: macaron.Delims{Left: "[[", Right: "]]"},
|
|
}))
|
|
|
|
m.Use(hs.healthHandler)
|
|
m.Use(hs.metricsEndpoint)
|
|
m.Use(middleware.GetContextHandler(
|
|
hs.AuthTokenService,
|
|
hs.RemoteCacheService,
|
|
))
|
|
m.Use(middleware.OrgRedirect())
|
|
|
|
// needs to be after context handler
|
|
if setting.EnforceDomain {
|
|
m.Use(middleware.ValidateHostHeader(setting.Domain))
|
|
}
|
|
|
|
m.Use(middleware.HandleNoCacheHeader())
|
|
}
|
|
|
|
func (hs *HTTPServer) metricsEndpoint(ctx *macaron.Context) {
|
|
if !hs.Cfg.MetricsEndpointEnabled {
|
|
return
|
|
}
|
|
|
|
if ctx.Req.Method != "GET" || ctx.Req.URL.Path != "/metrics" {
|
|
return
|
|
}
|
|
|
|
if hs.metricsEndpointBasicAuthEnabled() && !BasicAuthenticatedRequest(ctx.Req, hs.Cfg.MetricsEndpointBasicAuthUsername, hs.Cfg.MetricsEndpointBasicAuthPassword) {
|
|
ctx.Resp.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{}).
|
|
ServeHTTP(ctx.Resp, ctx.Req.Request)
|
|
}
|
|
|
|
func (hs *HTTPServer) healthHandler(ctx *macaron.Context) {
|
|
notHeadOrGet := ctx.Req.Method != http.MethodGet && ctx.Req.Method != http.MethodHead
|
|
if notHeadOrGet || ctx.Req.URL.Path != "/api/health" {
|
|
return
|
|
}
|
|
|
|
data := simplejson.New()
|
|
data.Set("database", "ok")
|
|
data.Set("version", setting.BuildVersion)
|
|
data.Set("commit", setting.BuildCommit)
|
|
|
|
if err := bus.Dispatch(&models.GetDBHealthQuery{}); err != nil {
|
|
data.Set("database", "failing")
|
|
ctx.Resp.Header().Set("Content-Type", "application/json; charset=UTF-8")
|
|
ctx.Resp.WriteHeader(503)
|
|
} else {
|
|
ctx.Resp.Header().Set("Content-Type", "application/json; charset=UTF-8")
|
|
ctx.Resp.WriteHeader(200)
|
|
}
|
|
|
|
dataBytes, _ := data.EncodePretty()
|
|
ctx.Resp.Write(dataBytes)
|
|
}
|
|
|
|
func (hs *HTTPServer) mapStatic(m *macaron.Macaron, rootDir string, dir string, prefix string) {
|
|
headers := func(c *macaron.Context) {
|
|
c.Resp.Header().Set("Cache-Control", "public, max-age=3600")
|
|
}
|
|
|
|
if prefix == "public/build" {
|
|
headers = func(c *macaron.Context) {
|
|
c.Resp.Header().Set("Cache-Control", "public, max-age=31536000")
|
|
}
|
|
}
|
|
|
|
if setting.Env == setting.DEV {
|
|
headers = func(c *macaron.Context) {
|
|
c.Resp.Header().Set("Cache-Control", "max-age=0, must-revalidate, no-cache")
|
|
}
|
|
}
|
|
|
|
m.Use(httpstatic.Static(
|
|
path.Join(rootDir, dir),
|
|
httpstatic.StaticOptions{
|
|
SkipLogging: true,
|
|
Prefix: prefix,
|
|
AddHeaders: headers,
|
|
},
|
|
))
|
|
}
|
|
|
|
func (hs *HTTPServer) metricsEndpointBasicAuthEnabled() bool {
|
|
return hs.Cfg.MetricsEndpointBasicAuthUsername != "" && hs.Cfg.MetricsEndpointBasicAuthPassword != ""
|
|
}
|