grafana/pkg/api/http_server.go
Serge Zaitsev 707d3536f0
Macaron: Strip down renderer middleware (#37627)
* strip down macaron renderer

* inline renderHTML

* remove IndentJSON parameter

* replace renderer with a html/template set

* fix failing test

* fix renderer paths in tests

* make template reloading even simpler

* unify ignored gzip path lookup

* fix csp middleware usage
2021-08-10 13:29:46 +02:00

480 lines
15 KiB
Go

package api
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"sync"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/services/librarypanels"
"github.com/grafana/grafana/pkg/services/oauthtoken"
"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/localcache"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
_ "github.com/grafana/grafana/pkg/plugins/backendplugin/manager"
"github.com/grafana/grafana/pkg/plugins/plugincontext"
"github.com/grafana/grafana/pkg/plugins/plugindashboards"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/services/datasourceproxy"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/hooks"
"github.com/grafana/grafana/pkg/services/live"
"github.com/grafana/grafana/pkg/services/live/pushhttp"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
"github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/schemaloader"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/services/shorturls"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb"
"github.com/grafana/grafana/pkg/util/errutil"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
macaron "gopkg.in/macaron.v1"
)
func init() {
registry.Register(&registry.Descriptor{
Name: "HTTPServer",
Instance: &HTTPServer{},
InitPriority: registry.High,
})
}
type HTTPServer struct {
log log.Logger
macaron *macaron.Macaron
context context.Context
httpSrv *http.Server
middlewares []macaron.Handler
PluginContextProvider *plugincontext.Provider `inject:""`
RouteRegister routing.RouteRegister `inject:""`
Bus bus.Bus `inject:""`
RenderService rendering.Service `inject:""`
Cfg *setting.Cfg `inject:""`
SettingsProvider setting.Provider `inject:""`
HooksService *hooks.HooksService `inject:""`
CacheService *localcache.CacheService `inject:""`
DatasourceCache datasources.CacheService `inject:""`
AuthTokenService models.UserTokenService `inject:""`
QuotaService *quota.QuotaService `inject:""`
RemoteCacheService *remotecache.RemoteCache `inject:""`
ProvisioningService provisioning.ProvisioningService `inject:""`
Login login.Service `inject:""`
License models.Licensing `inject:""`
AccessControl accesscontrol.AccessControl `inject:""`
BackendPluginManager backendplugin.Manager `inject:""`
DataProxy *datasourceproxy.DatasourceProxyService `inject:""`
PluginRequestValidator models.PluginRequestValidator `inject:""`
PluginManager plugins.Manager `inject:""`
SearchService *search.SearchService `inject:""`
ShortURLService shorturls.Service `inject:""`
Live *live.GrafanaLive `inject:""`
LivePushGateway *pushhttp.Gateway `inject:""`
ContextHandler *contexthandler.ContextHandler `inject:""`
SQLStore *sqlstore.SQLStore `inject:""`
DataService *tsdb.Service `inject:""`
PluginDashboardService *plugindashboards.Service `inject:""`
AlertEngine *alerting.AlertEngine `inject:""`
LoadSchemaService *schemaloader.SchemaLoaderService `inject:""`
Alertmanager *notifier.Alertmanager `inject:""`
LibraryPanelService librarypanels.Service `inject:""`
LibraryElementService libraryelements.Service `inject:""`
SocialService social.Service `inject:""`
OAuthTokenService *oauthtoken.Service `inject:""`
Listener net.Listener
}
func (hs *HTTPServer) Init() error {
hs.log = log.New("http.server")
hs.macaron = hs.newMacaron()
hs.registerRoutes()
return hs.declareFixedRoles()
}
func (hs *HTTPServer) AddMiddleware(middleware macaron.Handler) {
hs.middlewares = append(hs.middlewares, middleware)
}
func (hs *HTTPServer) Run(ctx context.Context) error {
hs.context = ctx
hs.applyRoutes()
// Remove any square brackets enclosing IPv6 addresses, a format we support for backwards compatibility
host := strings.TrimSuffix(strings.TrimPrefix(hs.Cfg.HTTPAddr, "["), "]")
hs.httpSrv = &http.Server{
Addr: net.JoinHostPort(host, hs.Cfg.HTTPPort),
Handler: hs.macaron,
ReadTimeout: hs.Cfg.ReadTimeout,
}
switch hs.Cfg.Protocol {
case setting.HTTP2Scheme:
if err := hs.configureHttp2(); err != nil {
return err
}
case setting.HTTPSScheme:
if err := hs.configureHttps(); err != nil {
return err
}
default:
}
listener, err := hs.getListener()
if err != nil {
return err
}
hs.log.Info("HTTP Server Listen", "address", listener.Addr().String(), "protocol",
hs.Cfg.Protocol, "subUrl", hs.Cfg.AppSubURL, "socket", hs.Cfg.SocketPath)
var wg sync.WaitGroup
wg.Add(1)
// handle http shutdown on server context done
go func() {
defer wg.Done()
<-ctx.Done()
if err := hs.httpSrv.Shutdown(context.Background()); err != nil {
hs.log.Error("Failed to shutdown server", "error", err)
}
}()
switch hs.Cfg.Protocol {
case setting.HTTPScheme, setting.SocketScheme:
if err := hs.httpSrv.Serve(listener); err != nil {
if errors.Is(err, http.ErrServerClosed) {
hs.log.Debug("server was shutdown gracefully")
return nil
}
return err
}
case setting.HTTP2Scheme, setting.HTTPSScheme:
if err := hs.httpSrv.ServeTLS(listener, hs.Cfg.CertFile, hs.Cfg.KeyFile); err != nil {
if errors.Is(err, http.ErrServerClosed) {
hs.log.Debug("server was shutdown gracefully")
return nil
}
return err
}
default:
panic(fmt.Sprintf("Unhandled protocol %q", hs.Cfg.Protocol))
}
wg.Wait()
return nil
}
func (hs *HTTPServer) getListener() (net.Listener, error) {
if hs.Listener != nil {
return hs.Listener, nil
}
switch hs.Cfg.Protocol {
case setting.HTTPScheme, setting.HTTPSScheme, setting.HTTP2Scheme:
listener, err := net.Listen("tcp", hs.httpSrv.Addr)
if err != nil {
return nil, errutil.Wrapf(err, "failed to open listener on address %s", hs.httpSrv.Addr)
}
return listener, nil
case setting.SocketScheme:
listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: hs.Cfg.SocketPath, Net: "unix"})
if err != nil {
return nil, errutil.Wrapf(err, "failed to open listener for socket %s", hs.Cfg.SocketPath)
}
// Make socket writable by group
// nolint:gosec
if err := os.Chmod(hs.Cfg.SocketPath, 0660); err != nil {
return nil, errutil.Wrapf(err, "failed to change socket permissions")
}
return listener, nil
default:
hs.log.Error("Invalid protocol", "protocol", hs.Cfg.Protocol)
return nil, fmt.Errorf("invalid protocol %q", hs.Cfg.Protocol)
}
}
func (hs *HTTPServer) configureHttps() error {
if hs.Cfg.CertFile == "" {
return fmt.Errorf("cert_file cannot be empty when using HTTPS")
}
if hs.Cfg.KeyFile == "" {
return fmt.Errorf("cert_key cannot be empty when using HTTPS")
}
if _, err := os.Stat(hs.Cfg.CertFile); os.IsNotExist(err) {
return fmt.Errorf(`cannot find SSL cert_file at %q`, hs.Cfg.CertFile)
}
if _, err := os.Stat(hs.Cfg.KeyFile); os.IsNotExist(err) {
return fmt.Errorf(`cannot find SSL key_file at %q`, hs.Cfg.KeyFile)
}
tlsCfg := &tls.Config{
MinVersion: tls.VersionTLS12,
PreferServerCipherSuites: true,
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
},
}
hs.httpSrv.TLSConfig = tlsCfg
hs.httpSrv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
return nil
}
func (hs *HTTPServer) configureHttp2() error {
if hs.Cfg.CertFile == "" {
return fmt.Errorf("cert_file cannot be empty when using HTTP2")
}
if hs.Cfg.KeyFile == "" {
return fmt.Errorf("cert_key cannot be empty when using HTTP2")
}
if _, err := os.Stat(hs.Cfg.CertFile); os.IsNotExist(err) {
return fmt.Errorf(`cannot find SSL cert_file at %q`, hs.Cfg.CertFile)
}
if _, err := os.Stat(hs.Cfg.KeyFile); os.IsNotExist(err) {
return fmt.Errorf(`cannot find SSL key_file at %q`, hs.Cfg.KeyFile)
}
tlsCfg := &tls.Config{
MinVersion: tls.VersionTLS12,
PreferServerCipherSuites: true,
CipherSuites: []uint16{
tls.TLS_CHACHA20_POLY1305_SHA256,
tls.TLS_AES_128_GCM_SHA256,
tls.TLS_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
},
NextProtos: []string{"h2", "http/1.1"},
}
hs.httpSrv.TLSConfig = tlsCfg
return nil
}
func (hs *HTTPServer) newMacaron() *macaron.Macaron {
macaron.Env = hs.Cfg.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(middleware.ReqSignedIn, hs.NotFoundHandler)
}
func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
m := hs.macaron
m.Use(middleware.RequestTracing())
m.Use(middleware.Logger(hs.Cfg))
if hs.Cfg.EnableGzip {
m.UseMiddleware(middleware.Gziper())
}
m.Use(middleware.Recovery(hs.Cfg))
hs.mapStatic(m, hs.Cfg.StaticRootPath, "build", "public/build")
hs.mapStatic(m, hs.Cfg.StaticRootPath, "", "public")
hs.mapStatic(m, hs.Cfg.StaticRootPath, "robots.txt", "robots.txt")
if hs.Cfg.ImageUploadProvider == "local" {
hs.mapStatic(m, hs.Cfg.ImagesDir, "", "/public/img/attachments")
}
m.Use(middleware.AddDefaultResponseHeaders(hs.Cfg))
if hs.Cfg.ServeFromSubPath && hs.Cfg.AppSubURL != "" {
m.SetURLPrefix(hs.Cfg.AppSubURL)
}
m.UseMiddleware(macaron.Renderer(filepath.Join(hs.Cfg.StaticRootPath, "views"), "[[", "]]"))
// These endpoints are used for monitoring the Grafana instance
// and should not be redirected or rejected.
m.Use(hs.healthzHandler)
m.Use(hs.apiHealthHandler)
m.Use(hs.metricsEndpoint)
m.Use(hs.ContextHandler.Middleware)
m.Use(middleware.OrgRedirect(hs.Cfg))
// needs to be after context handler
if hs.Cfg.EnforceDomain {
m.Use(middleware.ValidateHostHeader(hs.Cfg))
}
m.Use(middleware.HandleNoCacheHeader)
m.UseMiddleware(middleware.AddCSPHeader(hs.Cfg, hs.log))
for _, mw := range hs.middlewares {
m.Use(mw)
}
}
func (hs *HTTPServer) metricsEndpoint(ctx *macaron.Context) {
if !hs.Cfg.MetricsEndpointEnabled {
return
}
if ctx.Req.Method != http.MethodGet || 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{EnableOpenMetrics: true}).
ServeHTTP(ctx.Resp, ctx.Req.Request)
}
// healthzHandler always return 200 - Ok if Grafana's web server is running
func (hs *HTTPServer) healthzHandler(ctx *macaron.Context) {
notHeadOrGet := ctx.Req.Method != http.MethodGet && ctx.Req.Method != http.MethodHead
if notHeadOrGet || ctx.Req.URL.Path != "/healthz" {
return
}
ctx.Resp.WriteHeader(200)
_, err := ctx.Resp.Write([]byte("Ok"))
if err != nil {
hs.log.Error("could not write to response", "err", err)
}
}
// apiHealthHandler will return ok if Grafana's web server is running and it
// can access the database. If the database cannot be accessed it will return
// http status code 503.
func (hs *HTTPServer) apiHealthHandler(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")
if !hs.Cfg.AnonymousHideVersion {
data.Set("version", hs.Cfg.BuildVersion)
data.Set("commit", hs.Cfg.BuildCommit)
}
if !hs.databaseHealthy() {
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, err := data.EncodePretty()
if err != nil {
hs.log.Error("Failed to encode data", "err", err)
return
}
if _, err := ctx.Resp.Write(dataBytes); err != nil {
hs.log.Error("Failed to write to response", "err", err)
}
}
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 hs.Cfg.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 != ""
}