Support TLS settings, fix #1029

This commit is contained in:
Alexander Zobnin
2020-08-28 16:52:55 +03:00
parent 013fe5c37f
commit 8e781da7bd
3 changed files with 170 additions and 22 deletions

View File

@@ -49,7 +49,7 @@ func newZabbixDatasourceInstance(settings backend.DataSourceInstanceSettings) (i
logger := log.New()
logger.Debug("Initializing new data source instance")
zabbixAPI, err := zabbixapi.New(settings.URL)
zabbixAPI, err := zabbixapi.New(settings.URL, &settings)
if err != nil {
logger.Error("Error initializing Zabbix API", "error", err)
return nil, err

View File

@@ -2,18 +2,80 @@ package httpclient
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net"
"net/http"
"sync"
"time"
simplejson "github.com/bitly/go-simplejson"
"github.com/grafana/grafana-plugin-sdk-go/backend"
)
// NewHttpClient returns new http client
func NewHttpClient() *http.Client {
type proxyTransportCache struct {
cache map[int64]cachedTransport
sync.Mutex
}
// dataSourceTransport implements http.RoundTripper (https://golang.org/pkg/net/http/#RoundTripper)
type dataSourceTransport struct {
headers map[string]string
transport *http.Transport
}
// RoundTrip executes a single HTTP transaction, returning a Response for the provided Request.
func (d *dataSourceTransport) RoundTrip(req *http.Request) (*http.Response, error) {
for key, value := range d.headers {
req.Header.Set(key, value)
}
return d.transport.RoundTrip(req)
}
type cachedTransport struct {
updated time.Time
*dataSourceTransport
}
var ptc = proxyTransportCache{
cache: make(map[int64]cachedTransport),
}
// GetHttpClient returns new http.Client. Transport either initialized or got from cache.
func GetHttpClient(ds *backend.DataSourceInstanceSettings) (*http.Client, error) {
transport, err := getHttpTransport(ds)
if err != nil {
return nil, err
}
return &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
Renegotiation: tls.RenegotiateFreelyAsClient,
},
Timeout: time.Duration(time.Second * 30),
Transport: transport,
}, nil
}
func getHttpTransport(ds *backend.DataSourceInstanceSettings) (*dataSourceTransport, error) {
ptc.Lock()
defer ptc.Unlock()
if t, present := ptc.cache[ds.ID]; present && ds.Updated.Equal(t.updated) {
return t.dataSourceTransport, nil
}
tlsConfig, err := getTLSConfig(ds)
if err != nil {
return nil, err
}
tlsConfig.Renegotiation = tls.RenegotiateFreelyAsClient
// Create transport which adds all
customHeaders := getCustomHeaders(ds)
transport := &http.Transport{
TLSClientConfig: tlsConfig,
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
@@ -23,7 +85,87 @@ func NewHttpClient() *http.Client {
ExpectContinueTimeout: 1 * time.Second,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
},
Timeout: time.Duration(time.Second * 30),
}
dsTransport := &dataSourceTransport{
headers: customHeaders,
transport: transport,
}
ptc.cache[ds.ID] = cachedTransport{
dataSourceTransport: dsTransport,
updated: ds.Updated,
}
return dsTransport, nil
}
func getTLSConfig(ds *backend.DataSourceInstanceSettings) (*tls.Config, error) {
var tlsSkipVerify, tlsClientAuth, tlsAuthWithCACert bool
jsonData, err := simplejson.NewJson(ds.JSONData)
if err != nil {
return nil, err
}
if jsonData != nil {
tlsClientAuth = jsonData.Get("tlsAuth").MustBool(false)
tlsAuthWithCACert = jsonData.Get("tlsAuthWithCACert").MustBool(false)
tlsSkipVerify = jsonData.Get("tlsSkipVerify").MustBool(false)
}
tlsConfig := &tls.Config{
InsecureSkipVerify: tlsSkipVerify,
}
if tlsClientAuth || tlsAuthWithCACert {
decrypted := ds.DecryptedSecureJSONData
if tlsAuthWithCACert && len(decrypted["tlsCACert"]) > 0 {
caPool := x509.NewCertPool()
ok := caPool.AppendCertsFromPEM([]byte(decrypted["tlsCACert"]))
if !ok {
return nil, errors.New("Failed to parse TLS CA PEM certificate")
}
tlsConfig.RootCAs = caPool
}
if tlsClientAuth {
cert, err := tls.X509KeyPair([]byte(decrypted["tlsClientCert"]), []byte(decrypted["tlsClientKey"]))
if err != nil {
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{cert}
}
}
return tlsConfig, nil
}
// getCustomHeaders returns a map with all the to be set headers
// The map key represents the HeaderName and the value represents this header's value
func getCustomHeaders(ds *backend.DataSourceInstanceSettings) map[string]string {
headers := make(map[string]string)
jsonData, err := simplejson.NewJson(ds.JSONData)
if jsonData == nil || err != nil {
return headers
}
decrypted := ds.DecryptedSecureJSONData
index := 1
for {
headerNameSuffix := fmt.Sprintf("httpHeaderName%d", index)
headerValueSuffix := fmt.Sprintf("httpHeaderValue%d", index)
key := jsonData.Get(headerNameSuffix).MustString()
if key == "" {
// No (more) header values are available
break
}
if val, ok := decrypted[headerValueSuffix]; ok {
headers[key] = val
}
index++
}
return headers
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/alexanderzobnin/grafana-zabbix/pkg/httpclient"
"github.com/bitly/go-simplejson"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"golang.org/x/net/context/ctxhttp"
)
@@ -31,17 +32,22 @@ type ZabbixAPI struct {
type ZabbixAPIParams = map[string]interface{}
// New returns new ZabbixAPI instance initialized with given URL or error.
func New(api_url string) (*ZabbixAPI, error) {
func New(api_url string, dsInfo *backend.DataSourceInstanceSettings) (*ZabbixAPI, error) {
apiLogger := log.New()
zabbixURL, err := url.Parse(api_url)
if err != nil {
return nil, err
}
client, err := httpclient.GetHttpClient(dsInfo)
if err != nil {
return nil, err
}
return &ZabbixAPI{
url: zabbixURL,
logger: apiLogger,
httpClient: httpclient.NewHttpClient(),
httpClient: client,
}, nil
}