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/http_server.go

455 lines
13 KiB

package api
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"os"
"path"
"path/filepath"
"sync"
"github.com/grafana/grafana/pkg/services/live"
"github.com/grafana/grafana/pkg/services/search"
Dashboard: Allow shortlink generation (#27409) * intial frontend resolution/redirection logic * backend scaffolding * enough of the frontend to actually test end to end * bugfixes * add tests * cleanup * explore too hard for now * fix build * Docs: add docs * FE test * redirect directly from backend * validate incoming uids * add last_seen_at * format documentation * more documentation feedback * very shaky migration of get route to middleware * persist unix timestamps * add id, orgId to table * fixes for orgId scoping * whoops forgot the middleware * only redirect to absolute URLs under the AppUrl domain * move lookup route to /goto/:uid, stop manually setting 404 response code * renaming things according to PR feedback * tricky deletion * sneaky readd * fix test * more BE renaming * FE updates -- no more @ts-ignore hacking :) and accounting for subpath * Simplify code Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Short URLs: Drop usage of bus Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * ShortURLService: Make injectable Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Rename file Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Add handling of url parsing and creating of full shortURL to backend * Update test, remove unused imports * Update pkg/api/short_urls.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Add correct import * Pass context to short url service * Remove not needed error log * Rename dto and field to denote URL rather than path * Update api docs based on feedback/suggestion * Rename files to singular * Revert to send relative path to backend * Fixes after review * Return dto when creating short URL that includes the full url Use full url to provide shorten URL to the user * Fix after review * Fix relative url path when creating new short url Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> Co-authored-by: Ivana <ivana.huckova@gmail.com> Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
5 years ago
"github.com/grafana/grafana/pkg/services/shorturls"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"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/registry"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/hooks"
"github.com/grafana/grafana/pkg/services/login"
eval "github.com/grafana/grafana/pkg/services/ngalert"
"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/setting"
"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
RouteRegister routing.RouteRegister `inject:""`
Bus bus.Bus `inject:""`
RenderService rendering.Service `inject:""`
Cfg *setting.Cfg `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.LoginService `inject:""`
License models.Licensing `inject:""`
BackendPluginManager backendplugin.Manager `inject:""`
PluginManager *plugins.PluginManager `inject:""`
SearchService *search.SearchService `inject:""`
AlertNG *eval.AlertNG `inject:""`
Dashboard: Allow shortlink generation (#27409) * intial frontend resolution/redirection logic * backend scaffolding * enough of the frontend to actually test end to end * bugfixes * add tests * cleanup * explore too hard for now * fix build * Docs: add docs * FE test * redirect directly from backend * validate incoming uids * add last_seen_at * format documentation * more documentation feedback * very shaky migration of get route to middleware * persist unix timestamps * add id, orgId to table * fixes for orgId scoping * whoops forgot the middleware * only redirect to absolute URLs under the AppUrl domain * move lookup route to /goto/:uid, stop manually setting 404 response code * renaming things according to PR feedback * tricky deletion * sneaky readd * fix test * more BE renaming * FE updates -- no more @ts-ignore hacking :) and accounting for subpath * Simplify code Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Short URLs: Drop usage of bus Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * ShortURLService: Make injectable Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Rename file Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Add handling of url parsing and creating of full shortURL to backend * Update test, remove unused imports * Update pkg/api/short_urls.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Add correct import * Pass context to short url service * Remove not needed error log * Rename dto and field to denote URL rather than path * Update api docs based on feedback/suggestion * Rename files to singular * Revert to send relative path to backend * Fixes after review * Return dto when creating short URL that includes the full url Use full url to provide shorten URL to the user * Fix after review * Fix relative url path when creating new short url Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> Co-authored-by: Ivana <ivana.huckova@gmail.com> Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
5 years ago
ShortURLService *shorturls.ShortURLService `inject:""`
Live *live.GrafanaLive `inject:""`
Listener net.Listener
}
func (hs *HTTPServer) Init() error {
hs.log = log.New("http.server")
hs.macaron = hs.newMacaron()
hs.registerRoutes()
return nil
}
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()
hs.httpSrv = &http.Server{
Addr: net.JoinHostPort(setting.HttpAddr, setting.HttpPort),
Handler: hs.macaron,
}
switch setting.Protocol {
case setting.HTTP2Scheme:
if err := hs.configureHttp2(); err != nil {
return err
}
case setting.HTTPSScheme:
if err := hs.configureHttps(); err != nil {
return err
}
}
listener, err := hs.getListener()
if err != nil {
return err
}
hs.log.Info("HTTP Server Listen", "address", listener.Addr().String(), "protocol",
setting.Protocol, "subUrl", setting.AppSubUrl, "socket", setting.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 setting.Protocol {
case setting.HTTPScheme, setting.SocketScheme:
if err := hs.httpSrv.Serve(listener); err != nil {
if 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, setting.CertFile, setting.KeyFile); err != nil {
if err == http.ErrServerClosed {
hs.log.Debug("server was shutdown gracefully")
return nil
}
return err
}
default:
panic(fmt.Sprintf("Unhandled protocol %q", setting.Protocol))
}
wg.Wait()
return nil
}
func (hs *HTTPServer) getListener() (net.Listener, error) {
if hs.Listener != nil {
return hs.Listener, nil
}
switch setting.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: setting.SocketPath, Net: "unix"})
if err != nil {
return nil, errutil.Wrapf(err, "failed to open listener for socket %s", setting.SocketPath)
}
// Make socket writable by group
if err := os.Chmod(setting.SocketPath, 0660); err != nil {
return nil, errutil.Wrapf(err, "failed to change socket permissions")
}
return listener, nil
default:
hs.log.Error("Invalid protocol", "protocol", setting.Protocol)
return nil, fmt.Errorf("invalid protocol %q", setting.Protocol)
}
}
func (hs *HTTPServer) configureHttps() error {
if setting.CertFile == "" {
return fmt.Errorf("cert_file cannot be empty when using HTTPS")
}
if setting.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_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_AES_128_CBC_SHA,
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 setting.CertFile == "" {
return fmt.Errorf("cert_file cannot be empty when using HTTP2")
}
if setting.KeyFile == "" {
return fmt.Errorf("cert_key cannot be empty when using HTTP2")
}
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: false,
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 = 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(m, 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())
if setting.ServeFromSubPath && setting.AppSubUrl != "" {
m.SetURLPrefix(setting.AppSubUrl)
}
m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: filepath.Join(setting.StaticRootPath, "views"),
IndentJSON: macaron.Env != macaron.PROD,
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
// 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(middleware.GetContextHandler(
hs.AuthTokenService,
hs.RemoteCacheService,
hs.RenderService,
))
m.Use(middleware.OrgRedirect())
// needs to be after context handler
if setting.EnforceDomain {
m.Use(middleware.ValidateHostHeader(setting.Domain))
}
m.Use(middleware.HandleNoCacheHeader())
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{}).
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.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", setting.BuildVersion)
data.Set("commit", setting.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 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 != ""
}