github.com/Ingenico-ePayments/connect-sdk-go@v0.0.0-20240318153750-1f8cd329b9c9/defaultimpl/DefaultConnection.go (about)

     1  package defaultimpl
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/rand"
     6  	"crypto/tls"
     7  	"encoding/base64"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"mime/multipart"
    13  	"net"
    14  	"net/http"
    15  	"net/textproto"
    16  	"net/url"
    17  	"strings"
    18  	"time"
    19  
    20  	"github.com/Ingenico-ePayments/connect-sdk-go/communicator/communication"
    21  	sdkErrors "github.com/Ingenico-ePayments/connect-sdk-go/errors"
    22  	"github.com/Ingenico-ePayments/connect-sdk-go/logging"
    23  	"github.com/Ingenico-ePayments/connect-sdk-go/logging/obfuscation"
    24  )
    25  
    26  // DefaultConnection is the default implementation for the connection interface. Supports Pooling, and is thread safe.
    27  type DefaultConnection struct {
    28  	client              http.Client
    29  	underlyingTransport *http.Transport
    30  	logger              logging.CommunicatorLogger
    31  	proxyAuth           string
    32  	bodyObfuscator      obfuscation.BodyObfuscator
    33  	headerObfuscator    obfuscation.HeaderObfuscator
    34  }
    35  
    36  func (c *DefaultConnection) logRequest(id, body string, req *http.Request) error {
    37  	if c.logger == nil {
    38  		return nil
    39  	}
    40  
    41  	var url url.URL
    42  	if req.URL != nil {
    43  		url = *req.URL
    44  	}
    45  
    46  	reqMessage, err := logging.NewRequestLogMessageBuilderWithObfuscators(id, req.Method, url, c.bodyObfuscator, c.headerObfuscator)
    47  	if err != nil {
    48  		c.logError(id, err)
    49  		return err
    50  	}
    51  
    52  	for k, v := range req.Header {
    53  		for _, rv := range v {
    54  			reqMessage.AddHeader(k, rv) // #nosec G104
    55  		}
    56  	}
    57  
    58  	reqMessage.SetBody(body, req.Header.Get("Content-Type")) // #nosec G104
    59  
    60  	message, err := reqMessage.BuildMessage()
    61  	if err != nil {
    62  		c.logError(id, err)
    63  		return err
    64  	}
    65  
    66  	c.logger.LogRequestLogMessage(message)
    67  
    68  	return nil
    69  }
    70  
    71  func (c *DefaultConnection) logResponse(id string, reader io.Reader, binaryResponse bool, resp *http.Response, duration time.Duration) error {
    72  	if c.logger == nil {
    73  		return nil
    74  	}
    75  
    76  	respMessage, err := logging.NewResponseLogMessageBuilderWithObfuscators(id, resp.StatusCode, duration, c.bodyObfuscator, c.headerObfuscator)
    77  	if err != nil {
    78  		c.logError(id, err)
    79  		return err
    80  	}
    81  
    82  	for k, v := range resp.Header {
    83  		for _, rv := range v {
    84  			respMessage.AddHeader(k, rv) // #nosec G104
    85  		}
    86  	}
    87  
    88  	if binaryResponse {
    89  		respMessage.SetBinaryBody(resp.Header.Get("Content-Type")) // #nosec G104
    90  	} else {
    91  		bodyBuff, err := ioutil.ReadAll(reader)
    92  		if err != nil {
    93  			c.logError(id, err)
    94  			return err
    95  		}
    96  
    97  		respMessage.SetBody(string(bodyBuff), resp.Header.Get("Content-Type")) // #nosec G104
    98  	}
    99  
   100  	message, err := respMessage.BuildMessage()
   101  	if err != nil {
   102  		c.logError(id, err)
   103  		return err
   104  	}
   105  
   106  	c.logger.LogResponseLogMessage(message)
   107  
   108  	return nil
   109  }
   110  
   111  func (c *DefaultConnection) logError(id string, err error) {
   112  	if c.logger != nil {
   113  		c.logger.LogError(id, err)
   114  	}
   115  }
   116  
   117  // Close implements the io.Closer interface
   118  func (c *DefaultConnection) Close() error {
   119  	// No-op, because the http.Client connection's close automatically after the socket timeout passes
   120  	// and they can't be closed manually
   121  	return nil
   122  }
   123  
   124  // NewDefaultConnection creates a new object that implements Connection, and initializes it
   125  func NewDefaultConnection(socketTimeout, connectTimeout, keepAliveTimeout, idleTimeout time.Duration, maxConnections int, proxy *url.URL) (*DefaultConnection, error) {
   126  	dialer := net.Dialer{
   127  		Timeout:   connectTimeout,
   128  		KeepAlive: keepAliveTimeout,
   129  	}
   130  
   131  	transport := &http.Transport{
   132  		DialContext:     dialer.DialContext,
   133  		Proxy:           http.ProxyURL(proxy),
   134  		IdleConnTimeout: idleTimeout,
   135  		MaxIdleConns:    maxConnections,
   136  		TLSClientConfig: &tls.Config{
   137  			MinVersion: tls.VersionTLS12,
   138  		},
   139  	}
   140  
   141  	client := http.Client{
   142  		Transport: transport,
   143  		Timeout:   socketTimeout,
   144  	}
   145  
   146  	proxyAuth := getProxyAuth(proxy)
   147  
   148  	return &DefaultConnection{client, transport, nil, proxyAuth, obfuscation.DefaultBodyObfuscator(), obfuscation.DefaultHeaderObfuscator()}, nil
   149  }
   150  
   151  // Get sends a GET request to the Ingenico ePayments platform and calls the given response handler with the response.
   152  func (c *DefaultConnection) Get(uri url.URL, headerList []communication.Header, handler communication.ResponseHandler) (interface{}, error) {
   153  	return c.sendRequest("GET", uri, headerList, nil, "", handler)
   154  }
   155  
   156  // Delete sends a DELETE request to the Ingenico ePayments platform and calls the given response handler with the response.
   157  func (c *DefaultConnection) Delete(uri url.URL, headerList []communication.Header, handler communication.ResponseHandler) (interface{}, error) {
   158  	return c.sendRequest("DELETE", uri, headerList, nil, "", handler)
   159  }
   160  
   161  // Post sends a POST request to the Ingenico ePayments platform and calls the given response handler with the response.
   162  func (c *DefaultConnection) Post(uri url.URL, headerList []communication.Header, body string, handler communication.ResponseHandler) (interface{}, error) {
   163  	return c.sendRequest("POST", uri, headerList, strings.NewReader(body), body, handler)
   164  }
   165  
   166  // PostMultipart sends a multipart/form-data POST request to the Ingenico ePayments platform and calls the given response handler with the response.
   167  func (c *DefaultConnection) PostMultipart(uri url.URL, headerList []communication.Header, body *communication.MultipartFormDataObject, handler communication.ResponseHandler) (interface{}, error) {
   168  	r, err := c.createMultipartReader(body)
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  	defer r.Close() // #nosec G307
   173  	return c.sendRequest("POST", uri, headerList, r, "<binary content>", handler)
   174  }
   175  
   176  // Put sends a PUT request to the Ingenico ePayments platform and returns the response.
   177  func (c *DefaultConnection) Put(uri url.URL, headerList []communication.Header, body string, handler communication.ResponseHandler) (interface{}, error) {
   178  	return c.sendRequest("PUT", uri, headerList, strings.NewReader(body), body, handler)
   179  }
   180  
   181  // PutMultipart sends a multipart/form-data POST request to the Ingenico ePayments platform and calls the given response handler with the response.
   182  func (c *DefaultConnection) PutMultipart(uri url.URL, headerList []communication.Header, body *communication.MultipartFormDataObject, handler communication.ResponseHandler) (interface{}, error) {
   183  	r, err := c.createMultipartReader(body)
   184  	if err != nil {
   185  		return nil, err
   186  	}
   187  	defer r.Close() // #nosec G307
   188  	return c.sendRequest("PUT", uri, headerList, r, "<binary content>", handler)
   189  }
   190  
   191  func pseudoUUID() (string, error) {
   192  	b := make([]byte, 16)
   193  	_, err := rand.Read(b)
   194  	if err != nil {
   195  		return "", err
   196  	}
   197  	return fmt.Sprintf("%X-%X-%X-%X-%X", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]), nil
   198  }
   199  
   200  func getProxyAuth(proxy *url.URL) string {
   201  	if proxy == nil || proxy.User == nil {
   202  		return ""
   203  	}
   204  
   205  	return "Basic " + base64.StdEncoding.EncodeToString([]byte(proxy.User.String()))
   206  }
   207  
   208  func isBinaryContent(headers []communication.Header) bool {
   209  	header := communication.Headers(headers).GetHeader("Content-Type")
   210  	if header == nil {
   211  		return false
   212  	}
   213  
   214  	contentType := strings.ToLower(header.Value())
   215  
   216  	return !strings.HasPrefix(contentType, "text/") && !strings.Contains(contentType, "json") && !strings.Contains(contentType, "xml")
   217  }
   218  
   219  func (c *DefaultConnection) sendRequest(method string, uri url.URL, headerList []communication.Header, body io.Reader, bodyString string, handler communication.ResponseHandler) (interface{}, error) {
   220  	id, err := pseudoUUID()
   221  	if err != nil {
   222  		return nil, err
   223  	}
   224  
   225  	httpRequest, err := http.NewRequest(method, uri.String(), body)
   226  	if err != nil {
   227  		c.logError(id, err)
   228  		return nil, err
   229  	}
   230  
   231  	for _, h := range headerList {
   232  		httpRequest.Header[h.Name()] = append(httpRequest.Header[h.Name()], h.Value())
   233  	}
   234  	if len(c.proxyAuth) > 0 {
   235  		httpRequest.Header["Proxy-Authorization"] = append(httpRequest.Header["Proxy-Authorization"], c.proxyAuth)
   236  	}
   237  
   238  	start := time.Now()
   239  
   240  	c.logRequest(id, bodyString, httpRequest) // #nosec G104
   241  
   242  	resp, err := c.client.Do(httpRequest)
   243  	switch ce := err.(type) {
   244  	case *url.Error:
   245  		{
   246  			c.logError(id, ce)
   247  
   248  			newErr, _ := sdkErrors.NewCommunicationError(ce)
   249  			return nil, newErr
   250  		}
   251  	}
   252  	if err != nil {
   253  		c.logError(id, err)
   254  		return nil, err
   255  	}
   256  
   257  	end := time.Now()
   258  
   259  	defer func() {
   260  		err := resp.Body.Close()
   261  		if err != nil {
   262  			c.logError(id, err)
   263  		}
   264  	}()
   265  
   266  	respHeaders := []communication.Header{}
   267  	for name, values := range resp.Header {
   268  		if name == "X-Gcs-Idempotence-Request-Timestamp" {
   269  			name = "X-GCS-Idempotence-Request-Timestamp"
   270  		}
   271  
   272  		header, err := communication.NewHeader(name, values[0])
   273  		if err != nil {
   274  			c.logError(id, err)
   275  			return nil, err
   276  		}
   277  		respHeaders = append(respHeaders, *header)
   278  	}
   279  
   280  	bodyReader := resp.Body.(io.Reader)
   281  	if isBinaryContent(respHeaders) {
   282  		c.logResponse(id, nil, true, resp, end.Sub(start)) // #nosec G104
   283  	} else {
   284  		readBuffer := bytes.NewBuffer([]byte{})
   285  		teeReader := io.TeeReader(resp.Body, readBuffer)
   286  		bodyReader = teeReader
   287  		defer c.logResponse(id, io.MultiReader(readBuffer, teeReader), false, resp, end.Sub(start))
   288  	}
   289  
   290  	return handler.Handle(resp.StatusCode, respHeaders, bodyReader)
   291  }
   292  
   293  // CloseIdleConnections closes all HTTP connections that have been idle for the specified time. This should also include
   294  // all expired HTTP connections.
   295  // timespan represents the time spent idle
   296  // Note: in the current implementation, it is only possible to close the connection after a predetermined time
   297  // Therefore, this implementation ignores the parameter, and instead uses the preconfigured one
   298  func (c *DefaultConnection) CloseIdleConnections(t time.Duration) {
   299  	// Assume t is equal to configured value
   300  	c.underlyingTransport.CloseIdleConnections()
   301  }
   302  
   303  // CloseExpiredConnections closes all expired HTTP connections.
   304  func (c *DefaultConnection) CloseExpiredConnections() {
   305  	// No-op, because this is done automatically for this implementation
   306  }
   307  
   308  // SetBodyObfuscator sets the body obfuscator to use.
   309  func (c *DefaultConnection) SetBodyObfuscator(bodyObfuscator obfuscation.BodyObfuscator) {
   310  	c.bodyObfuscator = bodyObfuscator
   311  }
   312  
   313  // SetHeaderObfuscator sets the header obfuscator to use.
   314  func (c *DefaultConnection) SetHeaderObfuscator(headerObfuscator obfuscation.HeaderObfuscator) {
   315  	c.headerObfuscator = headerObfuscator
   316  }
   317  
   318  // EnableLogging implements the logging.Capable interface
   319  // Enables logging to the given CommunicatorLogger
   320  func (c *DefaultConnection) EnableLogging(l logging.CommunicatorLogger) {
   321  	c.logger = l
   322  }
   323  
   324  // DisableLogging implements the logging.Capable interface
   325  // Disables logging
   326  func (c *DefaultConnection) DisableLogging() {
   327  	c.logger = nil
   328  }
   329  
   330  var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
   331  
   332  func escapeQuotes(s string) string {
   333  	return quoteEscaper.Replace(s)
   334  }
   335  
   336  func (c *DefaultConnection) createMultipartReader(body *communication.MultipartFormDataObject) (io.ReadCloser, error) {
   337  	r, w := io.Pipe()
   338  
   339  	writer := multipart.NewWriter(w)
   340  	err := writer.SetBoundary(body.GetBoundary())
   341  	if err != nil {
   342  		return nil, err
   343  	}
   344  	if writer.FormDataContentType() != body.GetContentType() {
   345  		return nil, errors.New("multipart.Writer  did not create the expected content type")
   346  	}
   347  
   348  	go func() {
   349  		for name, value := range body.GetValues() {
   350  			err := writer.WriteField(name, value)
   351  			if err != nil {
   352  				w.CloseWithError(err)
   353  				return
   354  			}
   355  		}
   356  		for name, file := range body.GetFiles() {
   357  			// Do not use writer.CreateFormFile because it does not allow a custom content type
   358  			header := textproto.MIMEHeader{}
   359  			header.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, escapeQuotes(name), escapeQuotes(file.GetFileName())))
   360  			header.Set("Content-Type", file.GetContentType())
   361  			pw, err := writer.CreatePart(header)
   362  			if err != nil {
   363  				w.CloseWithError(err)
   364  				return
   365  			}
   366  			_, err = io.Copy(pw, file.GetContent())
   367  			if err != nil {
   368  				w.CloseWithError(err)
   369  				return
   370  			}
   371  		}
   372  		err = writer.Close()
   373  		if err != nil {
   374  			w.CloseWithError(err)
   375  			return
   376  		}
   377  		w.Close() // #nosec G104
   378  	}()
   379  
   380  	return r, nil
   381  }