1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Class CurlHandle |
4
|
|
|
* |
5
|
|
|
* @filesource CurlHandle.php |
6
|
|
|
* @created 30.08.2018 |
7
|
|
|
* @package chillerlan\HTTP\CurlUtils |
8
|
|
|
* @author smiley <[email protected]> |
9
|
|
|
* @copyright 2018 smiley |
10
|
|
|
* @license MIT |
11
|
|
|
*/ |
12
|
|
|
|
13
|
|
|
namespace chillerlan\HTTP\CurlUtils; |
14
|
|
|
|
15
|
|
|
use chillerlan\Settings\SettingsContainerInterface; |
16
|
|
|
use Psr\Http\Message\{RequestInterface, ResponseInterface}; |
17
|
|
|
|
18
|
|
|
use function array_key_exists, count, curl_close, curl_init, curl_reset, curl_setopt_array, |
19
|
|
|
explode, in_array, is_resource, strlen, strtolower, strtoupper, substr, trim; |
20
|
|
|
|
21
|
|
|
use const CURLOPT_CAINFO, CURLOPT_CONNECTTIMEOUT, CURLOPT_CUSTOMREQUEST, CURLOPT_FOLLOWLOCATION, CURLOPT_HEADER, |
22
|
|
|
CURLOPT_HEADERFUNCTION, CURLOPT_HTTP_VERSION, CURLOPT_HTTPHEADER, CURLOPT_INFILESIZE, CURLOPT_NOBODY, |
23
|
|
|
CURLOPT_POSTFIELDS, CURLOPT_PROGRESSFUNCTION, CURLOPT_PROTOCOLS, CURLOPT_READFUNCTION, CURLOPT_RETURNTRANSFER, |
24
|
|
|
CURLOPT_SSL_VERIFYHOST, CURLOPT_SSL_VERIFYPEER, CURLOPT_TIMEOUT, CURLOPT_UPLOAD, CURLOPT_URL, CURLOPT_USERAGENT, |
25
|
|
|
CURLOPT_USERPWD, CURLOPT_WRITEFUNCTION, CURLPROTO_HTTP, CURLPROTO_HTTPS, CURL_HTTP_VERSION_2TLS; |
26
|
|
|
|
27
|
|
|
class CurlHandle implements CurlHandleInterface{ |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* The cURL handle |
31
|
|
|
* |
32
|
|
|
* @var resource |
33
|
|
|
*/ |
34
|
|
|
public $curl; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* a handle ID (counter), used in CurlMultiClient |
38
|
|
|
* |
39
|
|
|
* @var int |
40
|
|
|
*/ |
41
|
|
|
public $id; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* a retry counter, used in CurlMultiClient |
45
|
|
|
* |
46
|
|
|
* @var int |
47
|
|
|
*/ |
48
|
|
|
public $retries; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* @var \Psr\Http\Message\RequestInterface |
52
|
|
|
*/ |
53
|
|
|
public $request; |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* @var \Psr\Http\Message\ResponseInterface |
57
|
|
|
*/ |
58
|
|
|
public $response; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* @var \chillerlan\HTTP\HTTPOptions |
62
|
|
|
*/ |
63
|
|
|
protected $options; |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* CurlHandle constructor. |
67
|
|
|
* |
68
|
|
|
* @param \Psr\Http\Message\RequestInterface $request |
69
|
|
|
* @param \Psr\Http\Message\ResponseInterface $response |
70
|
|
|
* @param \chillerlan\Settings\SettingsContainerInterface $options |
71
|
|
|
*/ |
72
|
|
|
public function __construct(RequestInterface $request, ResponseInterface $response, SettingsContainerInterface $options){ |
73
|
|
|
$this->request = $request; |
74
|
|
|
$this->response = $response; |
75
|
|
|
$this->options = $options; |
76
|
|
|
$this->curl = curl_init(); |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* close an existing cURL handle on exit |
81
|
|
|
*/ |
82
|
|
|
public function __destruct(){ |
83
|
|
|
$this->close(); |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* @return \chillerlan\HTTP\CurlUtils\CurlHandleInterface |
88
|
|
|
*/ |
89
|
|
|
public function close():CurlHandleInterface{ |
90
|
|
|
|
91
|
|
|
if(is_resource($this->curl)){ |
92
|
|
|
curl_close($this->curl); |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
return $this; |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* @return array |
100
|
|
|
*/ |
101
|
|
|
protected function initCurlOptions():array{ |
102
|
|
|
return [ |
103
|
|
|
CURLOPT_HEADER => false, |
104
|
|
|
CURLOPT_RETURNTRANSFER => true, |
105
|
|
|
CURLOPT_FOLLOWLOCATION => false, |
106
|
|
|
CURLOPT_URL => (string)$this->request->getUri()->withFragment(''), |
107
|
|
|
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2TLS, |
108
|
|
|
CURLOPT_USERAGENT => $this->options->user_agent, |
109
|
|
|
CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS, |
110
|
|
|
CURLOPT_SSL_VERIFYPEER => true, |
111
|
|
|
CURLOPT_SSL_VERIFYHOST => 2, |
112
|
|
|
CURLOPT_CAINFO => $this->options->ca_info, |
113
|
|
|
CURLOPT_TIMEOUT => $this->options->timeout, |
114
|
|
|
CURLOPT_CONNECTTIMEOUT => 30, |
115
|
|
|
CURLOPT_WRITEFUNCTION => [$this, 'writefunction'], |
116
|
|
|
CURLOPT_HEADERFUNCTION => [$this, 'headerfunction'], |
117
|
|
|
]; |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
/** |
121
|
|
|
* @param array $options |
122
|
|
|
* |
123
|
|
|
* @return array |
124
|
|
|
*/ |
125
|
|
|
protected function setBodyOptions(array $options):array{ |
126
|
|
|
$body = $this->request->getBody(); |
127
|
|
|
$bodySize = $body->getSize(); |
128
|
|
|
|
129
|
|
|
if($bodySize === 0){ |
130
|
|
|
return $options; |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
if($body->isSeekable()){ |
134
|
|
|
$body->rewind(); |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
// Message has non empty body. |
138
|
|
|
if($bodySize === null || $bodySize > 1 << 20){ |
139
|
|
|
// Avoid full loading large or unknown size body into memory |
140
|
|
|
$options[CURLOPT_UPLOAD] = true; |
141
|
|
|
|
142
|
|
|
if($bodySize !== null){ |
143
|
|
|
$options[CURLOPT_INFILESIZE] = $bodySize; |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
$options[CURLOPT_READFUNCTION] = [$this, 'readfunction']; |
147
|
|
|
} |
148
|
|
|
// Small body can be loaded into memory |
149
|
|
|
else{ |
150
|
|
|
$options[CURLOPT_POSTFIELDS] = (string)$body; |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
return $options; |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
/** |
157
|
|
|
* @param array $options |
158
|
|
|
* |
159
|
|
|
* @return array |
160
|
|
|
*/ |
161
|
|
|
protected function setHeaderOptions(array $options):array{ |
162
|
|
|
$headers = []; |
163
|
|
|
|
164
|
|
|
foreach($this->request->getHeaders() as $name => $values){ |
165
|
|
|
$header = strtolower($name); |
166
|
|
|
|
167
|
|
|
// curl-client does not support "Expect-Continue", so dropping "expect" headers |
168
|
|
|
if($header === 'expect'){ |
169
|
|
|
continue; |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
if($header === 'content-length'){ |
173
|
|
|
|
174
|
|
|
// Small body content length can be calculated here. |
175
|
|
|
if(array_key_exists(CURLOPT_POSTFIELDS, $options)){ |
176
|
|
|
$values = [strlen($options[CURLOPT_POSTFIELDS])]; |
177
|
|
|
} |
178
|
|
|
// Else if there is no body, forcing "Content-length" to 0 |
179
|
|
|
elseif(!array_key_exists(CURLOPT_READFUNCTION, $options)){ |
180
|
|
|
$values = ['0']; |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
} |
184
|
|
|
|
185
|
|
View Code Duplication |
foreach($values as $value){ |
|
|
|
|
186
|
|
|
$value = (string)$value; |
187
|
|
|
|
188
|
|
|
// cURL requires a special format for empty headers. |
189
|
|
|
// See https://github.com/guzzle/guzzle/issues/1882 for more details. |
190
|
|
|
$headers[] = $value === '' ? $name.';' : $name.': '.$value; |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
$options[CURLOPT_HTTPHEADER] = $headers; |
196
|
|
|
|
197
|
|
|
// If the Expect header is not present, prevent curl from adding it |
198
|
|
|
if(!$this->request->hasHeader('Expect')){ |
199
|
|
|
$options[CURLOPT_HTTPHEADER][] = 'Expect:'; |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
// cURL sometimes adds a content-type by default. Prevent this. |
203
|
|
|
if(!$this->request->hasHeader('Content-Type')){ |
204
|
|
|
$options[CURLOPT_HTTPHEADER][] = 'Content-Type:'; |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
return $options; |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* @return \chillerlan\HTTP\CurlUtils\CurlHandleInterface |
212
|
|
|
*/ |
213
|
|
|
public function init():CurlHandleInterface{ |
214
|
|
|
$options = $this->initCurlOptions(); |
215
|
|
|
$userinfo = $this->request->getUri()->getUserInfo(); |
216
|
|
|
$method = $this->request->getMethod(); |
217
|
|
|
|
218
|
|
|
if(!empty($userinfo)){ |
219
|
|
|
$options[CURLOPT_USERPWD] = $userinfo; |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
/* |
223
|
|
|
* Some HTTP methods cannot have payload: |
224
|
|
|
* |
225
|
|
|
* - GET — cURL will automatically change the method to PUT or POST |
226
|
|
|
* if we set CURLOPT_UPLOAD or CURLOPT_POSTFIELDS. |
227
|
|
|
* - HEAD — cURL treats HEAD as a GET request with same restrictions. |
228
|
|
|
* - TRACE — According to RFC7231: a client MUST NOT send a message body in a TRACE request. |
229
|
|
|
*/ |
230
|
|
|
|
231
|
|
|
if(in_array($method, ['DELETE', 'PATCH', 'POST', 'PUT'], true)){ |
232
|
|
|
$options = $this->setBodyOptions($options); |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
// This will set HTTP method to "HEAD". |
236
|
|
|
if($method === 'HEAD'){ |
237
|
|
|
$options[CURLOPT_NOBODY] = true; |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
// GET is a default method. Other methods should be specified explicitly. |
241
|
|
|
if($method !== 'GET'){ |
242
|
|
|
$options[CURLOPT_CUSTOMREQUEST] = $method; |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
// overwrite the default values with $curl_options |
246
|
|
|
foreach($this->options->curl_options as $k => $v){ |
247
|
|
|
// skip some options that are only set automatically |
248
|
|
|
if(in_array($k, [CURLOPT_HTTPHEADER, CURLOPT_CUSTOMREQUEST, CURLOPT_NOBODY], true)){ |
249
|
|
|
continue; |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
$options[$k] = $v; |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
$options = $this->setHeaderOptions($options); |
256
|
|
|
|
257
|
|
|
curl_setopt_array($this->curl, $options); |
258
|
|
|
|
259
|
|
|
return $this; |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
/** |
263
|
|
|
* @internal |
264
|
|
|
* |
265
|
|
|
* @param resource $curl |
266
|
|
|
* @param resource $stream |
267
|
|
|
* @param int $length |
268
|
|
|
* |
269
|
|
|
* @return string |
270
|
|
|
*/ |
271
|
|
|
public function readfunction($curl, $stream, int $length):string{ |
272
|
|
|
return $this->request->getBody()->read($length); |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* @internal |
277
|
|
|
* |
278
|
|
|
* @param resource $curl |
279
|
|
|
* @param string $data |
280
|
|
|
* |
281
|
|
|
* @return int |
282
|
|
|
*/ |
283
|
|
|
public function writefunction($curl, string $data):int{ |
284
|
|
|
return $this->response->getBody()->write($data); |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
/** |
288
|
|
|
* @internal |
289
|
|
|
* |
290
|
|
|
* @param resource $curl |
291
|
|
|
* @param string $line |
292
|
|
|
* |
293
|
|
|
* @return int |
294
|
|
|
*/ |
295
|
|
|
public function headerfunction($curl, string $line):int{ |
296
|
|
|
$str = trim($line); |
297
|
|
|
$header = explode(':', $str, 2); |
298
|
|
|
|
299
|
|
|
if(count($header) === 2){ |
300
|
|
|
$this->response = $this->response |
301
|
|
|
->withAddedHeader(trim($header[0]), trim($header[1])); |
302
|
|
|
} |
303
|
|
|
elseif(substr(strtoupper($str), 0, 5) === 'HTTP/'){ |
304
|
|
|
$status = explode(' ', $str, 3); |
305
|
|
|
$reason = count($status) > 2 ? trim($status[2]) : ''; |
306
|
|
|
|
307
|
|
|
$this->response = $this->response |
308
|
|
|
->withStatus((int)$status[1], $reason) |
309
|
|
|
->withProtocolVersion(substr($status[0], 5)) |
310
|
|
|
; |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
return strlen($line); |
314
|
|
|
} |
315
|
|
|
|
316
|
|
|
} |
317
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.