2022-05-06 04:58:02 -04:00
|
|
|
package api
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/url"
|
|
|
|
|
|
|
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
2022-06-27 12:23:15 -04:00
|
|
|
|
2023-09-11 06:13:13 -04:00
|
|
|
"github.com/grafana/grafana/pkg/api/response"
|
|
|
|
|
"github.com/grafana/grafana/pkg/middleware/requestmeta"
|
2023-09-25 06:10:47 -04:00
|
|
|
"github.com/grafana/grafana/pkg/plugins"
|
2023-04-28 08:02:27 -04:00
|
|
|
"github.com/grafana/grafana/pkg/plugins/httpresponsesender"
|
2023-01-27 02:50:36 -05:00
|
|
|
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
2022-06-27 12:23:15 -04:00
|
|
|
"github.com/grafana/grafana/pkg/services/datasources"
|
2022-05-06 04:58:02 -04:00
|
|
|
"github.com/grafana/grafana/pkg/util/proxyutil"
|
|
|
|
|
"github.com/grafana/grafana/pkg/web"
|
|
|
|
|
)
|
|
|
|
|
|
2026-05-13 12:53:11 -04:00
|
|
|
const maxResourceBodySize = 128 * 1024 * 1024 // 128 MiB
|
|
|
|
|
|
2022-05-06 04:58:02 -04:00
|
|
|
// CallResource passes a resource call from a plugin to the backend plugin.
|
|
|
|
|
//
|
|
|
|
|
// /api/plugins/:pluginId/resources/*
|
2023-01-27 02:50:36 -05:00
|
|
|
func (hs *HTTPServer) CallResource(c *contextmodel.ReqContext) {
|
2022-05-06 04:58:02 -04:00
|
|
|
hs.callPluginResource(c, web.Params(c.Req)[":pluginId"])
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-27 02:50:36 -05:00
|
|
|
func (hs *HTTPServer) callPluginResource(c *contextmodel.ReqContext, pluginID string) {
|
2025-04-10 08:42:23 -04:00
|
|
|
pCtx, err := hs.pluginContextProvider.Get(c.Req.Context(), pluginID, c.SignedInUser, c.GetOrgID())
|
2022-05-06 04:58:02 -04:00
|
|
|
if err != nil {
|
2023-09-25 06:10:47 -04:00
|
|
|
if errors.Is(err, plugins.ErrPluginNotRegistered) {
|
2023-06-08 07:59:51 -04:00
|
|
|
c.JsonApiErr(404, "Plugin not found", nil)
|
|
|
|
|
return
|
|
|
|
|
}
|
2022-05-06 04:58:02 -04:00
|
|
|
c.JsonApiErr(500, "Failed to get plugin settings", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req, err := hs.pluginResourceRequest(c)
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.JsonApiErr(http.StatusBadRequest, "Failed for create plugin resource request", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err = hs.makePluginResourceRequest(c.Resp, req, pCtx); err != nil {
|
|
|
|
|
handleCallResourceError(err, c)
|
2023-09-11 06:13:13 -04:00
|
|
|
return
|
2022-05-06 04:58:02 -04:00
|
|
|
}
|
2023-09-11 06:13:13 -04:00
|
|
|
|
|
|
|
|
requestmeta.WithStatusSource(c.Req.Context(), c.Resp.Status())
|
2022-05-06 04:58:02 -04:00
|
|
|
}
|
|
|
|
|
|
2023-01-27 02:50:36 -05:00
|
|
|
func (hs *HTTPServer) callPluginResourceWithDataSource(c *contextmodel.ReqContext, pluginID string, ds *datasources.DataSource) {
|
2023-06-08 07:59:51 -04:00
|
|
|
pCtx, err := hs.pluginContextProvider.GetWithDataSource(c.Req.Context(), pluginID, c.SignedInUser, ds)
|
2022-05-06 04:58:02 -04:00
|
|
|
if err != nil {
|
2023-09-25 06:10:47 -04:00
|
|
|
if errors.Is(err, plugins.ErrPluginNotRegistered) {
|
2023-06-08 07:59:51 -04:00
|
|
|
c.JsonApiErr(404, "Plugin not found", nil)
|
|
|
|
|
return
|
|
|
|
|
}
|
2022-05-06 04:58:02 -04:00
|
|
|
c.JsonApiErr(500, "Failed to get plugin settings", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-28 14:47:07 -04:00
|
|
|
err = hs.DataSourceRequestValidator.Validate(ds.URL, ds.JsonDataMap(), c.Req)
|
2022-05-06 04:58:02 -04:00
|
|
|
if err != nil {
|
|
|
|
|
c.JsonApiErr(http.StatusForbidden, "Access denied", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req, err := hs.pluginResourceRequest(c)
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.JsonApiErr(http.StatusBadRequest, "Failed for create plugin resource request", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err = hs.makePluginResourceRequest(c.Resp, req, pCtx); err != nil {
|
|
|
|
|
handleCallResourceError(err, c)
|
2023-09-11 06:13:13 -04:00
|
|
|
return
|
2022-05-06 04:58:02 -04:00
|
|
|
}
|
2023-09-11 06:13:13 -04:00
|
|
|
|
|
|
|
|
requestmeta.WithStatusSource(c.Req.Context(), c.Resp.Status())
|
2022-05-06 04:58:02 -04:00
|
|
|
}
|
|
|
|
|
|
2023-01-27 02:50:36 -05:00
|
|
|
func (hs *HTTPServer) pluginResourceRequest(c *contextmodel.ReqContext) (*http.Request, error) {
|
2022-05-06 04:58:02 -04:00
|
|
|
clonedReq := c.Req.Clone(c.Req.Context())
|
|
|
|
|
rawURL := web.Params(c.Req)["*"]
|
2024-01-29 04:31:49 -05:00
|
|
|
|
|
|
|
|
clonedReq.URL = &url.URL{
|
|
|
|
|
Path: rawURL,
|
|
|
|
|
RawQuery: clonedReq.URL.RawQuery,
|
2022-05-06 04:58:02 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return clonedReq, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (hs *HTTPServer) makePluginResourceRequest(w http.ResponseWriter, req *http.Request, pCtx backend.PluginContext) error {
|
|
|
|
|
proxyutil.PrepareProxyRequest(req)
|
|
|
|
|
|
2026-05-13 12:53:11 -04:00
|
|
|
body, err := io.ReadAll(http.MaxBytesReader(w, req.Body, maxResourceBodySize))
|
2022-05-06 04:58:02 -04:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to read request body: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
crReq := &backend.CallResourceRequest{
|
|
|
|
|
PluginContext: pCtx,
|
|
|
|
|
Path: req.URL.Path,
|
|
|
|
|
Method: req.Method,
|
|
|
|
|
URL: req.URL.String(),
|
|
|
|
|
Headers: req.Header,
|
|
|
|
|
Body: body,
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-28 08:02:27 -04:00
|
|
|
httpSender := httpresponsesender.New(w)
|
|
|
|
|
return hs.pluginClient.CallResource(req.Context(), crReq, httpSender)
|
2022-05-06 04:58:02 -04:00
|
|
|
}
|
|
|
|
|
|
2023-01-27 02:50:36 -05:00
|
|
|
func handleCallResourceError(err error, reqCtx *contextmodel.ReqContext) {
|
2026-05-13 12:53:11 -04:00
|
|
|
var maxBytesErr *http.MaxBytesError
|
|
|
|
|
if errors.As(err, &maxBytesErr) {
|
|
|
|
|
resp := response.Error(http.StatusRequestEntityTooLarge, "Request body too large", err)
|
|
|
|
|
resp.WriteTo(reqCtx)
|
|
|
|
|
return
|
|
|
|
|
}
|
2023-09-11 06:13:13 -04:00
|
|
|
resp := response.ErrOrFallback(http.StatusInternalServerError, "Failed to call resource", err)
|
|
|
|
|
resp.WriteTo(reqCtx)
|
2022-05-06 04:58:02 -04:00
|
|
|
}
|