2017-08-22 23:14:15 +08:00
package pluginproxy
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
2020-02-18 20:26:01 +08:00
"log"
2017-08-22 23:14:15 +08:00
"net"
"net/http"
"net/http/httputil"
"net/url"
2017-08-23 23:18:43 +08:00
"strconv"
2017-08-22 23:14:15 +08:00
"strings"
"time"
2017-09-12 16:25:40 +08:00
"github.com/opentracing/opentracing-go"
2019-02-02 08:40:57 +08:00
"golang.org/x/oauth2"
2017-09-12 16:25:40 +08:00
2019-02-02 08:40:57 +08:00
"github.com/grafana/grafana/pkg/bus"
2020-02-18 20:26:01 +08:00
glog "github.com/grafana/grafana/pkg/infra/log"
2019-03-14 02:34:55 +08:00
"github.com/grafana/grafana/pkg/login/social"
2017-08-22 23:14:15 +08:00
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"
)
var (
2020-02-18 20:26:01 +08:00
logger = glog . New ( "data-proxy-log" )
2018-09-06 21:50:16 +08:00
client = newHTTPClient ( )
2017-08-22 23:14:15 +08:00
)
type DataSourceProxy struct {
2018-06-14 19:39:46 +08:00
ds * m . DataSource
ctx * m . ReqContext
targetUrl * url . URL
proxyPath string
route * plugins . AppPluginRoute
plugin * plugins . DataSourcePlugin
2019-03-14 20:04:47 +08:00
cfg * setting . Cfg
2018-06-12 23:39:38 +08:00
}
2020-01-15 20:03:12 +08:00
type handleResponseTransport struct {
transport http . RoundTripper
}
func ( t * handleResponseTransport ) RoundTrip ( req * http . Request ) ( * http . Response , error ) {
res , err := t . transport . RoundTrip ( req )
if err != nil {
return nil , err
}
res . Header . Del ( "Set-Cookie" )
return res , nil
}
2018-06-14 19:39:46 +08:00
type httpClient interface {
2018-06-12 23:39:38 +08:00
Do ( req * http . Request ) ( * http . Response , error )
2017-08-22 23:14:15 +08:00
}
2020-02-18 20:26:01 +08:00
type logWrapper struct {
logger glog . Logger
}
// Write writes log messages as bytes from proxy
func ( lw * logWrapper ) Write ( p [ ] byte ) ( n int , err error ) {
withoutNewline := strings . TrimSuffix ( string ( p ) , "\n" )
lw . logger . Error ( "Data proxy error" , "error" , withoutNewline )
return len ( p ) , nil
}
// NewDataSourceProxy creates a new Datasource proxy
2019-03-14 20:04:47 +08:00
func NewDataSourceProxy ( ds * m . DataSource , plugin * plugins . DataSourcePlugin , ctx * m . ReqContext , proxyPath string , cfg * setting . Cfg ) * DataSourceProxy {
2018-03-23 05:13:46 +08:00
targetURL , _ := url . Parse ( ds . Url )
2017-08-23 16:52:31 +08:00
2017-08-22 23:14:15 +08:00
return & DataSourceProxy {
ds : ds ,
2017-08-23 16:52:31 +08:00
plugin : plugin ,
2017-08-22 23:14:15 +08:00
ctx : ctx ,
proxyPath : proxyPath ,
2018-03-23 05:13:46 +08:00
targetUrl : targetURL ,
2019-03-14 20:04:47 +08:00
cfg : cfg ,
2018-06-14 19:39:46 +08:00
}
}
func newHTTPClient ( ) httpClient {
return & http . Client {
2019-02-11 20:42:05 +08:00
Timeout : 30 * time . Second ,
2018-06-14 19:39:46 +08:00
Transport : & http . Transport { Proxy : http . ProxyFromEnvironment } ,
2017-08-22 23:14:15 +08:00
}
}
func ( proxy * DataSourceProxy ) HandleRequest ( ) {
if err := proxy . validateRequest ( ) ; err != nil {
proxy . ctx . JsonApiErr ( 403 , err . Error ( ) , nil )
return
}
2020-02-18 20:26:01 +08:00
proxyErrorLogger := logger . New ( "userId" , proxy . ctx . UserId , "orgId" , proxy . ctx . OrgId , "uname" , proxy . ctx . Login , "path" , proxy . ctx . Req . URL . Path , "remote_addr" , proxy . ctx . RemoteAddr ( ) , "referer" , proxy . ctx . Req . Referer ( ) )
2017-08-22 23:14:15 +08:00
reverseProxy := & httputil . ReverseProxy {
Director : proxy . getDirector ( ) ,
FlushInterval : time . Millisecond * 200 ,
2020-02-18 20:26:01 +08:00
ErrorLog : log . New ( & logWrapper { logger : proxyErrorLogger } , "" , 0 ) ,
2017-08-22 23:14:15 +08:00
}
2020-01-15 20:03:12 +08:00
transport , err := proxy . ds . GetHttpTransport ( )
2017-08-22 23:14:15 +08:00
if err != nil {
proxy . ctx . JsonApiErr ( 400 , "Unable to load TLS certificate" , err )
return
}
2020-01-15 20:03:12 +08:00
reverseProxy . Transport = & handleResponseTransport {
transport : transport ,
}
2017-08-22 23:14:15 +08:00
proxy . logRequest ( )
2017-09-12 16:25:40 +08:00
span , ctx := opentracing . StartSpanFromContext ( proxy . ctx . Req . Context ( ) , "datasource reverse proxy" )
proxy . ctx . Req . Request = proxy . ctx . Req . WithContext ( ctx )
defer span . Finish ( )
span . SetTag ( "datasource_id" , proxy . ds . Id )
span . SetTag ( "datasource_type" , proxy . ds . Type )
span . SetTag ( "user_id" , proxy . ctx . SignedInUser . UserId )
span . SetTag ( "org_id" , proxy . ctx . SignedInUser . OrgId )
2018-03-21 05:21:24 +08:00
proxy . addTraceFromHeaderValue ( span , "X-Panel-Id" , "panel_id" )
proxy . addTraceFromHeaderValue ( span , "X-Dashboard-Id" , "dashboard_id" )
2019-10-09 00:57:53 +08:00
if err := opentracing . GlobalTracer ( ) . Inject (
2017-09-12 17:06:37 +08:00
span . Context ( ) ,
opentracing . HTTPHeaders ,
2019-10-09 00:57:53 +08:00
opentracing . HTTPHeadersCarrier ( proxy . ctx . Req . Request . Header ) ) ; err != nil {
logger . Error ( "Failed to inject span context instance" , "err" , err )
}
2017-09-12 17:06:37 +08:00
2017-08-22 23:14:15 +08:00
reverseProxy . ServeHTTP ( proxy . ctx . Resp , proxy . ctx . Req . Request )
}
2018-03-21 05:21:24 +08:00
func ( proxy * DataSourceProxy ) addTraceFromHeaderValue ( span opentracing . Span , headerName string , tagName string ) {
panelId := proxy . ctx . Req . Header . Get ( headerName )
dashId , err := strconv . Atoi ( panelId )
if err == nil {
span . SetTag ( tagName , dashId )
}
}
2017-08-22 23:14:15 +08:00
func ( proxy * DataSourceProxy ) getDirector ( ) func ( req * http . Request ) {
return func ( req * http . Request ) {
req . URL . Scheme = proxy . targetUrl . Scheme
req . URL . Host = proxy . targetUrl . Host
req . Host = proxy . targetUrl . Host
reqQueryVals := req . URL . Query ( )
if proxy . ds . Type == m . DS_INFLUXDB_08 {
2019-01-29 05:18:48 +08:00
req . URL . Path = util . JoinURLFragments ( proxy . targetUrl . Path , "db/" + proxy . ds . Database + "/" + proxy . proxyPath )
2017-08-22 23:14:15 +08:00
reqQueryVals . Add ( "u" , proxy . ds . User )
2019-04-15 17:11:17 +08:00
reqQueryVals . Add ( "p" , proxy . ds . DecryptedPassword ( ) )
2017-08-22 23:14:15 +08:00
req . URL . RawQuery = reqQueryVals . Encode ( )
} else if proxy . ds . Type == m . DS_INFLUXDB {
2019-01-29 05:18:48 +08:00
req . URL . Path = util . JoinURLFragments ( proxy . targetUrl . Path , proxy . proxyPath )
2017-08-22 23:14:15 +08:00
req . URL . RawQuery = reqQueryVals . Encode ( )
if ! proxy . ds . BasicAuth {
req . Header . Del ( "Authorization" )
2019-04-15 17:11:17 +08:00
req . Header . Add ( "Authorization" , util . GetBasicAuthHeader ( proxy . ds . User , proxy . ds . DecryptedPassword ( ) ) )
2017-08-22 23:14:15 +08:00
}
} else {
2019-01-29 05:18:48 +08:00
req . URL . Path = util . JoinURLFragments ( proxy . targetUrl . Path , proxy . proxyPath )
2017-08-22 23:14:15 +08:00
}
if proxy . ds . BasicAuth {
req . Header . Del ( "Authorization" )
2019-04-15 17:11:17 +08:00
req . Header . Add ( "Authorization" , util . GetBasicAuthHeader ( proxy . ds . BasicAuthUser , proxy . ds . DecryptedBasicAuthPassword ( ) ) )
2017-08-22 23:14:15 +08:00
}
dsAuth := req . Header . Get ( "X-DS-Authorization" )
if len ( dsAuth ) > 0 {
req . Header . Del ( "X-DS-Authorization" )
req . Header . Del ( "Authorization" )
req . Header . Add ( "Authorization" , dsAuth )
}
2019-03-14 23:28:32 +08:00
if proxy . cfg . SendUserHeader && ! proxy . ctx . SignedInUser . IsAnonymous {
2019-03-14 20:04:47 +08:00
req . Header . Add ( "X-Grafana-User" , proxy . ctx . SignedInUser . Login )
}
2017-10-18 06:47:15 +08:00
// clear cookie header, except for whitelisted cookies
var keptCookies [ ] * http . Cookie
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 )
}
}
}
}
}
2017-08-22 23:14:15 +08:00
req . Header . Del ( "Cookie" )
2017-10-18 06:47:15 +08:00
for _ , c := range keptCookies {
req . AddCookie ( c )
}
2017-08-22 23:14:15 +08:00
// 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" )
2018-08-15 15:46:59 +08:00
req . Header . Set ( "User-Agent" , fmt . Sprintf ( "Grafana/%s" , setting . BuildVersion ) )
2017-08-22 23:14:15 +08:00
2018-11-08 19:30:10 +08:00
// Clear Origin and Referer to avoir CORS issues
req . Header . Del ( "Origin" )
req . Header . Del ( "Referer" )
2017-08-22 23:14:15 +08:00
// 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 {
2018-09-10 23:49:13 +08:00
ApplyRoute ( proxy . ctx . Req . Context ( ) , req , proxy . proxyPath , proxy . route , proxy . ds )
2017-08-22 23:14:15 +08:00
}
2019-02-02 08:40:57 +08:00
if proxy . ds . JsonData != nil && proxy . ds . JsonData . Get ( "oauthPassThru" ) . MustBool ( ) {
2019-03-14 01:45:32 +08:00
addOAuthPassThruAuth ( proxy . ctx , req )
2019-02-02 08:40:57 +08:00
}
2017-08-22 23:14:15 +08:00
}
}
func ( proxy * DataSourceProxy ) validateRequest ( ) error {
2017-08-23 16:52:31 +08:00
if ! checkWhiteList ( proxy . ctx , proxy . targetUrl . Host ) {
2017-08-22 23:14:15 +08:00
return errors . New ( "Target url is not a valid target" )
}
if proxy . ds . Type == m . DS_PROMETHEUS {
2017-11-12 15:38:10 +08:00
if proxy . ctx . Req . Request . Method == "DELETE" {
return errors . New ( "Deletes not allowed on proxied Prometheus datasource" )
}
if proxy . ctx . Req . Request . Method == "PUT" {
return errors . New ( "Puts not allowed on proxied Prometheus datasource" )
}
if proxy . ctx . Req . Request . Method == "POST" && ! ( proxy . proxyPath == "api/v1/query" || proxy . proxyPath == "api/v1/query_range" ) {
return errors . New ( "Posts not allowed on proxied Prometheus datasource except on /query and /query_range" )
2017-08-22 23:14:15 +08:00
}
}
if proxy . ds . Type == m . DS_ES {
if proxy . ctx . Req . Request . Method == "DELETE" {
return errors . New ( "Deletes not allowed on proxied Elasticsearch datasource" )
}
if proxy . ctx . Req . Request . Method == "PUT" {
return errors . New ( "Puts not allowed on proxied Elasticsearch datasource" )
}
if proxy . ctx . Req . Request . Method == "POST" && proxy . proxyPath != "_msearch" {
return errors . New ( "Posts not allowed on proxied Elasticsearch datasource except on /_msearch" )
}
}
// found route if there are any
2017-08-23 16:52:31 +08:00
if len ( proxy . plugin . Routes ) > 0 {
for _ , route := range proxy . plugin . Routes {
// method match
if route . Method != "" && route . Method != "*" && route . Method != proxy . ctx . Req . Method {
continue
}
2017-08-22 23:14:15 +08:00
2017-08-23 16:52:31 +08:00
if route . ReqRole . IsValid ( ) {
if ! proxy . ctx . HasUserRole ( route . ReqRole ) {
return errors . New ( "Plugin proxy route access denied" )
2017-08-22 23:14:15 +08:00
}
}
2017-08-23 16:52:31 +08:00
if strings . HasPrefix ( proxy . proxyPath , route . Path ) {
proxy . route = route
break
}
2017-08-22 23:14:15 +08:00
}
}
return nil
}
func ( proxy * DataSourceProxy ) logRequest ( ) {
if ! setting . DataProxyLogging {
return
}
var body string
if proxy . ctx . Req . Request . Body != nil {
buffer , err := ioutil . ReadAll ( proxy . ctx . Req . Request . Body )
if err == nil {
proxy . ctx . Req . Request . Body = ioutil . NopCloser ( bytes . NewBuffer ( buffer ) )
body = string ( buffer )
}
}
logger . Info ( "Proxying incoming request" ,
"userid" , proxy . ctx . UserId ,
"orgid" , proxy . ctx . OrgId ,
"username" , proxy . ctx . Login ,
"datasource" , proxy . ds . Type ,
"uri" , proxy . ctx . Req . RequestURI ,
"method" , proxy . ctx . Req . Request . Method ,
"body" , body )
}
2018-03-08 00:54:50 +08:00
func checkWhiteList ( c * m . ReqContext , host string ) bool {
2017-08-22 23:14:15 +08:00
if host != "" && len ( setting . DataProxyWhiteList ) > 0 {
if _ , exists := setting . DataProxyWhiteList [ host ] ; ! exists {
c . JsonApiErr ( 403 , "Data proxy hostname and ip are not included in whitelist" , nil )
return false
}
}
return true
}
2019-03-14 01:45:32 +08:00
func addOAuthPassThruAuth ( c * m . ReqContext , req * http . Request ) {
2019-03-21 02:32:41 +08:00
authInfoQuery := & m . GetAuthInfoQuery { UserId : c . UserId }
if err := bus . Dispatch ( authInfoQuery ) ; err != nil {
2019-03-14 01:45:32 +08:00
logger . Error ( "Error feching oauth information for user" , "error" , err )
return
}
2019-03-21 02:32:41 +08:00
provider := authInfoQuery . Result . AuthModule
2019-03-14 01:45:32 +08:00
connect , ok := social . SocialMap [ strings . TrimPrefix ( provider , "oauth_" ) ] // The socialMap keys don't have "oauth_" prefix, but everywhere else in the system does
if ! ok {
logger . Error ( "Failed to find oauth provider with given name" , "provider" , provider )
return
}
// TokenSource handles refreshing the token if it has expired
token , err := connect . TokenSource ( c . Req . Context ( ) , & oauth2 . Token {
2019-03-21 02:32:41 +08:00
AccessToken : authInfoQuery . Result . OAuthAccessToken ,
Expiry : authInfoQuery . Result . OAuthExpiry ,
RefreshToken : authInfoQuery . Result . OAuthRefreshToken ,
TokenType : authInfoQuery . Result . OAuthTokenType ,
2019-03-14 01:45:32 +08:00
} ) . Token ( )
if err != nil {
2019-11-16 00:14:53 +08:00
logger . Error ( "Failed to retrieve access token from oauth provider" , "provider" , authInfoQuery . Result . AuthModule , "error" , err )
2019-03-14 01:45:32 +08:00
return
}
// If the tokens are not the same, update the entry in the DB
2019-03-21 02:32:41 +08:00
if token . AccessToken != authInfoQuery . Result . OAuthAccessToken {
updateAuthCommand := & m . UpdateAuthInfoCommand {
2019-06-12 19:47:38 +08:00
UserId : authInfoQuery . Result . UserId ,
2019-03-21 02:32:41 +08:00
AuthModule : authInfoQuery . Result . AuthModule ,
AuthId : authInfoQuery . Result . AuthId ,
2019-03-14 01:45:32 +08:00
OAuthToken : token ,
}
2019-03-21 02:32:41 +08:00
if err := bus . Dispatch ( updateAuthCommand ) ; err != nil {
2019-03-14 01:45:32 +08:00
logger . Error ( "Failed to update access token during token refresh" , "error" , err )
return
}
}
req . Header . Del ( "Authorization" )
req . Header . Add ( "Authorization" , fmt . Sprintf ( "%s %s" , token . Type ( ) , token . AccessToken ) )
}