1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Trait HTTPOptionsTrait |
4
|
|
|
* |
5
|
|
|
* @filesource HTTPOptionsTrait.php |
6
|
|
|
* @created 28.08.2018 |
7
|
|
|
* @package chillerlan\HTTP |
8
|
|
|
* @author Smiley <[email protected]> |
9
|
|
|
* @copyright 2018 Smiley |
10
|
|
|
* @license MIT |
11
|
|
|
*/ |
12
|
|
|
|
13
|
|
|
namespace chillerlan\HTTP; |
14
|
|
|
|
15
|
|
|
use chillerlan\HTTP\Psr18\{ClientException, CurlHandle}; |
16
|
|
|
|
17
|
|
|
use function file_exists, ini_get, is_array, is_dir, is_file, is_link, is_string, readlink, trim; |
18
|
|
|
|
19
|
|
|
use const CURLOPT_CAINFO, CURLOPT_CAPATH, CURLOPT_SSL_VERIFYHOST, CURLOPT_SSL_VERIFYPEER; |
20
|
|
|
|
21
|
|
|
trait HTTPOptionsTrait{ |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* @var string |
25
|
|
|
*/ |
26
|
|
|
protected $user_agent = 'chillerlanHttpInterface/2.0 +https://github.com/chillerlan/php-httpinterface'; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* options for each curl instance |
30
|
|
|
* |
31
|
|
|
* this array is being merged into the default options as the last thing before curl_exec(). |
32
|
|
|
* none of the values (except existence of the CA file) will be checked - that's up to the implementation. |
33
|
|
|
* |
34
|
|
|
* @var array |
35
|
|
|
*/ |
36
|
|
|
protected $curl_options = []; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* CA Root Certificates for use with CURL/SSL (if not configured in php.ini or available in a default path) |
40
|
|
|
* |
41
|
|
|
* @var string |
42
|
|
|
* |
43
|
|
|
* @link https://curl.haxx.se/docs/caextract.html |
44
|
|
|
* @link https://curl.haxx.se/ca/cacert.pem |
45
|
|
|
* @link https://raw.githubusercontent.com/bagder/ca-bundle/master/ca-bundle.crt |
46
|
|
|
*/ |
47
|
|
|
protected $ca_info = null; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* see CURLOPT_SSL_VERIFYPEER |
51
|
|
|
* requires either HTTPOptions::$ca_info or a properly working system CA file |
52
|
|
|
* |
53
|
|
|
* @var bool |
54
|
|
|
* @link http://php.net/manual/en/function.curl-setopt.php |
55
|
|
|
*/ |
56
|
|
|
protected $ssl_verifypeer = true; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* The CurlHandleInterface to use in CurlClient::sendRequest() |
60
|
|
|
* |
61
|
|
|
* @var string |
62
|
|
|
*/ |
63
|
|
|
protected $curlHandle = CurlHandle::class; |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* HTTPOptionsTrait constructor |
67
|
|
|
* |
68
|
|
|
* @throws \Psr\Http\Client\ClientExceptionInterface |
69
|
|
|
*/ |
70
|
|
|
protected function HTTPOptionsTrait():void{ |
71
|
|
|
|
72
|
|
|
if(!is_array($this->curl_options)){ |
73
|
|
|
$this->curl_options = []; |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
if(!is_string($this->user_agent) || empty(trim($this->user_agent))){ |
77
|
|
|
throw new ClientException('invalid user agent'); |
78
|
|
|
} |
79
|
|
|
|
80
|
|
|
$this->setCA(); |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* @return void |
85
|
|
|
* @throws \Psr\Http\Client\ClientExceptionInterface |
86
|
|
|
*/ |
87
|
|
|
protected function setCA():void{ |
88
|
|
|
|
89
|
|
|
if(!$this->checkVerifyEnabled()){ |
90
|
|
|
return; |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
// a path/dir/link to a CA bundle is given, let's check that |
94
|
|
|
if(is_string($this->ca_info)){ |
95
|
|
|
|
96
|
|
|
if($this->setCaBundle()){ |
97
|
|
|
return; |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
throw new ClientException('invalid path to SSL CA bundle (HTTPOptions::$ca_info): '.$this->ca_info); |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
// we somehow landed here, so let's check if there's a CA bundle given via the cURL options |
104
|
|
|
$ca = $this->curl_options[CURLOPT_CAPATH] ?? $this->curl_options[CURLOPT_CAINFO] ?? false; |
105
|
|
|
|
106
|
|
|
if($ca){ |
107
|
|
|
|
108
|
|
|
if($this->setCaBundleCurl($ca)){ |
109
|
|
|
return; |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
throw new ClientException('invalid path to SSL CA bundle (CURLOPT_CAPATH/CURLOPT_CAINFO): '.$ca); |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
// check php.ini options - PHP should find the file by itself |
116
|
|
|
if(file_exists(ini_get('curl.cainfo'))){ |
117
|
|
|
return; // @codeCoverageIgnore |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
// this is getting weird. as a last resort, we're going to check some default paths for a CA bundle file |
121
|
|
|
if($this->checkCaDefaultLocations()){ |
122
|
|
|
return; |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
// @codeCoverageIgnoreStart |
126
|
|
|
$msg = 'No system CA bundle could be found in any of the the common system locations. ' |
127
|
|
|
.'In order to verify peer certificates, you will need to supply the path on disk to a certificate bundle via ' |
128
|
|
|
.'HTTPOptions::$ca_info or HTTPOptions::$curl_options. If you do not need a specific certificate bundle, ' |
129
|
|
|
.'then you can download a CA bundle over here: https://curl.haxx.se/docs/caextract.html. ' |
130
|
|
|
.'Once you have a CA bundle available on disk, you can set the "curl.cainfo" php.ini setting to point ' |
131
|
|
|
.'to the path of the file, allowing you to omit the $ca_info or $curl_options setting. ' |
132
|
|
|
.'See http://curl.haxx.se/docs/sslcerts.html for more information.'; |
133
|
|
|
|
134
|
|
|
throw new ClientException($msg); |
135
|
|
|
// @codeCoverageIgnoreEnd |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* @return bool |
140
|
|
|
*/ |
141
|
|
|
protected function checkCaDefaultLocations():bool{ |
142
|
|
|
|
143
|
|
|
$cafiles = [ |
144
|
|
|
// check other php.ini settings |
145
|
|
|
ini_get('openssl.cafile'), |
146
|
|
|
// Red Hat, CentOS, Fedora (provided by the ca-certificates package) |
147
|
|
|
'/etc/pki/tls/certs/ca-bundle.crt', |
148
|
|
|
// Ubuntu, Debian (provided by the ca-certificates package) |
149
|
|
|
'/etc/ssl/certs/ca-certificates.crt', |
150
|
|
|
// FreeBSD (provided by the ca_root_nss package) |
151
|
|
|
'/usr/local/share/certs/ca-root-nss.crt', |
152
|
|
|
// SLES 12 (provided by the ca-certificates package) |
153
|
|
|
'/var/lib/ca-certificates/ca-bundle.pem', |
154
|
|
|
// OS X provided by homebrew (using the default path) |
155
|
|
|
'/usr/local/etc/openssl/cert.pem', |
156
|
|
|
// Google app engine |
157
|
|
|
'/etc/ca-certificates.crt', |
158
|
|
|
// Windows? |
159
|
|
|
// http://php.net/manual/en/function.curl-setopt.php#110457 |
160
|
|
|
'C:\\Windows\\system32\\curl-ca-bundle.crt', |
161
|
|
|
'C:\\Windows\\curl-ca-bundle.crt', |
162
|
|
|
'C:\\Windows\\system32\\cacert.pem', |
163
|
|
|
'C:\\Windows\\cacert.pem', |
164
|
|
|
// working path |
165
|
|
|
__DIR__.'/cacert.pem', |
166
|
|
|
]; |
167
|
|
|
|
168
|
|
|
foreach($cafiles as $file){ |
169
|
|
|
|
170
|
|
|
if(is_file($file) || (is_link($file) && is_file(readlink($file)))){ |
171
|
|
|
$this->curl_options[CURLOPT_CAINFO] = $file; |
172
|
|
|
$this->ca_info = $file; |
173
|
|
|
|
174
|
|
|
return true; |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
return false; |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
/** |
183
|
|
|
* @return bool |
184
|
|
|
*/ |
185
|
|
|
protected function checkVerifyEnabled():bool{ |
186
|
|
|
|
187
|
|
|
// disable verification if wanted so |
188
|
|
|
if($this->ssl_verifypeer !== true || (isset($this->curl_options[CURLOPT_SSL_VERIFYPEER]) && !$this->curl_options[CURLOPT_SSL_VERIFYPEER])){ |
189
|
|
|
unset($this->curl_options[CURLOPT_CAINFO], $this->curl_options[CURLOPT_CAPATH]); |
190
|
|
|
|
191
|
|
|
$this->curl_options[CURLOPT_SSL_VERIFYHOST] = 0; |
192
|
|
|
$this->curl_options[CURLOPT_SSL_VERIFYPEER] = false; |
193
|
|
|
|
194
|
|
|
return false; |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
$this->curl_options[CURLOPT_SSL_VERIFYHOST] = 2; |
198
|
|
|
$this->curl_options[CURLOPT_SSL_VERIFYPEER] = true; |
199
|
|
|
|
200
|
|
|
return true; |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
/** |
204
|
|
|
* @return bool |
205
|
|
|
*/ |
206
|
|
|
protected function setCaBundle():bool{ |
207
|
|
|
|
208
|
|
|
// if you - for whatever obscure reason - need to check Windows .lnk links, |
209
|
|
|
// see http://php.net/manual/en/function.is-link.php#91249 |
210
|
|
|
switch(true){ |
211
|
|
|
case is_dir($this->ca_info): |
212
|
|
View Code Duplication |
case is_link($this->ca_info) && is_dir(readlink($this->ca_info)): // @codeCoverageIgnore |
|
|
|
|
213
|
|
|
$this->curl_options[CURLOPT_CAPATH] = $this->ca_info; |
214
|
|
|
unset($this->curl_options[CURLOPT_CAINFO]); |
215
|
|
|
|
216
|
|
|
return true; |
217
|
|
|
|
218
|
|
|
case is_file($this->ca_info): |
219
|
|
View Code Duplication |
case is_link($this->ca_info) && is_file(readlink($this->ca_info)): // @codeCoverageIgnore |
|
|
|
|
220
|
|
|
$this->curl_options[CURLOPT_CAINFO] = $this->ca_info; |
221
|
|
|
unset($this->curl_options[CURLOPT_CAPATH]); |
222
|
|
|
|
223
|
|
|
return true; |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
return false; |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
/** |
230
|
|
|
* @param string $ca |
231
|
|
|
* |
232
|
|
|
* @return bool |
233
|
|
|
*/ |
234
|
|
|
protected function setCaBundleCurl(string $ca):bool{ |
235
|
|
|
|
236
|
|
|
// just check if the file/path exists |
237
|
|
|
switch(true){ |
238
|
|
|
case is_dir($ca): |
239
|
|
|
case is_link($ca) && is_dir(readlink($ca)): // @codeCoverageIgnore |
240
|
|
|
unset($this->curl_options[CURLOPT_CAINFO]); |
241
|
|
|
|
242
|
|
|
return true; |
243
|
|
|
|
244
|
|
|
case is_file($ca): |
245
|
|
|
case is_link($ca) && is_file(readlink($ca)): // @codeCoverageIgnore |
246
|
|
|
|
247
|
|
|
return true; |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
return false; |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
} |
254
|
|
|
|
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.