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 }