mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-08-27 00:37:20 +00:00
support adding a CA cert to http collector (#1624)
* add a TLS parameter for cacert * pass a ca cert into http request * test preflight * make schemas * log extra information from http request * pass a proxy into the collector spec * hitting a segfault; breakpoint * accept a dir, file, or a string-literal as CA * move tls params into get, put, post methods * test for cert untrusted response * make generate * make schemas * more test cases * make schemas * dont include system certs * make generate && make schemas * resolve gosec G402 warning * remove old check for system certs * ignore errcheck "return value not checked" linter errors
This commit is contained in:
@@ -186,7 +186,9 @@ type Get struct {
|
||||
Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"`
|
||||
// Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.
|
||||
// Missing value or empty string or means no timeout.
|
||||
Timeout string `json:"timeout,omitempty" yaml:"timeout,omitempty"`
|
||||
Timeout string `json:"timeout,omitempty" yaml:"timeout,omitempty"`
|
||||
TLS *TLSParams `json:"tls,omitempty" yaml:"tls,omitempty"`
|
||||
Proxy string `json:"proxy,omitempty" yaml:"proxy,omitempty"`
|
||||
}
|
||||
|
||||
type Post struct {
|
||||
@@ -196,7 +198,9 @@ type Post struct {
|
||||
Body string `json:"body,omitempty" yaml:"body,omitempty"`
|
||||
// Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.
|
||||
// Missing value or empty string or means no timeout.
|
||||
Timeout string `json:"timeout,omitempty" yaml:"timeout,omitempty"`
|
||||
Timeout string `json:"timeout,omitempty" yaml:"timeout,omitempty"`
|
||||
TLS *TLSParams `json:"tls,omitempty" yaml:"tls,omitempty"`
|
||||
Proxy string `json:"proxy,omitempty" yaml:"proxy,omitempty"`
|
||||
}
|
||||
|
||||
type Put struct {
|
||||
@@ -206,7 +210,9 @@ type Put struct {
|
||||
Body string `json:"body,omitempty" yaml:"body,omitempty"`
|
||||
// Timeout is the time to wait for a server's response. Its a duration e.g 15s, 2h30m.
|
||||
// Missing value or empty string or means no timeout.
|
||||
Timeout string `json:"timeout,omitempty" yaml:"timeout,omitempty"`
|
||||
Timeout string `json:"timeout,omitempty" yaml:"timeout,omitempty"`
|
||||
TLS *TLSParams `json:"tls,omitempty" yaml:"tls,omitempty"`
|
||||
Proxy string `json:"proxy,omitempty" yaml:"proxy,omitempty"`
|
||||
}
|
||||
|
||||
type Database struct {
|
||||
|
||||
@@ -1620,6 +1620,11 @@ func (in *Get) DeepCopyInto(out *Get) {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.TLS != nil {
|
||||
in, out := &in.TLS, &out.TLS
|
||||
*out = new(TLSParams)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Get.
|
||||
@@ -3416,6 +3421,11 @@ func (in *Post) DeepCopyInto(out *Post) {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.TLS != nil {
|
||||
in, out := &in.TLS, &out.TLS
|
||||
*out = new(TLSParams)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Post.
|
||||
@@ -3560,6 +3570,11 @@ func (in *Put) DeepCopyInto(out *Put) {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.TLS != nil {
|
||||
in, out := &in.TLS, &out.TLS
|
||||
*out = new(TLSParams)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Put.
|
||||
|
||||
@@ -32,15 +32,15 @@ func (c *CollectHostHTTP) Collect(progressChan chan<- interface{}) (map[string][
|
||||
case httpCollector.Get != nil:
|
||||
response, err = doRequest(
|
||||
"GET", httpCollector.Get.URL, httpCollector.Get.Headers,
|
||||
"", httpCollector.Get.InsecureSkipVerify, httpCollector.Get.Timeout)
|
||||
"", httpCollector.Get.InsecureSkipVerify, httpCollector.Get.Timeout, httpCollector.Get.TLS, httpCollector.Get.Proxy)
|
||||
case httpCollector.Post != nil:
|
||||
response, err = doRequest(
|
||||
"POST", httpCollector.Post.URL, httpCollector.Post.Headers,
|
||||
httpCollector.Post.Body, httpCollector.Post.InsecureSkipVerify, httpCollector.Post.Timeout)
|
||||
httpCollector.Post.Body, httpCollector.Post.InsecureSkipVerify, httpCollector.Post.Timeout, httpCollector.Post.TLS, httpCollector.Post.Proxy)
|
||||
case httpCollector.Put != nil:
|
||||
response, err = doRequest(
|
||||
"PUT", httpCollector.Put.URL, httpCollector.Put.Headers,
|
||||
httpCollector.Put.Body, httpCollector.Put.InsecureSkipVerify, httpCollector.Put.Timeout)
|
||||
httpCollector.Put.Body, httpCollector.Put.InsecureSkipVerify, httpCollector.Put.Timeout, httpCollector.Put.TLS, httpCollector.Put.Proxy)
|
||||
default:
|
||||
return nil, errors.New("no supported http request type")
|
||||
}
|
||||
|
||||
+101
-16
@@ -3,9 +3,13 @@ package collect
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
neturl "net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -52,16 +56,13 @@ func (c *CollectHTTP) Collect(progressChan chan<- interface{}) (CollectorResult,
|
||||
switch {
|
||||
case c.Collector.Get != nil:
|
||||
response, err = doRequest(
|
||||
"GET", c.Collector.Get.URL, c.Collector.Get.Headers,
|
||||
"", c.Collector.Get.InsecureSkipVerify, c.Collector.Get.Timeout)
|
||||
"GET", c.Collector.Get.URL, c.Collector.Get.Headers, "", c.Collector.Get.InsecureSkipVerify, c.Collector.Get.Timeout, c.Collector.Get.TLS, c.Collector.Get.Proxy)
|
||||
case c.Collector.Post != nil:
|
||||
response, err = doRequest(
|
||||
"POST", c.Collector.Post.URL, c.Collector.Post.Headers,
|
||||
c.Collector.Post.Body, c.Collector.Post.InsecureSkipVerify, c.Collector.Post.Timeout)
|
||||
"POST", c.Collector.Post.URL, c.Collector.Post.Headers, c.Collector.Post.Body, c.Collector.Post.InsecureSkipVerify, c.Collector.Post.Timeout, c.Collector.Post.TLS, c.Collector.Post.Proxy)
|
||||
case c.Collector.Put != nil:
|
||||
response, err = doRequest(
|
||||
"PUT", c.Collector.Put.URL, c.Collector.Put.Headers,
|
||||
c.Collector.Put.Body, c.Collector.Put.InsecureSkipVerify, c.Collector.Put.Timeout)
|
||||
"PUT", c.Collector.Put.URL, c.Collector.Put.Headers, c.Collector.Put.Body, c.Collector.Put.InsecureSkipVerify, c.Collector.Put.Timeout, c.Collector.Put.TLS, c.Collector.Put.Proxy)
|
||||
default:
|
||||
return nil, errors.New("no supported http request type")
|
||||
}
|
||||
@@ -82,24 +83,74 @@ func (c *CollectHTTP) Collect(progressChan chan<- interface{}) (CollectorResult,
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func doRequest(method, url string, headers map[string]string, body string, insecureSkipVerify bool, timeout string) (*http.Response, error) {
|
||||
func handleFileOrDir(path string) (bool, error) {
|
||||
f, err := os.Stat(path)
|
||||
if err != nil {
|
||||
klog.V(2).Infof("Failed to stat file path: %s\n", err)
|
||||
return false, err
|
||||
}
|
||||
if f.IsDir() {
|
||||
os.Setenv("SSL_CERT_DIR", path)
|
||||
klog.V(2).Infof("Using SSL_CERT_DIR: %s\n", path)
|
||||
} else if f.Mode().IsRegular() {
|
||||
os.Setenv("SSL_CERT_FILE", path)
|
||||
klog.V(2).Infof("Using SSL_CERT_FILE: %s\n", path)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func isPEMCertificate(s string) bool {
|
||||
return strings.Contains(s, "BEGIN CERTIFICATE") || strings.Contains(s, "BEGIN RSA PRIVATE KEY")
|
||||
}
|
||||
|
||||
func doRequest(method, url string, headers map[string]string, body string, insecureSkipVerify bool, timeout string, tlsParams *troubleshootv1beta2.TLSParams, proxy string) (*http.Response, error) {
|
||||
|
||||
t, err := parseTimeout(timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpClient := &http.Client{
|
||||
Timeout: t,
|
||||
tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12}
|
||||
httpTransport := &http.Transport{}
|
||||
|
||||
if tlsParams != nil && tlsParams.CACert != "" {
|
||||
if isPEMCertificate(tlsParams.CACert) {
|
||||
klog.V(2).Infof("Using PEM certificate from spec\n")
|
||||
certPool := x509.NewCertPool()
|
||||
if !certPool.AppendCertsFromPEM([]byte(tlsParams.CACert)) {
|
||||
return nil, errors.New("failed to append certificate to cert pool")
|
||||
}
|
||||
tlsConfig.RootCAs = certPool
|
||||
} else if _, err := handleFileOrDir(tlsParams.CACert); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to handle cacert file path")
|
||||
}
|
||||
}
|
||||
|
||||
if insecureSkipVerify {
|
||||
httpClient.Transport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
}
|
||||
|
||||
httpTransport.TLSClientConfig = tlsConfig
|
||||
|
||||
if proxy != "" || os.Getenv("HTTPS_PROXY") != "" {
|
||||
if proxy != "" {
|
||||
klog.V(2).Infof("Using proxy from spec: %s\n", proxy)
|
||||
httpTransport.Proxy = func(req *http.Request) (*neturl.URL, error) {
|
||||
return neturl.Parse(proxy)
|
||||
}
|
||||
} else {
|
||||
klog.V(2).Infof("Using proxy from environment: %s\n", os.Getenv("HTTPS_PROXY"))
|
||||
httpTransport.Proxy = http.ProxyFromEnvironment
|
||||
}
|
||||
}
|
||||
|
||||
httpClient := &http.Client{
|
||||
Timeout: t,
|
||||
Transport: &LoggingTransport{
|
||||
Transport: httpTransport,
|
||||
},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, url, strings.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -112,6 +163,36 @@ func doRequest(method, url string, headers map[string]string, body string, insec
|
||||
return httpClient.Do(req)
|
||||
}
|
||||
|
||||
type LoggingTransport struct {
|
||||
Transport http.RoundTripper
|
||||
}
|
||||
|
||||
func (t *LoggingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
// Log the request
|
||||
dumpReq, err := httputil.DumpRequestOut(req, true)
|
||||
if err != nil {
|
||||
klog.V(2).Infof("Failed to dump request: %+v\n", err)
|
||||
} else {
|
||||
klog.V(2).Infof("Request: %s\n", dumpReq)
|
||||
}
|
||||
|
||||
resp, err := t.Transport.RoundTrip(req)
|
||||
|
||||
// Log the response
|
||||
if err != nil {
|
||||
klog.V(2).Infof("Request failed: %+v\n", err)
|
||||
} else {
|
||||
dumpResp, err := httputil.DumpResponse(resp, true)
|
||||
if err != nil {
|
||||
klog.V(2).Infof("Failed to dump response: %v+\n", err)
|
||||
} else {
|
||||
klog.V(2).Infof("Response: %s\n", dumpResp)
|
||||
}
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func responseToOutput(response *http.Response, err error) ([]byte, error) {
|
||||
output := make(map[string]interface{})
|
||||
if err != nil {
|
||||
@@ -130,11 +211,15 @@ func responseToOutput(response *http.Response, err error) ([]byte, error) {
|
||||
}
|
||||
|
||||
var rawJSON json.RawMessage
|
||||
if err := json.Unmarshal(body, &rawJSON); err != nil {
|
||||
klog.Infof("failed to unmarshal response body as JSON: %v", err)
|
||||
if len(body) > 0 {
|
||||
if err := json.Unmarshal(body, &rawJSON); err != nil {
|
||||
klog.Infof("failed to unmarshal response body as JSON: %+v", err)
|
||||
rawJSON = json.RawMessage{}
|
||||
}
|
||||
} else {
|
||||
rawJSON = json.RawMessage{}
|
||||
klog.V(2).Infof("empty response body\n")
|
||||
}
|
||||
|
||||
output["response"] = HTTPResponse{
|
||||
Status: response.StatusCode,
|
||||
Body: string(body),
|
||||
|
||||
@@ -83,6 +83,13 @@ func TestCollectHTTP_Collect(t *testing.T) {
|
||||
res.WriteHeader(http.StatusInternalServerError)
|
||||
res.Write([]byte("{\"error\": { \"message\": \"context deadline exceeded\"}}"))
|
||||
})
|
||||
mux.HandleFunc("/certificate-mismatch", func(res http.ResponseWriter, req *http.Request) {
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
fmt.Println("Sleeping for 2 seconds on /error call")
|
||||
res.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
res.WriteHeader(http.StatusInternalServerError)
|
||||
res.Write([]byte("{\"error\": { \"message\": \"Request failed: proxyconnect tcp: tls: failed to verify certificate: x509: \"10.0.0.254\" certificate is not trusted\"}}"))
|
||||
})
|
||||
|
||||
sample_get_response := &ResponseData{
|
||||
Response: Response{
|
||||
@@ -125,9 +132,15 @@ func TestCollectHTTP_Collect(t *testing.T) {
|
||||
Message: "context deadline exceeded",
|
||||
},
|
||||
}
|
||||
|
||||
sample_error_bytes, _ := sample_error_response.ToJSONbytes()
|
||||
|
||||
sample_certificate_untrusted := &ErrorResponse{
|
||||
Error: HTTPError{
|
||||
Message: "Request failed: proxyconnect tcp: tls: failed to verify certificate: x509: \"10.0.0.254\" certificate is not trusted",
|
||||
},
|
||||
}
|
||||
sample_certificate_untrusted_bytes, _ := sample_certificate_untrusted.ToJSONbytes()
|
||||
|
||||
tests := []CollectorTest{
|
||||
{
|
||||
// check valid file path when CollectorName is not supplied
|
||||
@@ -250,7 +263,25 @@ func TestCollectHTTP_Collect(t *testing.T) {
|
||||
checkTimeout: true,
|
||||
wantErr: false,
|
||||
},
|
||||
// TODO: add TLS cert case
|
||||
{
|
||||
name: "TLS: certificate is not trusted",
|
||||
Collector: &troubleshootv1beta2.HTTP{
|
||||
CollectorMeta: troubleshootv1beta2.CollectorMeta{
|
||||
CollectorName: "",
|
||||
},
|
||||
Get: &troubleshootv1beta2.Get{
|
||||
Timeout: "300ms",
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
progressChan: nil,
|
||||
},
|
||||
want: CollectorResult{
|
||||
"result.json": sample_certificate_untrusted_bytes,
|
||||
},
|
||||
checkTimeout: true,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
var ts *httptest.Server
|
||||
@@ -273,6 +304,9 @@ func TestCollectHTTP_Collect(t *testing.T) {
|
||||
c.Collector.Get.URL = fmt.Sprintf("%s%s", url, "/error")
|
||||
response_data := sample_error_response
|
||||
response_data.testCollectHTTP(t, &tt, c)
|
||||
c.Collector.Get.URL = fmt.Sprintf("%s%s", url, "/certificate-mismatch")
|
||||
response_data = sample_certificate_untrusted
|
||||
response_data.testCollectHTTP(t, &tt, c)
|
||||
} else {
|
||||
c.Collector.Get.URL = fmt.Sprintf("%s%s", url, "/get")
|
||||
response_data := sample_get_response
|
||||
|
||||
Reference in New Issue
Block a user