2019-12-09 17:35:31 +01:00
|
|
|
package http
|
|
|
|
|
|
|
|
import (
|
2020-01-14 10:29:50 +01:00
|
|
|
"bytes"
|
2020-05-25 19:48:59 +02:00
|
|
|
"context"
|
2020-11-09 11:47:03 +01:00
|
|
|
"crypto/tls"
|
2021-08-19 11:29:33 +02:00
|
|
|
"crypto/x509"
|
2020-07-14 10:58:57 +02:00
|
|
|
"encoding/json"
|
|
|
|
"encoding/xml"
|
2019-12-09 17:35:31 +01:00
|
|
|
"fmt"
|
|
|
|
"io"
|
2020-07-14 10:58:57 +02:00
|
|
|
"io/ioutil"
|
2020-01-14 10:29:50 +01:00
|
|
|
"mime/multipart"
|
2020-03-23 15:02:22 +01:00
|
|
|
"net"
|
2019-12-09 17:35:31 +01:00
|
|
|
"net/http"
|
2020-01-14 10:29:50 +01:00
|
|
|
"os"
|
2021-08-19 11:29:33 +02:00
|
|
|
"path"
|
|
|
|
"path/filepath"
|
2020-01-14 10:29:50 +01:00
|
|
|
"strings"
|
2019-12-09 17:35:31 +01:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/SAP/jenkins-library/pkg/log"
|
2021-06-23 14:41:52 +02:00
|
|
|
"github.com/SAP/jenkins-library/pkg/piperutils"
|
2021-08-19 11:29:33 +02:00
|
|
|
"github.com/SAP/jenkins-library/pkg/reporting"
|
2020-11-11 13:35:53 +01:00
|
|
|
"github.com/hashicorp/go-retryablehttp"
|
2020-05-25 19:48:59 +02:00
|
|
|
"github.com/motemen/go-nuts/roundtime"
|
2019-12-09 17:35:31 +01:00
|
|
|
"github.com/pkg/errors"
|
2020-01-14 10:29:50 +01:00
|
|
|
"github.com/sirupsen/logrus"
|
2019-12-09 17:35:31 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
// Client defines an http client object
|
|
|
|
type Client struct {
|
2020-11-09 11:47:03 +01:00
|
|
|
maxRequestDuration time.Duration
|
2020-11-11 13:35:53 +01:00
|
|
|
maxRetries int
|
2020-11-09 11:47:03 +01:00
|
|
|
transportTimeout time.Duration
|
|
|
|
transportSkipVerification bool
|
|
|
|
username string
|
|
|
|
password string
|
|
|
|
token string
|
|
|
|
logger *logrus.Entry
|
|
|
|
cookieJar http.CookieJar
|
|
|
|
doLogRequestBodyOnDebug bool
|
|
|
|
doLogResponseBodyOnDebug bool
|
2021-02-04 14:58:35 +01:00
|
|
|
useDefaultTransport bool
|
2021-08-19 11:29:33 +02:00
|
|
|
trustedCerts []string
|
2019-12-09 17:35:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// ClientOptions defines the options to be set on the client
|
|
|
|
type ClientOptions struct {
|
2020-03-23 15:02:22 +01:00
|
|
|
// MaxRequestDuration has a default value of "0", meaning "no maximum
|
|
|
|
// request duration". If it is greater than 0, an overall, hard timeout
|
|
|
|
// for the request will be enforced. This should only be used if the
|
|
|
|
// length of the request bodies is known.
|
|
|
|
MaxRequestDuration time.Duration
|
2020-11-11 13:35:53 +01:00
|
|
|
MaxRetries int
|
2020-06-10 11:14:55 +02:00
|
|
|
// TransportTimeout defaults to 3 minutes, if not specified. It is
|
2020-03-23 15:02:22 +01:00
|
|
|
// used for the transport layer and duration of handshakes and such.
|
2020-11-09 11:47:03 +01:00
|
|
|
TransportTimeout time.Duration
|
|
|
|
TransportSkipVerification bool
|
|
|
|
Username string
|
|
|
|
Password string
|
|
|
|
Token string
|
|
|
|
Logger *logrus.Entry
|
|
|
|
CookieJar http.CookieJar
|
|
|
|
DoLogRequestBodyOnDebug bool
|
|
|
|
DoLogResponseBodyOnDebug bool
|
2021-02-04 14:58:35 +01:00
|
|
|
UseDefaultTransport bool
|
2021-08-19 11:29:33 +02:00
|
|
|
TrustedCerts []string
|
2020-05-25 19:48:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// TransportWrapper is a wrapper for central logging capabilities
|
|
|
|
type TransportWrapper struct {
|
|
|
|
Transport http.RoundTripper
|
|
|
|
doLogRequestBodyOnDebug bool
|
|
|
|
doLogResponseBodyOnDebug bool
|
|
|
|
}
|
|
|
|
|
|
|
|
// UploadRequestData encapsulates the parameters for calling uploader.Upload()
|
|
|
|
type UploadRequestData struct {
|
|
|
|
// Method is the HTTP method used for the request. Must be one of http.MethodPost or http.MethodPut.
|
|
|
|
Method string
|
|
|
|
// URL for the request
|
|
|
|
URL string
|
|
|
|
// File path to be stored in the created form field.
|
|
|
|
File string
|
|
|
|
// Form field name under which the file name will be stored.
|
|
|
|
FileFieldName string
|
|
|
|
// Additional form fields which will be added to the request if not nil.
|
|
|
|
FormFields map[string]string
|
|
|
|
// Reader from which the file contents will be read.
|
|
|
|
FileContent io.Reader
|
|
|
|
Header http.Header
|
|
|
|
Cookies []*http.Cookie
|
2021-10-21 10:03:42 +02:00
|
|
|
UploadType string
|
2019-12-09 17:35:31 +01:00
|
|
|
}
|
|
|
|
|
2020-01-14 10:29:50 +01:00
|
|
|
// Sender provides an interface to the piper http client for uid/pwd and token authenticated requests
|
2019-12-09 17:35:31 +01:00
|
|
|
type Sender interface {
|
|
|
|
SendRequest(method, url string, body io.Reader, header http.Header, cookies []*http.Cookie) (*http.Response, error)
|
|
|
|
SetOptions(options ClientOptions)
|
|
|
|
}
|
|
|
|
|
2020-01-14 10:29:50 +01:00
|
|
|
// Uploader provides an interface to the piper http client for uid/pwd and token authenticated requests with upload capabilities
|
|
|
|
type Uploader interface {
|
2020-03-23 15:02:22 +01:00
|
|
|
Sender
|
2021-10-21 10:03:42 +02:00
|
|
|
UploadRequest(method, url, file, fieldName string, header http.Header, cookies []*http.Cookie, uploadType string) (*http.Response, error)
|
|
|
|
UploadFile(url, file, fieldName string, header http.Header, cookies []*http.Cookie, uploadType string) (*http.Response, error)
|
2020-05-25 19:48:59 +02:00
|
|
|
Upload(data UploadRequestData) (*http.Response, error)
|
2020-01-14 10:29:50 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// UploadFile uploads a file's content as multipart-form POST request to the specified URL
|
2021-10-21 10:03:42 +02:00
|
|
|
func (c *Client) UploadFile(url, file, fileFieldName string, header http.Header, cookies []*http.Cookie, uploadType string) (*http.Response, error) {
|
|
|
|
return c.UploadRequest(http.MethodPost, url, file, fileFieldName, header, cookies, uploadType)
|
2020-01-22 14:22:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// UploadRequest uploads a file's content as multipart-form with given http method request to the specified URL
|
2021-10-21 10:03:42 +02:00
|
|
|
func (c *Client) UploadRequest(method, url, file, fileFieldName string, header http.Header, cookies []*http.Cookie, uploadType string) (*http.Response, error) {
|
2020-05-25 19:48:59 +02:00
|
|
|
fileHandle, err := os.Open(file)
|
|
|
|
if err != nil {
|
|
|
|
return &http.Response{}, errors.Wrapf(err, "unable to locate file %v", file)
|
|
|
|
}
|
|
|
|
defer fileHandle.Close()
|
2021-10-21 10:03:42 +02:00
|
|
|
|
2020-05-25 19:48:59 +02:00
|
|
|
return c.Upload(UploadRequestData{
|
|
|
|
Method: method,
|
|
|
|
URL: url,
|
|
|
|
File: file,
|
|
|
|
FileFieldName: fileFieldName,
|
|
|
|
FileContent: fileHandle,
|
|
|
|
Header: header,
|
|
|
|
Cookies: cookies,
|
2021-10-21 10:03:42 +02:00
|
|
|
UploadType: uploadType,
|
2020-05-25 19:48:59 +02:00
|
|
|
})
|
|
|
|
}
|
2020-01-22 15:10:40 +01:00
|
|
|
|
2021-10-21 10:03:42 +02:00
|
|
|
// Upload uploads a file's content as multipart-form or pure binary with given http method request to the specified URL
|
2020-05-25 19:48:59 +02:00
|
|
|
func (c *Client) Upload(data UploadRequestData) (*http.Response, error) {
|
|
|
|
if data.Method != http.MethodPost && data.Method != http.MethodPut {
|
|
|
|
return nil, errors.New(fmt.Sprintf("Http method %v is not allowed. Possible values are %v or %v", data.Method, http.MethodPost, http.MethodPut))
|
2020-01-22 15:10:40 +01:00
|
|
|
}
|
|
|
|
|
2021-10-21 10:03:42 +02:00
|
|
|
// Binary upload :: other options ("binary" or "form").
|
|
|
|
if data.UploadType == "binary" {
|
|
|
|
request, err := c.createRequest(data.Method, data.URL, data.FileContent, &data.Header, data.Cookies)
|
|
|
|
if err != nil {
|
|
|
|
c.logger.Debugf("New %v request to %v (binary upload)", data.Method, data.URL)
|
|
|
|
return &http.Response{}, errors.Wrapf(err, "error creating %v request to %v (binary upload)", data.Method, data.URL)
|
|
|
|
}
|
|
|
|
request.Header.Add("Content-Type", "application/octet-stream")
|
|
|
|
request.Header.Add("Connection", "Keep-Alive")
|
|
|
|
|
|
|
|
return c.Send(request)
|
|
|
|
|
|
|
|
} else { // For form upload
|
|
|
|
|
|
|
|
bodyBuffer := &bytes.Buffer{}
|
|
|
|
bodyWriter := multipart.NewWriter(bodyBuffer)
|
2020-01-14 10:29:50 +01:00
|
|
|
|
2021-10-21 10:03:42 +02:00
|
|
|
if data.FormFields != nil {
|
|
|
|
for fieldName, fieldValue := range data.FormFields {
|
|
|
|
err := bodyWriter.WriteField(fieldName, fieldValue)
|
|
|
|
if err != nil {
|
|
|
|
return &http.Response{}, errors.Wrapf(err, "error writing form field %v with value %v", fieldName, fieldValue)
|
|
|
|
}
|
2020-05-25 19:48:59 +02:00
|
|
|
}
|
|
|
|
}
|
2020-01-14 10:29:50 +01:00
|
|
|
|
2021-10-21 10:03:42 +02:00
|
|
|
fileWriter, err := bodyWriter.CreateFormFile(data.FileFieldName, data.File)
|
|
|
|
if err != nil {
|
|
|
|
return &http.Response{}, errors.Wrapf(err, "error creating form file %v for field %v", data.File, data.FileFieldName)
|
|
|
|
}
|
2020-01-14 10:29:50 +01:00
|
|
|
|
2021-10-21 10:03:42 +02:00
|
|
|
_, err = piperutils.CopyData(fileWriter, data.FileContent)
|
|
|
|
if err != nil {
|
|
|
|
return &http.Response{}, errors.Wrapf(err, "unable to copy file content of %v into request body", data.File)
|
|
|
|
}
|
|
|
|
err = bodyWriter.Close()
|
2020-01-14 10:29:50 +01:00
|
|
|
|
2021-10-21 10:03:42 +02:00
|
|
|
request, err := c.createRequest(data.Method, data.URL, bodyBuffer, &data.Header, data.Cookies)
|
|
|
|
if err != nil {
|
|
|
|
c.logger.Debugf("New %v request to %v", data.Method, data.URL)
|
|
|
|
return &http.Response{}, errors.Wrapf(err, "error creating %v request to %v", data.Method, data.URL)
|
|
|
|
}
|
2020-01-14 10:29:50 +01:00
|
|
|
|
2021-10-21 10:03:42 +02:00
|
|
|
startBoundary := strings.Index(bodyWriter.FormDataContentType(), "=") + 1
|
|
|
|
boundary := bodyWriter.FormDataContentType()[startBoundary:]
|
|
|
|
request.Header.Add("Content-Type", "multipart/form-data; boundary=\""+boundary+"\"")
|
|
|
|
request.Header.Add("Connection", "Keep-Alive")
|
2020-01-14 10:29:50 +01:00
|
|
|
|
2021-10-21 10:03:42 +02:00
|
|
|
return c.Send(request)
|
|
|
|
}
|
2020-01-14 10:29:50 +01:00
|
|
|
}
|
|
|
|
|
2019-12-09 17:35:31 +01:00
|
|
|
// SendRequest sends an http request with a defined method
|
2021-01-12 15:26:45 +01:00
|
|
|
//
|
|
|
|
// On error, any Response can be ignored and the Response.Body
|
|
|
|
// does not need to be closed.
|
2019-12-09 17:35:31 +01:00
|
|
|
func (c *Client) SendRequest(method, url string, body io.Reader, header http.Header, cookies []*http.Cookie) (*http.Response, error) {
|
2020-01-14 10:29:50 +01:00
|
|
|
request, err := c.createRequest(method, url, body, &header, cookies)
|
|
|
|
if err != nil {
|
|
|
|
return &http.Response{}, errors.Wrapf(err, "error creating %v request to %v", method, url)
|
|
|
|
}
|
2019-12-09 17:35:31 +01:00
|
|
|
|
2021-02-08 14:26:15 +01:00
|
|
|
return c.Send(request)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Send sends an http request
|
|
|
|
func (c *Client) Send(request *http.Request) (*http.Response, error) {
|
|
|
|
httpClient := c.initialize()
|
2020-01-14 10:29:50 +01:00
|
|
|
response, err := httpClient.Do(request)
|
|
|
|
if err != nil {
|
2021-02-08 14:26:15 +01:00
|
|
|
return response, errors.Wrapf(err, "HTTP %v request to %v failed", request.Method, request.URL)
|
2020-01-14 10:29:50 +01:00
|
|
|
}
|
2021-02-08 14:26:15 +01:00
|
|
|
return c.handleResponse(response, request.URL.String())
|
2020-01-14 10:29:50 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// SetOptions sets options used for the http client
|
|
|
|
func (c *Client) SetOptions(options ClientOptions) {
|
2020-05-25 19:48:59 +02:00
|
|
|
c.doLogRequestBodyOnDebug = options.DoLogRequestBodyOnDebug
|
|
|
|
c.doLogResponseBodyOnDebug = options.DoLogResponseBodyOnDebug
|
2021-02-04 14:58:35 +01:00
|
|
|
c.useDefaultTransport = options.UseDefaultTransport
|
2020-03-23 15:02:22 +01:00
|
|
|
c.transportTimeout = options.TransportTimeout
|
2020-11-09 11:47:03 +01:00
|
|
|
c.transportSkipVerification = options.TransportSkipVerification
|
2020-03-23 15:02:22 +01:00
|
|
|
c.maxRequestDuration = options.MaxRequestDuration
|
2020-01-14 10:29:50 +01:00
|
|
|
c.username = options.Username
|
|
|
|
c.password = options.Password
|
|
|
|
c.token = options.Token
|
2021-06-15 11:13:24 +02:00
|
|
|
if options.MaxRetries < 0 {
|
|
|
|
c.maxRetries = 0
|
|
|
|
} else if options.MaxRetries == 0 {
|
|
|
|
c.maxRetries = 15
|
|
|
|
} else {
|
|
|
|
c.maxRetries = options.MaxRetries
|
|
|
|
}
|
2020-02-06 16:16:34 +01:00
|
|
|
|
|
|
|
if options.Logger != nil {
|
|
|
|
c.logger = options.Logger
|
|
|
|
} else {
|
|
|
|
c.logger = log.Entry().WithField("package", "SAP/jenkins-library/pkg/http")
|
|
|
|
}
|
2020-02-04 11:43:27 +01:00
|
|
|
c.cookieJar = options.CookieJar
|
2021-08-19 11:29:33 +02:00
|
|
|
c.trustedCerts = options.TrustedCerts
|
2020-01-14 10:29:50 +01:00
|
|
|
}
|
|
|
|
|
2021-05-28 12:13:19 +02:00
|
|
|
// StandardClient returns a stdlib *http.Client which respects the custom settings.
|
|
|
|
func (c *Client) StandardClient() *http.Client {
|
|
|
|
return c.initialize()
|
|
|
|
}
|
|
|
|
|
2020-01-14 10:29:50 +01:00
|
|
|
func (c *Client) initialize() *http.Client {
|
2019-12-09 17:35:31 +01:00
|
|
|
c.applyDefaults()
|
2020-01-29 13:17:54 +01:00
|
|
|
c.logger = log.Entry().WithField("package", "SAP/jenkins-library/pkg/http")
|
2019-12-09 17:35:31 +01:00
|
|
|
|
2020-05-25 19:48:59 +02:00
|
|
|
var transport = &TransportWrapper{
|
|
|
|
Transport: &http.Transport{
|
|
|
|
DialContext: (&net.Dialer{
|
|
|
|
Timeout: c.transportTimeout,
|
|
|
|
}).DialContext,
|
|
|
|
ResponseHeaderTimeout: c.transportTimeout,
|
|
|
|
ExpectContinueTimeout: c.transportTimeout,
|
|
|
|
TLSHandshakeTimeout: c.transportTimeout,
|
2020-11-09 11:47:03 +01:00
|
|
|
TLSClientConfig: &tls.Config{
|
|
|
|
InsecureSkipVerify: c.transportSkipVerification,
|
|
|
|
},
|
2020-05-25 19:48:59 +02:00
|
|
|
},
|
|
|
|
doLogRequestBodyOnDebug: c.doLogRequestBodyOnDebug,
|
|
|
|
doLogResponseBodyOnDebug: c.doLogResponseBodyOnDebug,
|
2020-03-23 15:02:22 +01:00
|
|
|
}
|
2020-11-11 13:35:53 +01:00
|
|
|
|
2021-08-19 11:29:33 +02:00
|
|
|
if (len(c.trustedCerts)) > 0 && !c.useDefaultTransport && !c.transportSkipVerification {
|
|
|
|
log.Entry().Info("adding certs for tls to trust")
|
|
|
|
err := c.configureTLSToTrustCertificates(transport)
|
|
|
|
if err != nil {
|
|
|
|
log.Entry().Infof("adding certs for tls config failed : v%, continuing with the existing tsl config", err)
|
|
|
|
}
|
|
|
|
} else {
|
2021-09-23 10:05:44 +02:00
|
|
|
log.Entry().Debug("no trusted certs found / using default transport / insecure skip set to true / : continuing with existing tls config")
|
2021-08-19 11:29:33 +02:00
|
|
|
}
|
|
|
|
|
2020-11-11 13:35:53 +01:00
|
|
|
var httpClient *http.Client
|
|
|
|
if c.maxRetries > 0 {
|
|
|
|
retryClient := retryablehttp.NewClient()
|
2021-06-15 11:13:24 +02:00
|
|
|
localLogger := log.Entry()
|
|
|
|
localLogger.Level = logrus.DebugLevel
|
|
|
|
retryClient.Logger = localLogger
|
2020-11-11 13:35:53 +01:00
|
|
|
retryClient.HTTPClient.Timeout = c.maxRequestDuration
|
|
|
|
retryClient.HTTPClient.Jar = c.cookieJar
|
|
|
|
retryClient.RetryMax = c.maxRetries
|
2021-02-04 14:58:35 +01:00
|
|
|
if !c.useDefaultTransport {
|
|
|
|
retryClient.HTTPClient.Transport = transport
|
|
|
|
}
|
2021-03-09 13:41:07 +01:00
|
|
|
retryClient.CheckRetry = func(ctx context.Context, resp *http.Response, err error) (bool, error) {
|
2021-06-15 11:13:24 +02:00
|
|
|
if err != nil && (strings.Contains(err.Error(), "timeout") || strings.Contains(err.Error(), "timed out") || strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "connection reset")) {
|
|
|
|
// Assuming timeouts, resets, and similar could be retried
|
2021-03-09 13:41:07 +01:00
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
return retryablehttp.DefaultRetryPolicy(ctx, resp, err)
|
|
|
|
}
|
2020-11-11 13:35:53 +01:00
|
|
|
httpClient = retryClient.StandardClient()
|
|
|
|
} else {
|
|
|
|
httpClient = &http.Client{}
|
|
|
|
httpClient.Timeout = c.maxRequestDuration
|
|
|
|
httpClient.Jar = c.cookieJar
|
2021-02-04 14:58:35 +01:00
|
|
|
if !c.useDefaultTransport {
|
|
|
|
httpClient.Transport = transport
|
|
|
|
}
|
2019-12-09 17:35:31 +01:00
|
|
|
}
|
2020-11-11 13:35:53 +01:00
|
|
|
|
2020-11-09 11:47:03 +01:00
|
|
|
if c.transportSkipVerification {
|
|
|
|
c.logger.Debugf("TLS verification disabled")
|
|
|
|
}
|
2020-11-11 13:35:53 +01:00
|
|
|
|
2020-03-23 15:02:22 +01:00
|
|
|
c.logger.Debugf("Transport timeout: %v, max request duration: %v", c.transportTimeout, c.maxRequestDuration)
|
2020-01-14 10:29:50 +01:00
|
|
|
|
|
|
|
return httpClient
|
|
|
|
}
|
2019-12-09 17:35:31 +01:00
|
|
|
|
2020-05-25 19:48:59 +02:00
|
|
|
type contextKey struct {
|
|
|
|
name string
|
|
|
|
}
|
|
|
|
|
|
|
|
var contextKeyRequestStart = &contextKey{"RequestStart"}
|
|
|
|
|
|
|
|
// RoundTrip is the core part of this module and implements http.RoundTripper.
|
|
|
|
// Executes HTTP request with request/response logging.
|
|
|
|
func (t *TransportWrapper) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
|
|
ctx := context.WithValue(req.Context(), contextKeyRequestStart, time.Now())
|
|
|
|
req = req.WithContext(ctx)
|
|
|
|
|
|
|
|
t.logRequest(req)
|
|
|
|
resp, err := t.Transport.RoundTrip(req)
|
|
|
|
t.logResponse(resp)
|
|
|
|
|
|
|
|
return resp, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *TransportWrapper) logRequest(req *http.Request) {
|
|
|
|
log.Entry().Debug("--------------------------------")
|
|
|
|
log.Entry().Debugf("--> %v request to %v", req.Method, req.URL)
|
2021-01-04 10:06:28 +01:00
|
|
|
log.Entry().Debugf("headers: %v", transformHeaders(req.Header))
|
2020-05-25 19:48:59 +02:00
|
|
|
log.Entry().Debugf("cookies: %v", transformCookies(req.Cookies()))
|
|
|
|
if t.doLogRequestBodyOnDebug {
|
|
|
|
log.Entry().Debugf("body: %v", transformBody(req.Body))
|
|
|
|
}
|
|
|
|
log.Entry().Debug("--------------------------------")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *TransportWrapper) logResponse(resp *http.Response) {
|
|
|
|
if resp != nil {
|
|
|
|
ctx := resp.Request.Context()
|
|
|
|
if start, ok := ctx.Value(contextKeyRequestStart).(time.Time); ok {
|
|
|
|
log.Entry().Debugf("<-- response %v %v (%v)", resp.StatusCode, resp.Request.URL, roundtime.Duration(time.Now().Sub(start), 2))
|
|
|
|
} else {
|
|
|
|
log.Entry().Debugf("<-- response %v %v", resp.StatusCode, resp.Request.URL)
|
|
|
|
}
|
|
|
|
if t.doLogResponseBodyOnDebug {
|
|
|
|
log.Entry().Debugf("body: %v", transformBody(resp.Body))
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
log.Entry().Debug("response <nil>")
|
|
|
|
}
|
|
|
|
log.Entry().Debug("--------------------------------")
|
|
|
|
}
|
|
|
|
|
2021-01-04 10:06:28 +01:00
|
|
|
func transformHeaders(header http.Header) http.Header {
|
|
|
|
var h http.Header = map[string][]string{}
|
|
|
|
for name, value := range header {
|
|
|
|
if name == "Authorization" {
|
|
|
|
for _, v := range value {
|
|
|
|
// The format of the Authorization header value is: <type> <cred>.
|
|
|
|
// We don't register the full string since only the part after
|
|
|
|
// the first token is the secret in the narrower sense (applies at
|
|
|
|
// least for basic auth)
|
|
|
|
log.RegisterSecret(strings.Join(strings.Split(v, " ")[1:], " "))
|
|
|
|
}
|
|
|
|
// Since
|
|
|
|
// 1.) The auth header type itself might serve as a vector for an
|
|
|
|
// intrusion
|
|
|
|
// 2.) We cannot make assumtions about the structure of the auth
|
|
|
|
// header value since that depends on the type, e.g. several tokens
|
|
|
|
// where only some of the tokens define the secret
|
|
|
|
// we hide the full auth header value anyway in order to be on the
|
|
|
|
// save side.
|
|
|
|
value = []string{"<set>"}
|
|
|
|
}
|
|
|
|
h[name] = value
|
|
|
|
}
|
|
|
|
return h
|
|
|
|
}
|
|
|
|
|
2020-05-25 19:48:59 +02:00
|
|
|
func transformCookies(cookies []*http.Cookie) string {
|
|
|
|
result := ""
|
|
|
|
for _, c := range cookies {
|
|
|
|
result = fmt.Sprintf("%v %v", result, c.String())
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
func transformBody(body io.ReadCloser) string {
|
|
|
|
if body == nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
buf.ReadFrom(body)
|
|
|
|
return buf.String()
|
|
|
|
}
|
|
|
|
|
2020-01-14 10:29:50 +01:00
|
|
|
func (c *Client) createRequest(method, url string, body io.Reader, header *http.Header, cookies []*http.Cookie) (*http.Request, error) {
|
2019-12-09 17:35:31 +01:00
|
|
|
request, err := http.NewRequest(method, url, body)
|
|
|
|
if err != nil {
|
2020-01-14 10:29:50 +01:00
|
|
|
return &http.Request{}, err
|
2019-12-09 17:35:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if header != nil {
|
2020-01-14 10:29:50 +01:00
|
|
|
for name, headers := range *header {
|
2019-12-09 17:35:31 +01:00
|
|
|
for _, h := range headers {
|
|
|
|
request.Header.Add(name, h)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if cookies != nil {
|
|
|
|
for _, cookie := range cookies {
|
|
|
|
request.AddCookie(cookie)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
Changes for Pipeline Reporting (#3213)
* Adds GetLog() function to orchestrator
* Fixes BUILD_NUMBER env variable
* Fixes correct env var for JENKINS_HOME
* Adds getEnv to read env variables with default value, adds test for jenkins GetLog() implementation
* Adds possibility to read errorJsons; updates splunk package for log files (WIP)
* Uncommenting dev code
* Adds GetLog() function to orchestrator
* Fixes BUILD_NUMBER env variable
* Fixes correct env var for JENKINS_HOME
* Adds getEnv to read env variables with default value, adds test for jenkins GetLog() implementation
* Adds possibility to read errorJsons; updates splunk package for log files (WIP)
* Uncommenting dev code
* Adds GetRequest function which holds the response in memory (not saved to disk)
* Implements GetLog() function for ADO, adds function to read PipelineRuntime
* PAT has been revoked
* Changes http package, s.t. if password only is required basic auth works too
* Adds env variable for azure token, error handling in case of unauthenticated/nil response
* Adds logging output in case env variable can not be read and fallback variable needs to be used
* Adds usage of environment variables for auth, uses jenkins api
* Adds init functionality for orchestrators, updates GetLog() and GetPipelineStartTime() function
* Adds initaliziation function for orchestrator authetnication
* Adds settings struct for orchestrator authentication
* Adds function to whole logfile to Splunk
* Struct for pipeline related telemetry information
* Increase messagebatch size to 10k
* Changes splunk package to a pointer based implementation, updates generated files and corresponding template and tests for splunk
* Changes telemetry package to pointer based implementation to have multiple telemetry objects, adjusted tests and splunk implementation
* Changes content type to txt
* Send telemetry independent of logfiles, increases amount of messages per file
* Adds JobURL for orchestrators and UnknownOrchestrator as fallback
* telemetry makes use of orchestrator specific information
* Adds orchestrator independent correlationID
* Adds custom fields for pipeline status
* go fmt
* Removes env var test - no env variables are read anymore
* Use UnknownOrchestratorConfigProvider in case the orchestrator can not be initalized
* Removes Custom fields from telemetry as these can not be reflected in SWA
* Adds custom telemetry information (piperHash,..) to each step telemetry information
* Removes falltrough in case no orchestrator has been found
* Updates tests for orchestrator package
* Adds orchestrator import in generated files
* Updates generator files for internal library
* Adds orchestrator telemetry information to steps
* Updates generated files, fatalHook writes to cpe
* Go generate from master, go fmt
* Adds Custom Data field LastErrorCode
* Removes GetLog() test
* Update init_unix.go
* Update docker_integration_test_executor.go
* Update integration_api_cli_test.go
* Reverts go1.17 fmt formatting
* Reverts go1.17 fmt formatting
* Reverts go1.17 fmt formatting
* Renames customTelemetryData to stepTelemetryData
* Adjustments to orchestrator-package, cleanup, adds JobName
* Adjusts commonPipelineEnvironment path
* Adds pipelineTelemetry struct to telemetry package, removes pipeline telemetry structs from splunk package
* Go fmt
* Changes path for errorDetails, adds debug information
* Removes custom fields from step, adds orchestrator, commithash to baseMetadata
* Adjusts tests for telemetry package
* Adds tests for orchestrator
* Updates generated files, initalization of splunk client only if its available in the config
* Fixes typo in helper go
* Update pkg/http/downloader.go
* Update pkg/http/downloader.go
* Update pkg/log/fatalHook.go
* Update fatalHook.go
* Update pkg/splunk/splunk.go
* Update pkg/telemetry/data.go
* Adds GetBuildStatus() and GetAPIInformation() to orchestrators
* error formatting
* Bugfix: dont send telemetry data if disabled, adjusts test
* go fmt
* Use correct error handling
* Update pkg/telemetry/telemetry.go
* Fixes telemetry disabled in the tests
* Fixes http tests
* Log fatal errors to logFile
* Adds CustomReportingConfig to hooks
* Cleanup comments in splunk package
* Adds possibility to send telemetry to custom endpoint
* Adds debug output for the payload
* Debug output for the payload as a string
* Adds test cases for changes in telemetry package
* go fmt
* Adds generated files for new step
* Reverts changes for http tests, causing problems with go1.15, changes need to be applied for newer go version >=1.17
* Adjusts test for sonarExecuteScan
* Adjusts test for sonarExecuteScan
* Adds explanation for customreportingConfig
* Makes disableing of customSend more obvious
* Adds custom step reporting to each step, updates generated files, adjusts helper testdata
* fixes unit test wrong usage of logging
* Send pipeline data altough there has been no error, adjust test cases
* Reverts changes for customReporting
* Updates generated files, removes customReporting
* Removes writing errorDetails to CPE
* Reverts usage of customreporting
* go fmt
* reverts changes in http_test
* reverts changes in http_test
* Skips integration cnb test
Co-authored-by: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com>
2021-11-18 17:50:03 +01:00
|
|
|
if len(c.username) > 0 || len(c.password) > 0 {
|
2019-12-09 17:35:31 +01:00
|
|
|
request.SetBasicAuth(c.username, c.password)
|
2020-01-14 10:29:50 +01:00
|
|
|
c.logger.Debug("Using Basic Authentication ****/****")
|
2019-12-09 17:35:31 +01:00
|
|
|
}
|
|
|
|
|
2020-01-14 10:29:50 +01:00
|
|
|
if len(c.token) > 0 {
|
|
|
|
request.Header.Add("Authorization", c.token)
|
2019-12-09 17:35:31 +01:00
|
|
|
}
|
|
|
|
|
2020-01-14 10:29:50 +01:00
|
|
|
return request, nil
|
|
|
|
}
|
|
|
|
|
2020-12-16 08:55:22 +01:00
|
|
|
func (c *Client) handleResponse(response *http.Response, url string) (*http.Response, error) {
|
2019-12-09 17:35:31 +01:00
|
|
|
// 2xx codes do not create an error
|
|
|
|
if response.StatusCode >= 200 && response.StatusCode < 300 {
|
|
|
|
return response, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
switch response.StatusCode {
|
|
|
|
case http.StatusUnauthorized:
|
2020-01-14 10:29:50 +01:00
|
|
|
c.logger.WithField("HTTP Error", "401 (Unauthorized)").Error("Credentials invalid, please check your user credentials!")
|
2019-12-09 17:35:31 +01:00
|
|
|
case http.StatusForbidden:
|
2020-01-14 10:29:50 +01:00
|
|
|
c.logger.WithField("HTTP Error", "403 (Forbidden)").Error("Permission issue, please check your user permissions!")
|
2019-12-09 17:35:31 +01:00
|
|
|
case http.StatusNotFound:
|
2020-12-16 08:55:22 +01:00
|
|
|
c.logger.WithField("HTTP Error", "404 (Not Found)").Errorf("Requested resource ('%s') could not be found", url)
|
2019-12-09 17:35:31 +01:00
|
|
|
case http.StatusInternalServerError:
|
2020-05-25 19:48:59 +02:00
|
|
|
c.logger.WithField("HTTP Error", "500 (Internal Server Error)").Error("Unknown error occurred.")
|
2019-12-09 17:35:31 +01:00
|
|
|
}
|
|
|
|
|
2021-08-19 11:29:33 +02:00
|
|
|
return response, fmt.Errorf("request to %v returned with response %v", response.Request.URL, response.Status)
|
2019-12-09 17:35:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) applyDefaults() {
|
2020-03-23 15:02:22 +01:00
|
|
|
if c.transportTimeout == 0 {
|
2020-06-10 11:14:55 +02:00
|
|
|
c.transportTimeout = 3 * time.Minute
|
2019-12-09 17:35:31 +01:00
|
|
|
}
|
2020-01-27 23:40:53 +01:00
|
|
|
if c.logger == nil {
|
|
|
|
c.logger = log.Entry().WithField("package", "SAP/jenkins-library/pkg/http")
|
|
|
|
}
|
2019-12-09 17:35:31 +01:00
|
|
|
}
|
2020-07-14 10:58:57 +02:00
|
|
|
|
2021-08-19 11:29:33 +02:00
|
|
|
func (c *Client) configureTLSToTrustCertificates(transport *TransportWrapper) error {
|
|
|
|
|
|
|
|
trustStoreDir, err := getWorkingDirForTrustStore()
|
|
|
|
fileUtils := &piperutils.Files{}
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "failed to create trust store directory")
|
|
|
|
}
|
|
|
|
/* insecure := flag.Bool("insecure-ssl", false, "Accept/Ignore all server SSL certificates") */
|
|
|
|
|
|
|
|
for _, certificate := range c.trustedCerts {
|
|
|
|
rootCAs, _ := x509.SystemCertPool()
|
|
|
|
|
|
|
|
if rootCAs == nil {
|
|
|
|
rootCAs = x509.NewCertPool()
|
|
|
|
}
|
|
|
|
|
|
|
|
filename := path.Base(certificate)
|
|
|
|
filename = strings.ReplaceAll(filename, " ", "")
|
|
|
|
target := filepath.Join(trustStoreDir, filename)
|
|
|
|
if exists, _ := fileUtils.FileExists(target); !exists {
|
|
|
|
log.Entry().WithField("source", certificate).WithField("target", target).Info("Downloading TLS certificate")
|
|
|
|
request, err := http.NewRequest("GET", certificate, nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
httpClient := &http.Client{}
|
|
|
|
httpClient.Timeout = c.maxRequestDuration
|
|
|
|
httpClient.Jar = c.cookieJar
|
|
|
|
if !c.useDefaultTransport {
|
|
|
|
httpClient.Transport = transport
|
|
|
|
}
|
|
|
|
response, err := httpClient.Do(request)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "HTTP %v request to %v failed", request.Method, request.URL)
|
|
|
|
}
|
|
|
|
|
|
|
|
if response.StatusCode >= 200 && response.StatusCode < 300 {
|
|
|
|
defer response.Body.Close()
|
|
|
|
parent := filepath.Dir(target)
|
|
|
|
if len(parent) > 0 {
|
|
|
|
if err = os.MkdirAll(parent, 0775); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fileHandler, err := os.Create(target)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "unable to create file %v", filename)
|
|
|
|
}
|
|
|
|
defer fileHandler.Close()
|
|
|
|
|
|
|
|
_, err = io.Copy(fileHandler, response.Body)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "unable to copy content from url to file %v", filename)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the SystemCertPool, continue with an empty pool on error
|
|
|
|
certs, err := ioutil.ReadFile(target)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "Failed to read cert file %v", certificate)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Append our cert to the system pool
|
|
|
|
if ok := rootCAs.AppendCertsFromPEM(certs); !ok {
|
|
|
|
log.Entry().Infof("cert not appended to root ca %v", certificate)
|
|
|
|
return fmt.Errorf("cert not appended to root ca %v", certificate)
|
|
|
|
}
|
|
|
|
|
|
|
|
*transport = TransportWrapper{
|
|
|
|
Transport: &http.Transport{
|
|
|
|
DialContext: (&net.Dialer{
|
|
|
|
Timeout: c.transportTimeout,
|
|
|
|
}).DialContext,
|
|
|
|
ResponseHeaderTimeout: c.transportTimeout,
|
|
|
|
ExpectContinueTimeout: c.transportTimeout,
|
|
|
|
TLSHandshakeTimeout: c.transportTimeout,
|
|
|
|
TLSClientConfig: &tls.Config{
|
|
|
|
InsecureSkipVerify: false,
|
|
|
|
RootCAs: rootCAs,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
doLogRequestBodyOnDebug: c.doLogRequestBodyOnDebug,
|
|
|
|
doLogResponseBodyOnDebug: c.doLogResponseBodyOnDebug,
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Entry().Infof("%v appended to root CA successfully", certificate)
|
|
|
|
|
|
|
|
} else {
|
|
|
|
return errors.Wrapf(err, "Download of TLS certificate %v failed with status code %v", certificate, response.StatusCode)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
log.Entry().Infof("existing certs found, appending to rootCA")
|
|
|
|
certs, err := ioutil.ReadFile(target)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "Failed to read cert file %v", certificate)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Append our cert to the system pool
|
|
|
|
if ok := rootCAs.AppendCertsFromPEM(certs); !ok {
|
|
|
|
log.Entry().Infof("cert not appended to root ca %v", certificate)
|
|
|
|
}
|
|
|
|
|
|
|
|
*transport = TransportWrapper{
|
|
|
|
Transport: &http.Transport{
|
|
|
|
DialContext: (&net.Dialer{
|
|
|
|
Timeout: c.transportTimeout,
|
|
|
|
}).DialContext,
|
|
|
|
ResponseHeaderTimeout: c.transportTimeout,
|
|
|
|
ExpectContinueTimeout: c.transportTimeout,
|
|
|
|
TLSHandshakeTimeout: c.transportTimeout,
|
|
|
|
TLSClientConfig: &tls.Config{
|
|
|
|
InsecureSkipVerify: false,
|
|
|
|
RootCAs: rootCAs,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
doLogRequestBodyOnDebug: c.doLogRequestBodyOnDebug,
|
|
|
|
doLogResponseBodyOnDebug: c.doLogResponseBodyOnDebug,
|
|
|
|
}
|
|
|
|
log.Entry().Infof("%v appended to root CA successfully", certificate)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func getWorkingDirForTrustStore() (string, error) {
|
|
|
|
fileUtils := &piperutils.Files{}
|
|
|
|
if exists, _ := fileUtils.DirExists(reporting.StepReportDirectory); !exists {
|
|
|
|
err := fileUtils.MkdirAll(".pipeline/trustStore", 0777)
|
|
|
|
if err != nil {
|
|
|
|
return "", errors.Wrap(err, "failed to create trust store directory")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ".pipeline/trustStore", nil
|
|
|
|
}
|
|
|
|
|
2020-09-24 07:41:06 +02:00
|
|
|
// ParseHTTPResponseBodyXML parses a XML http response into a given interface
|
2020-07-14 10:58:57 +02:00
|
|
|
func ParseHTTPResponseBodyXML(resp *http.Response, response interface{}) error {
|
|
|
|
if resp == nil {
|
|
|
|
return errors.Errorf("cannot parse HTTP response with value <nil>")
|
|
|
|
}
|
|
|
|
|
|
|
|
bodyText, readErr := ioutil.ReadAll(resp.Body)
|
|
|
|
if readErr != nil {
|
|
|
|
return errors.Wrap(readErr, "HTTP response body could not be read")
|
|
|
|
}
|
|
|
|
|
|
|
|
marshalErr := xml.Unmarshal(bodyText, &response)
|
|
|
|
if marshalErr != nil {
|
|
|
|
return errors.Wrapf(marshalErr, "HTTP response body could not be parsed as XML: %v", string(bodyText))
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-09-24 07:41:06 +02:00
|
|
|
// ParseHTTPResponseBodyJSON parses a JSON http response into a given interface
|
2020-07-14 10:58:57 +02:00
|
|
|
func ParseHTTPResponseBodyJSON(resp *http.Response, response interface{}) error {
|
|
|
|
if resp == nil {
|
|
|
|
return errors.Errorf("cannot parse HTTP response with value <nil>")
|
|
|
|
}
|
|
|
|
|
|
|
|
bodyText, readErr := ioutil.ReadAll(resp.Body)
|
|
|
|
if readErr != nil {
|
|
|
|
return errors.Wrapf(readErr, "HTTP response body could not be read")
|
|
|
|
}
|
|
|
|
|
|
|
|
marshalErr := json.Unmarshal(bodyText, &response)
|
|
|
|
if marshalErr != nil {
|
|
|
|
return errors.Wrapf(marshalErr, "HTTP response body could not be parsed as JSON: %v", string(bodyText))
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|