Backend plugins: Prepare and clean request headers before resource calls (#22321)
Moves common request proxy utilities to proxyutil package with support for removing X-Forwarded-Host, X-Forwarded-Port, X-Forwarded-Proto headers, setting X-Forwarded-For header and cleaning Cookie header. Using the proxyutil package to prepare and clean request headers before resource calls. Closes #21512
This commit is contained in:
+4
-4
@@ -252,8 +252,8 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
apiRoute.Get("/plugins/:pluginId/settings", Wrap(GetPluginSettingByID))
|
||||
apiRoute.Get("/plugins/:pluginId/markdown/:name", Wrap(GetPluginMarkdown))
|
||||
apiRoute.Get("/plugins/:pluginId/health", Wrap(hs.CheckHealth))
|
||||
apiRoute.Any("/plugins/:pluginId/resources", Wrap(hs.CallResource))
|
||||
apiRoute.Any("/plugins/:pluginId/resources/*", Wrap(hs.CallResource))
|
||||
apiRoute.Any("/plugins/:pluginId/resources", hs.CallResource)
|
||||
apiRoute.Any("/plugins/:pluginId/resources/*", hs.CallResource)
|
||||
|
||||
apiRoute.Group("/plugins", func(pluginRoute routing.RouteRegister) {
|
||||
pluginRoute.Get("/:pluginId/dashboards/", Wrap(GetPluginDashboards))
|
||||
@@ -263,8 +263,8 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
apiRoute.Get("/frontend/settings/", hs.GetFrontendSettings)
|
||||
apiRoute.Any("/datasources/proxy/:id/*", reqSignedIn, hs.ProxyDataSourceRequest)
|
||||
apiRoute.Any("/datasources/proxy/:id", reqSignedIn, hs.ProxyDataSourceRequest)
|
||||
apiRoute.Any("/datasources/:id/resources", Wrap(hs.CallDatasourceResource))
|
||||
apiRoute.Any("/datasources/:id/resources/*", Wrap(hs.CallDatasourceResource))
|
||||
apiRoute.Any("/datasources/:id/resources", hs.CallDatasourceResource)
|
||||
apiRoute.Any("/datasources/:id/resources/*", hs.CallDatasourceResource)
|
||||
|
||||
// Folders
|
||||
apiRoute.Group("/folders", func(folderRoute routing.RouteRegister) {
|
||||
|
||||
+23
-50
@@ -255,70 +255,43 @@ func GetDataSourceIdByName(c *m.ReqContext) Response {
|
||||
}
|
||||
|
||||
// /api/datasources/:id/resources/*
|
||||
func (hs *HTTPServer) CallDatasourceResource(c *m.ReqContext) Response {
|
||||
func (hs *HTTPServer) CallDatasourceResource(c *m.ReqContext) {
|
||||
datasourceID := c.ParamsInt64(":id")
|
||||
ds, err := hs.DatasourceCache.GetDatasource(datasourceID, c.SignedInUser, c.SkipCache)
|
||||
if err != nil {
|
||||
if err == m.ErrDataSourceAccessDenied {
|
||||
return Error(403, "Access denied to datasource", err)
|
||||
c.JsonApiErr(403, "Access denied to datasource", err)
|
||||
return
|
||||
}
|
||||
return Error(500, "Unable to load datasource meta data", err)
|
||||
c.JsonApiErr(500, "Unable to load datasource meta data", err)
|
||||
return
|
||||
}
|
||||
|
||||
// find plugin
|
||||
plugin, ok := plugins.DataSources[ds.Type]
|
||||
if !ok {
|
||||
return Error(500, "Unable to find datasource plugin", err)
|
||||
c.JsonApiErr(500, "Unable to find datasource plugin", err)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := c.Req.Body().Bytes()
|
||||
if err != nil {
|
||||
return Error(500, "Failed to read request body", err)
|
||||
}
|
||||
|
||||
jsonDataBytes, err := ds.JsonData.MarshalJSON()
|
||||
if err != nil {
|
||||
return Error(500, "Failed to marshal JSON data to bytes", err)
|
||||
}
|
||||
|
||||
req := backendplugin.CallResourceRequest{
|
||||
Config: backendplugin.PluginConfig{
|
||||
OrgID: c.OrgId,
|
||||
PluginID: plugin.Id,
|
||||
PluginType: plugin.Type,
|
||||
JSONData: jsonDataBytes,
|
||||
DecryptedSecureJSONData: ds.DecryptedValues(),
|
||||
Updated: ds.Updated,
|
||||
DataSourceConfig: &backendplugin.DataSourceConfig{
|
||||
ID: ds.Id,
|
||||
Name: ds.Name,
|
||||
URL: ds.Url,
|
||||
Database: ds.Database,
|
||||
User: ds.User,
|
||||
BasicAuthEnabled: ds.BasicAuth,
|
||||
BasicAuthUser: ds.BasicAuthUser,
|
||||
},
|
||||
config := backendplugin.PluginConfig{
|
||||
OrgID: c.OrgId,
|
||||
PluginID: plugin.Id,
|
||||
PluginType: plugin.Type,
|
||||
JSONData: ds.JsonData,
|
||||
DecryptedSecureJSONData: ds.DecryptedValues(),
|
||||
Updated: ds.Updated,
|
||||
DataSourceConfig: &backendplugin.DataSourceConfig{
|
||||
ID: ds.Id,
|
||||
Name: ds.Name,
|
||||
URL: ds.Url,
|
||||
Database: ds.Database,
|
||||
User: ds.User,
|
||||
BasicAuthEnabled: ds.BasicAuth,
|
||||
BasicAuthUser: ds.BasicAuthUser,
|
||||
},
|
||||
Path: c.Params("*"),
|
||||
Method: c.Req.Method,
|
||||
URL: c.Req.URL.String(),
|
||||
Headers: c.Req.Header.Clone(),
|
||||
Body: body,
|
||||
}
|
||||
resp, err := hs.BackendPluginManager.CallResource(c.Req.Context(), req)
|
||||
if err != nil {
|
||||
return Error(500, "Failed to call datasource resource", err)
|
||||
}
|
||||
|
||||
if resp.Status >= 400 {
|
||||
return Error(resp.Status, "", nil)
|
||||
}
|
||||
|
||||
return &NormalResponse{
|
||||
body: resp.Body,
|
||||
status: resp.Status,
|
||||
header: resp.Headers,
|
||||
}
|
||||
hs.BackendPluginManager.CallResource(config, c, c.Params("*"))
|
||||
}
|
||||
|
||||
func convertModelToDtos(ds *m.DataSource) dtos.DataSource {
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
@@ -24,6 +23,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/util/proxyutil"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -185,48 +185,22 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
|
||||
req.Header.Add("X-Grafana-User", proxy.ctx.SignedInUser.Login)
|
||||
}
|
||||
|
||||
// clear cookie header, except for whitelisted cookies
|
||||
var keptCookies []*http.Cookie
|
||||
keepCookieNames := []string{}
|
||||
if proxy.ds.JsonData != nil {
|
||||
if keepCookies := proxy.ds.JsonData.Get("keepCookies"); keepCookies != nil {
|
||||
keepCookieNames := keepCookies.MustStringArray()
|
||||
for _, c := range req.Cookies() {
|
||||
for _, v := range keepCookieNames {
|
||||
if c.Name == v {
|
||||
keptCookies = append(keptCookies, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
keepCookieNames = keepCookies.MustStringArray()
|
||||
}
|
||||
}
|
||||
req.Header.Del("Cookie")
|
||||
for _, c := range keptCookies {
|
||||
req.AddCookie(c)
|
||||
}
|
||||
|
||||
// clear X-Forwarded Host/Port/Proto headers
|
||||
req.Header.Del("X-Forwarded-Host")
|
||||
req.Header.Del("X-Forwarded-Port")
|
||||
req.Header.Del("X-Forwarded-Proto")
|
||||
proxyutil.ClearCookieHeader(req, keepCookieNames)
|
||||
proxyutil.PrepareProxyRequest(req)
|
||||
|
||||
req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion))
|
||||
|
||||
// Clear Origin and Referer to avoir CORS issues
|
||||
req.Header.Del("Origin")
|
||||
req.Header.Del("Referer")
|
||||
|
||||
// set X-Forwarded-For header
|
||||
if req.RemoteAddr != "" {
|
||||
remoteAddr, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||
if err != nil {
|
||||
remoteAddr = req.RemoteAddr
|
||||
}
|
||||
if req.Header.Get("X-Forwarded-For") != "" {
|
||||
req.Header.Set("X-Forwarded-For", req.Header.Get("X-Forwarded-For")+", "+remoteAddr)
|
||||
} else {
|
||||
req.Header.Set("X-Forwarded-For", remoteAddr)
|
||||
}
|
||||
}
|
||||
|
||||
if proxy.route != nil {
|
||||
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds)
|
||||
}
|
||||
|
||||
@@ -2,18 +2,17 @@ package pluginproxy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/util/proxyutil"
|
||||
)
|
||||
|
||||
type templateData struct {
|
||||
@@ -71,23 +70,7 @@ func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPl
|
||||
req.Header.Del("Cookie")
|
||||
req.Header.Del("Set-Cookie")
|
||||
|
||||
// clear X-Forwarded Host/Port/Proto headers
|
||||
req.Header.Del("X-Forwarded-Host")
|
||||
req.Header.Del("X-Forwarded-Port")
|
||||
req.Header.Del("X-Forwarded-Proto")
|
||||
|
||||
// set X-Forwarded-For header
|
||||
if req.RemoteAddr != "" {
|
||||
remoteAddr, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||
if err != nil {
|
||||
remoteAddr = req.RemoteAddr
|
||||
}
|
||||
if req.Header.Get("X-Forwarded-For") != "" {
|
||||
req.Header.Set("X-Forwarded-For", req.Header.Get("X-Forwarded-For")+", "+remoteAddr)
|
||||
} else {
|
||||
req.Header.Set("X-Forwarded-For", remoteAddr)
|
||||
}
|
||||
}
|
||||
proxyutil.PrepareProxyRequest(req)
|
||||
|
||||
// Create a HTTP header with the context in it.
|
||||
ctxJSON, err := json.Marshal(ctx.SignedInUser)
|
||||
|
||||
+18
-44
@@ -1,16 +1,16 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@@ -238,66 +238,40 @@ func (hs *HTTPServer) CheckHealth(c *models.ReqContext) Response {
|
||||
}
|
||||
|
||||
// /api/plugins/:pluginId/resources/*
|
||||
func (hs *HTTPServer) CallResource(c *models.ReqContext) Response {
|
||||
func (hs *HTTPServer) CallResource(c *models.ReqContext) {
|
||||
pluginID := c.Params("pluginId")
|
||||
plugin, exists := plugins.Plugins[pluginID]
|
||||
if !exists {
|
||||
return Error(404, "Plugin not found, no installed plugin with that id", nil)
|
||||
c.JsonApiErr(404, "Plugin not found, no installed plugin with that id", nil)
|
||||
return
|
||||
}
|
||||
|
||||
var jsonDataBytes []byte
|
||||
var jsonData *simplejson.Json
|
||||
var decryptedSecureJSONData map[string]string
|
||||
var updated time.Time
|
||||
|
||||
ps, err := hs.getCachedPluginSettings(pluginID, c.SignedInUser)
|
||||
if err != nil {
|
||||
if err != models.ErrPluginSettingNotFound {
|
||||
return Error(500, "Failed to get plugin settings", err)
|
||||
c.JsonApiErr(500, "Failed to get plugin settings", err)
|
||||
return
|
||||
}
|
||||
jsonData = simplejson.New()
|
||||
decryptedSecureJSONData = make(map[string]string)
|
||||
} else {
|
||||
jsonDataBytes, err = json.Marshal(&ps.JsonData)
|
||||
if err != nil {
|
||||
return Error(500, "Failed to marshal JSON data to bytes", err)
|
||||
}
|
||||
|
||||
decryptedSecureJSONData = ps.DecryptedValues()
|
||||
updated = ps.Updated
|
||||
}
|
||||
|
||||
body, err := c.Req.Body().Bytes()
|
||||
if err != nil {
|
||||
return Error(500, "Failed to read request body", err)
|
||||
}
|
||||
|
||||
req := backendplugin.CallResourceRequest{
|
||||
Config: backendplugin.PluginConfig{
|
||||
OrgID: c.OrgId,
|
||||
PluginID: plugin.Id,
|
||||
PluginType: plugin.Type,
|
||||
JSONData: jsonDataBytes,
|
||||
DecryptedSecureJSONData: decryptedSecureJSONData,
|
||||
Updated: updated,
|
||||
},
|
||||
Path: c.Params("*"),
|
||||
Method: c.Req.Method,
|
||||
URL: c.Req.URL.String(),
|
||||
Headers: c.Req.Header.Clone(),
|
||||
Body: body,
|
||||
}
|
||||
resp, err := hs.BackendPluginManager.CallResource(c.Req.Context(), req)
|
||||
if err != nil {
|
||||
return Error(500, "Failed to call resource", err)
|
||||
}
|
||||
|
||||
if resp.Status >= 400 {
|
||||
return Error(resp.Status, "", nil)
|
||||
}
|
||||
|
||||
return &NormalResponse{
|
||||
body: resp.Body,
|
||||
status: resp.Status,
|
||||
header: resp.Headers,
|
||||
config := backendplugin.PluginConfig{
|
||||
OrgID: c.OrgId,
|
||||
PluginID: plugin.Id,
|
||||
PluginType: plugin.Type,
|
||||
JSONData: jsonData,
|
||||
DecryptedSecureJSONData: decryptedSecureJSONData,
|
||||
Updated: updated,
|
||||
}
|
||||
hs.BackendPluginManager.CallResource(config, c, c.Params("*"))
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) getCachedPluginSettings(pluginID string, user *models.SignedInUser) (*models.PluginSetting, error) {
|
||||
|
||||
Reference in New Issue
Block a user