2017-08-22 23:14:15 +08:00
package pluginproxy
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"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"
2017-08-22 23:14:15 +08:00
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
2019-02-02 08:40:57 +08:00
"github.com/grafana/grafana/pkg/social"
2017-08-22 23:14:15 +08:00
"github.com/grafana/grafana/pkg/util"
)
var (
2018-09-06 21:50:16 +08:00
logger = log . New ( "data-proxy-log" )
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
2018-06-12 23:39:38 +08:00
}
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
}
2018-03-08 00:54:50 +08:00
func NewDataSourceProxy ( ds * m . DataSource , plugin * plugins . DataSourcePlugin , ctx * m . ReqContext , proxyPath string ) * 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 ,
2018-06-14 19:39:46 +08:00
}
}
func newHTTPClient ( ) httpClient {
return & http . Client {
2019-01-25 03:04:21 +08:00
Timeout : time . Duration ( setting . DataProxyTimeout ) * 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
}
reverseProxy := & httputil . ReverseProxy {
Director : proxy . getDirector ( ) ,
FlushInterval : time . Millisecond * 200 ,
}
var err error
reverseProxy . Transport , err = proxy . ds . GetHttpTransport ( )
if err != nil {
proxy . ctx . JsonApiErr ( 400 , "Unable to load TLS certificate" , err )
return
}
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" )
2017-09-12 17:06:37 +08:00
opentracing . GlobalTracer ( ) . Inject (
span . Context ( ) ,
opentracing . HTTPHeaders ,
opentracing . HTTPHeadersCarrier ( proxy . ctx . Req . Request . Header ) )
2017-08-22 23:14:15 +08:00
reverseProxy . ServeHTTP ( proxy . ctx . Resp , proxy . ctx . Req . Request )
proxy . ctx . Resp . Header ( ) . Del ( "Set-Cookie" )
}
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 )
}
}
2018-04-16 22:28:20 +08:00
func ( proxy * DataSourceProxy ) useCustomHeaders ( req * http . Request ) {
decryptSdj := proxy . ds . SecureJsonData . Decrypt ( )
index := 1
for {
headerNameSuffix := fmt . Sprintf ( "httpHeaderName%d" , index )
headerValueSuffix := fmt . Sprintf ( "httpHeaderValue%d" , index )
if key := proxy . ds . JsonData . Get ( headerNameSuffix ) . MustString ( ) ; key != "" {
if val , ok := decryptSdj [ headerValueSuffix ] ; ok {
// remove if exists
if req . Header . Get ( key ) != "" {
req . Header . Del ( key )
}
req . Header . Add ( key , val )
logger . Debug ( "Using custom header " , "CustomHeaders" , key )
}
} else {
break
}
index += 1
}
}
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 )
reqQueryVals . Add ( "p" , proxy . ds . Password )
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" )
req . Header . Add ( "Authorization" , util . GetBasicAuthHeader ( proxy . ds . User , proxy . ds . Password ) )
}
} 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" )
req . Header . Add ( "Authorization" , util . GetBasicAuthHeader ( proxy . ds . BasicAuthUser , proxy . ds . BasicAuthPassword ) )
}
2018-04-16 22:28:20 +08:00
// Lookup and use custom headers
if proxy . ds . SecureJsonData != nil {
proxy . useCustomHeaders ( req )
}
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 )
}
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:22:22 +08:00
cmd := & m . GetAuthInfoQuery { UserId : proxy . ctx . UserId }
if err := bus . Dispatch ( cmd ) ; err != nil {
logger . Error ( "Error feching oauth information for user" , "error" , err )
}
provider := cmd . Result . AuthModule
2019-02-02 08:40:57 +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 )
}
// TokenSource handles refreshing the token if it has expired
token , err := connect . TokenSource ( proxy . ctx . Req . Context ( ) , & oauth2 . Token {
AccessToken : cmd . Result . OAuthAccessToken ,
Expiry : cmd . Result . OAuthExpiry ,
RefreshToken : cmd . Result . OAuthRefreshToken ,
TokenType : cmd . Result . OAuthTokenType ,
} ) . Token ( )
if err != nil {
logger . Error ( "Failed to retrieve access token from oauth provider" , "provider" , cmd . Result . AuthModule )
}
// If the tokens are not the same, update the entry in the DB
if token . AccessToken != cmd . Result . OAuthAccessToken {
cmd2 := & m . UpdateAuthInfoCommand {
UserId : cmd . Result . Id ,
AuthModule : cmd . Result . AuthModule ,
AuthId : cmd . Result . AuthId ,
OAuthToken : token ,
}
if err := bus . Dispatch ( cmd2 ) ; err != nil {
logger . Error ( "Failed to update access token during token refresh" , "error" , err )
}
}
req . Header . Del ( "Authorization" )
req . Header . Add ( "Authorization" , fmt . Sprintf ( "%s %s" , token . Type ( ) , token . AccessToken ) )
}
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
}