mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-02-14 10:19:54 +00:00
* 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
259 lines
6.4 KiB
Go
259 lines
6.4 KiB
Go
package collect
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
neturl "net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
|
"k8s.io/client-go/kubernetes"
|
|
"k8s.io/client-go/rest"
|
|
"k8s.io/klog/v2"
|
|
)
|
|
|
|
type HTTPResponse struct {
|
|
Status int `json:"status"`
|
|
Body string `json:"body"`
|
|
Headers map[string]string `json:"headers"`
|
|
RawJSON json.RawMessage `json:"raw_json,omitempty"`
|
|
}
|
|
|
|
type HTTPError struct {
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type CollectHTTP struct {
|
|
Collector *troubleshootv1beta2.HTTP
|
|
BundlePath string
|
|
Namespace string
|
|
ClientConfig *rest.Config
|
|
Client kubernetes.Interface
|
|
RBACErrors
|
|
}
|
|
|
|
func (c *CollectHTTP) Title() string {
|
|
return getCollectorName(c)
|
|
}
|
|
|
|
func (c *CollectHTTP) IsExcluded() (bool, error) {
|
|
return isExcluded(c.Collector.Exclude)
|
|
}
|
|
|
|
func (c *CollectHTTP) Collect(progressChan chan<- interface{}) (CollectorResult, error) {
|
|
var response *http.Response
|
|
var err error
|
|
|
|
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, 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, 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, c.Collector.Put.TLS, c.Collector.Put.Proxy)
|
|
default:
|
|
return nil, errors.New("no supported http request type")
|
|
}
|
|
|
|
o, err := responseToOutput(response, err)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fileName := "result.json"
|
|
if c.Collector.CollectorName != "" {
|
|
fileName = c.Collector.CollectorName + ".json"
|
|
}
|
|
|
|
output := NewResult()
|
|
output.SaveResult(c.BundlePath, filepath.Join(c.Collector.Name, fileName), bytes.NewBuffer(o))
|
|
|
|
return output, nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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 {
|
|
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
|
|
}
|
|
|
|
for k, v := range headers {
|
|
req.Header.Set(k, v)
|
|
}
|
|
|
|
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 {
|
|
output["error"] = HTTPError{
|
|
Message: err.Error(),
|
|
}
|
|
} else {
|
|
body, err := io.ReadAll(response.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
headers := make(map[string]string)
|
|
for k, v := range response.Header {
|
|
headers[k] = strings.Join(v, ",")
|
|
}
|
|
|
|
var rawJSON json.RawMessage
|
|
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),
|
|
Headers: headers,
|
|
RawJSON: rawJSON,
|
|
}
|
|
}
|
|
|
|
b, err := json.MarshalIndent(output, "", " ")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return b, nil
|
|
}
|
|
|
|
// parseTimeout parses a string into a time.Duration.
|
|
// If the string is empty, it returns 0.
|
|
func parseTimeout(s string) (time.Duration, error) {
|
|
var timeout time.Duration
|
|
var err error
|
|
if s == "" {
|
|
timeout = 0
|
|
} else {
|
|
timeout, err = time.ParseDuration(s)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
|
|
if timeout < 0 {
|
|
return 0, errors.New("timeout must be a positive duration")
|
|
}
|
|
|
|
return timeout, nil
|
|
}
|