1 <?php
2 /**
3 * Copyright 2019 Klarna AB
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17 * File containing the Connector class.
18 */
19
20 namespace Klarna\Rest\Transport;
21
22 /**
23 * Transport connector used to authenticate and make HTTP requests against the
24 * Klarna APIs. Transport uses CURL to perform HTTP(s) calls.
25 */
26 class CURLConnector implements ConnectorInterface
27 {
28 /**
29 * Default request type
30 */
31 const DEFAULT_CONTENT_TYPE = 'application/json';
32
33 /**
34 * Extra CURL request options.
35 */
36 protected $options = [];
37
38 /**
39 * Merchant ID.
40 *
41 * @var string
42 */
43 protected $merchantId;
44
45 /**
46 * Shared secret.
47 *
48 * @var string
49 */
50 protected $sharedSecret;
51
52 /**
53 * Base URL.
54 *
55 * @var string
56 */
57 protected $baseUrl;
58
59 /**
60 * HTTP user agent.
61 *
62 * @var UserAgent
63 */
64 protected $userAgent;
65
66 public function __construct(
67 $merchantId,
68 $sharedSecret,
69 $baseUrl,
70 UserAgentInterface $userAgent = null
71 ) {
72 $this->merchantId = $merchantId;
73 $this->sharedSecret = $sharedSecret;
74 $this->baseUrl = rtrim($baseUrl, '/');
75
76 if ($userAgent === null) {
77 $userAgent = UserAgent::createDefault(['CURLConnector/' . curl_version()['version']]);
78 }
79 $this->userAgent = $userAgent;
80 }
81
82 /**
83 * Sets CURL request options.
84 *
85 * @param options CURL options
86 *
87 * @return self instance
88 */
89 public function setOptions($options)
90 {
91 $this->options = $options;
92 return $this;
93 }
94
95 /**
96 * Sends HTTP GET request to specified path.
97 *
98 * @param string $path URL path.
99 * @param array $headers HTTP request headers
100 * @return ApiResponse Processed response
101 *
102 * @throws RuntimeException if HTTP transport failed to execute a call
103 */
104 public function get($path, $headers = [])
105 {
106 return $this->request(Method::GET, $path, $headers);
107 }
108
109 /**
110 * Sends HTTP POST request to specified path.
111 *
112 * @param string $path URL path.
113 * @param string $data Data to be sent to API server in a payload. Example: json-encoded string
114 * @param array $headers HTTP request headers
115 * @return ApiResponse Processed response
116 * @throws RuntimeException if HTTP transport failed to execute a call
117 */
118 public function post($path, $data = null, $headers = [])
119 {
120 return $this->request(Method::POST, $path, $headers, $data);
121 }
122
123 /**
124 * Sends HTTP PUT request to specified path.
125 *
126 * @param string $path URL path.
127 * @param string $data Data to be sent to API server in a payload. Example: json-encoded string
128 * @param array $headers HTTP request headers
129 * @return ApiResponse Processed response
130 *
131 * @throws RuntimeException if HTTP transport failed to execute a call
132 */
133 public function put($path, $data = null, $headers = [])
134 {
135 return $this->request(Method::PUT, $path, $headers, $data);
136 }
137
138 /**
139 * Sends HTTP PATCH request to specified path.
140 *
141 * @param string $path URL path.
142 * @param string $data Data to be sent to API server in a payload. Example: json-encoded string
143 * @param array $headers HTTP request headers
144 * @return ApiResponse Processed response
145 *
146 * @throws RuntimeException if HTTP transport failed to execute a call
147 */
148 public function patch($path, $data = null, $headers = [])
149 {
150 return $this->request(Method::PATCH, $path, $headers, $data);
151 }
152
153 /**
154 * Sends HTTP DELETE request to specified path.
155 *
156 * @param string $path URL path.
157 * @param string $data Data to be sent to API server in a payload. Example: json-encoded string
158 * @param array $headers HTTP request headers
159 * @return ApiResponse Processed response
160 *
161 * @throws RuntimeException if HTTP transport failed to execute a call
162 */
163 public function delete($path, $data = null, $headers = [])
164 {
165 return $this->request(Method::DELETE, $path, $headers, $data);
166 }
167
168 /**
169 * Performs HTTP(S) request.
170 *
171 * @param string $path URL path.
172 * @param string $data Data to be sent to API server in a payload. Example: json-encoded string
173 * @param array $headers HTTP request headers
174 * @return ApiResponse Processed response
175 *
176 * @throws RuntimeException if HTTP transport failed to execute a call
177 */
178 protected function request($method, $url, array $headers = [], $data = null)
179 {
180 $headers = array_merge([
181 'User-Agent' => (string) $this->userAgent,
182 ], $headers);
183
184 if (isset($this->options['headers'])) {
185 $headers = array_merge($headers, $this->options['headers']);
186 }
187 array_walk($headers, function (&$v, $k) {
188 $v = $k . ': ' . $v;
189 });
190
191 $ch = curl_init();
192
193 if (!empty($this->merchantId)) {
194 curl_setopt($ch, CURLOPT_USERPWD, $this->merchantId . ':' . $this->sharedSecret);
195 }
196 if (!empty($this->options['ssl_cert'])) {
197 curl_setopt($ch, CURLOPT_SSLCERT, $this->options['ssl_cert']);
198 if (!empty($this->options['ssl_key'])) {
199 curl_setopt($ch, CURLOPT_SSLKEY, $this->options['ssl_key']);
200 }
201 }
202
203 if (!empty($this->options['timeout'])) {
204 curl_setopt($ch, CURLOPT_TIMEOUT, $this->options['timeout']);
205 }
206
207 curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
208
209 if ($method == Method::GET) {
210 curl_setopt($ch, CURLOPT_HTTPGET, 1);
211 } elseif ($method == Method::POST) {
212 curl_setopt($ch, CURLOPT_POST, 1);
213 curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
214 } else {
215 curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
216 curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
217 }
218
219 curl_setopt($ch, CURLOPT_URL, $this->baseUrl . $url);
220 curl_setopt($ch, CURLOPT_HEADER, 1);
221
222 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
223 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
224 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
225
226 if ($proxy = getenv('HTTP_PROXY')) {
227 $proxy = parse_url($proxy);
228
229 $proxyHost = $proxy['host'];
230 $proxyPort = $proxy['port'] ? ':' . $proxy['post'] : '';
231 curl_setopt($ch, CURLOPT_PROXY, $proxyHost . $proxyPort);
232 if (!empty($proxy['user'])) {
233 curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxy['user'] . ':' . $proxy['pass']);
234 }
235 }
236
237 $response = curl_exec($ch);
238
239 $errno = curl_errno($ch);
240 $error = curl_error($ch);
241 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
242 $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
243
244 curl_close($ch);
245
246 // Check the TCP transport issues
247 if (!empty($errno)) {
248 throw new \RuntimeException($error, $errno);
249 }
250
251 $rawHeaders = substr($response, 0, $header_size);
252 $body = substr($response, $header_size);
253 $headers = self::parseHeaders($rawHeaders);
254
255 return new ApiResponse($http_code, $body, $headers);
256 }
257
258 /**
259 * Factory method to create a connector instance.
260 *
261 * @param string $merchantId Merchant ID
262 * @param string $sharedSecret Shared secret
263 * @param string $baseUrl Base URL for HTTP requests
264 * @param UserAgentInterface $userAgent HTTP user agent to identify the client
265 *
266 * @return self
267 */
268 public static function create(
269 $merchantId,
270 $sharedSecret,
271 $baseUrl = self::EU_BASE_URL,
272 UserAgentInterface $userAgent = null
273 ) {
274 return new static($merchantId, $sharedSecret, $baseUrl, $userAgent);
275 }
276
277
278 /**
279 * Converts raw curl headers response to array.
280 *
281 * @param string $rawHeaders Headers part from the curl response
282 *
283 * @return array list of HTTP headers
284 */
285 protected static function parseHeaders($rawHeaders)
286 {
287 $headers = [];
288 foreach (explode("\r\n", $rawHeaders) as $i => $line) {
289 if (strlen($line) == 0) {
290 continue;
291 }
292
293 if (strpos($line, 'HTTP/') !== false) {
294 // The line contains the HTTP response information
295 $headers['Http'] = $line;
296 continue;
297 }
298 list($key, $value) = explode(': ', $line);
299 $headers[ucwords($key, '-_')][] = $value;
300 }
301 return $headers;
302 }
303 }
304