Passed
Push — main ( f2efe3...53dc0a )
by smiley
10:15
created

HTTPOptionsTrait::setCaBundle()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 12
nc 5
nop 0
dl 0
loc 21
rs 8.8333
c 1
b 0
f 0
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
 * @phan-file-suppress PhanTypeInvalidThrowsIsInterface
13
 */
14
15
namespace chillerlan\HTTP;
16
17
use chillerlan\HTTP\Psr18\ClientException;
18
19
use function file_exists, ini_get, is_array, is_dir, is_file, is_link, is_string, readlink, trim;
20
21
use const CURLOPT_CAINFO, CURLOPT_CAPATH, CURLOPT_SSL_VERIFYHOST, CURLOPT_SSL_VERIFYPEER;
22
23
trait HTTPOptionsTrait{
24
25
	/**
26
	 * A custom user agent string
27
	 */
28
	protected string $user_agent = 'chillerlanHttpInterface/2.0 +https://github.com/chillerlan/php-httpinterface';
29
30
	/**
31
	 * options for each curl instance
32
	 *
33
	 * this array is being merged into the default options as the last thing before curl_exec().
34
	 * none of the values (except existence of the CA file) will be checked - that's up to the implementation.
35
	 */
36
	protected array $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
	 * @link https://curl.haxx.se/docs/caextract.html
42
	 * @link https://curl.haxx.se/ca/cacert.pem
43
	 * @link https://raw.githubusercontent.com/bagder/ca-bundle/master/ca-bundle.crt
44
	 */
45
	protected ?string $ca_info = null;
46
47
	/**
48
	 * see CURLOPT_SSL_VERIFYPEER
49
	 * requires either HTTPOptions::$ca_info or a properly working system CA file
50
	 *
51
	 * @link https://php.net/manual/function.curl-setopt.php
52
	 */
53
	protected bool $ssl_verifypeer = true;
54
55
	/**
56
	 * options for the curl multi instance
57
	 *
58
	 * @link https://www.php.net/manual/function.curl-multi-setopt.php
59
	 */
60
	protected array $curl_multi_options = [];
61
62
	/**
63
	 * maximum of concurrent requests for curl_multi
64
	 */
65
	protected int $windowSize = 5;
66
67
	/**
68
	 * sleep timer (milliseconds) between each fired multi request on startup
69
	 */
70
	protected ?int $sleep = null;
71
72
	/**
73
	 * Timeout value
74
	 *
75
	 * @see \CURLOPT_TIMEOUT
76
	 */
77
	protected int $timeout = 10;
78
79
	/**
80
	 * Number of retries (multi fetch)
81
	 */
82
	protected int $retries = 3;
83
84
	/**
85
	 * HTTPOptionsTrait constructor
86
	 *
87
	 * @throws \Psr\Http\Client\ClientExceptionInterface
88
	 */
89
	protected function HTTPOptionsTrait():void{
90
91
		if(!is_array($this->curl_options)){
0 ignored issues
show
introduced by
The condition is_array($this->curl_options) is always true.
Loading history...
92
			$this->curl_options = [];
93
		}
94
95
		if(!is_string($this->user_agent) || empty(trim($this->user_agent))){
0 ignored issues
show
introduced by
The condition is_string($this->user_agent) is always true.
Loading history...
96
			throw new ClientException('invalid user agent');
97
		}
98
99
		$this->setCA();
100
	}
101
102
	/**
103
	 * @throws \Psr\Http\Client\ClientExceptionInterface
104
	 */
105
	protected function setCA():void{
106
107
		if(!$this->checkVerifyEnabled()){
108
			return;
109
		}
110
111
		// a path/dir/link to a CA bundle is given, let's check that
112
		if(is_string($this->ca_info)){
113
114
			if($this->setCaBundle()){
115
				return;
116
			}
117
118
			throw new ClientException('invalid path to SSL CA bundle (HTTPOptions::$ca_info): '.$this->ca_info);
119
		}
120
121
		// we somehow landed here, so let's check if there's a CA bundle given via the cURL options
122
		$ca = $this->curl_options[CURLOPT_CAPATH] ?? $this->curl_options[CURLOPT_CAINFO] ?? false;
123
124
		if($ca){
125
126
			if($this->setCaBundleCurl($ca)){
127
				return;
128
			}
129
130
			throw new ClientException('invalid path to SSL CA bundle (CURLOPT_CAPATH/CURLOPT_CAINFO): '.$ca);
131
		}
132
133
		// check php.ini options - PHP should find the file by itself
134
		if(file_exists(ini_get('curl.cainfo'))){
135
			return; // @codeCoverageIgnore
136
		}
137
138
		// this is getting weird. as a last resort, we're going to check some default paths for a CA bundle file
139
		if($this->checkCaDefaultLocations()){
140
			return;
141
		}
142
143
		// @codeCoverageIgnoreStart
144
		$msg = 'No system CA bundle could be found in any of the the common system locations. '
145
		       .'In order to verify peer certificates, you will need to supply the path on disk to a certificate bundle via  '
146
		       .'HTTPOptions::$ca_info or HTTPOptions::$curl_options. If you do not need a specific certificate bundle, '
147
		       .'then you can download a CA bundle over here: https://curl.haxx.se/docs/caextract.html. '
148
		       .'Once you have a CA bundle available on disk, you can set the "curl.cainfo" php.ini setting to point '
149
		       .'to the path of the file, allowing you to omit the $ca_info or $curl_options setting. '
150
		       .'See http://curl.haxx.se/docs/sslcerts.html for more information.';
151
152
		throw new ClientException($msg);
153
		// @codeCoverageIgnoreEnd
154
	}
155
156
	/**
157
	 * Check default locations for the CA bundle
158
	 *
159
	 * @internal
160
	 */
161
	protected function checkCaDefaultLocations():bool{
162
163
		$cafiles = [
164
			// check other php.ini settings
165
			ini_get('openssl.cafile'),
166
			// Red Hat, CentOS, Fedora (provided by the ca-certificates package)
167
			'/etc/pki/tls/certs/ca-bundle.crt',
168
			// Ubuntu, Debian (provided by the ca-certificates package)
169
			'/etc/ssl/certs/ca-certificates.crt',
170
			// FreeBSD (provided by the ca_root_nss package)
171
			'/usr/local/share/certs/ca-root-nss.crt',
172
			// SLES 12 (provided by the ca-certificates package)
173
			'/var/lib/ca-certificates/ca-bundle.pem',
174
			// OS X provided by homebrew (using the default path)
175
			'/usr/local/etc/openssl/cert.pem',
176
			// Google app engine
177
			'/etc/ca-certificates.crt',
178
			// Windows?
179
			// http://php.net/manual/en/function.curl-setopt.php#110457
180
			'C:\\Windows\\system32\\curl-ca-bundle.crt',
181
			'C:\\Windows\\curl-ca-bundle.crt',
182
			'C:\\Windows\\system32\\cacert.pem',
183
			'C:\\Windows\\cacert.pem',
184
			// working path
185
			__DIR__.'/cacert.pem',
186
		];
187
188
		foreach($cafiles as $file){
189
190
			if(is_file($file) || (is_link($file) && is_file(readlink($file)))){
191
				$this->curl_options[CURLOPT_CAINFO] = $file;
192
				$this->ca_info                      = $file;
193
194
				return true;
195
			}
196
197
		}
198
199
		return false; // @codeCoverageIgnore
200
	}
201
202
	/**
203
	 * @internal
204
	 */
205
	protected function checkVerifyEnabled():bool{
206
207
		// disable verification if wanted so
208
		if($this->ssl_verifypeer !== true || (isset($this->curl_options[CURLOPT_SSL_VERIFYPEER]) && !$this->curl_options[CURLOPT_SSL_VERIFYPEER])){
209
			unset($this->curl_options[CURLOPT_CAINFO], $this->curl_options[CURLOPT_CAPATH]);
210
211
			$this->curl_options[CURLOPT_SSL_VERIFYHOST] = 0;
212
			$this->curl_options[CURLOPT_SSL_VERIFYPEER] = false;
213
214
			return false;
215
		}
216
217
		$this->curl_options[CURLOPT_SSL_VERIFYHOST] = 2;
218
		$this->curl_options[CURLOPT_SSL_VERIFYPEER] = true;
219
220
		return true;
221
	}
222
223
	/**
224
	 * @internal
225
	 */
226
	protected function setCaBundle():bool{
227
228
		// if you - for whatever obscure reason - need to check Windows .lnk links,
229
		// see http://php.net/manual/en/function.is-link.php#91249
230
		switch(true){
231
			case is_dir($this->ca_info):
0 ignored issues
show
Bug introduced by
It seems like $this->ca_info can also be of type null; however, parameter $filename of is_dir() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

231
			case is_dir(/** @scrutinizer ignore-type */ $this->ca_info):
Loading history...
232
			case is_link($this->ca_info) && is_dir(readlink($this->ca_info)): // @codeCoverageIgnore
0 ignored issues
show
Bug introduced by
It seems like $this->ca_info can also be of type null; however, parameter $path of readlink() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

232
			case is_link($this->ca_info) && is_dir(readlink(/** @scrutinizer ignore-type */ $this->ca_info)): // @codeCoverageIgnore
Loading history...
Bug introduced by
It seems like $this->ca_info can also be of type null; however, parameter $filename of is_link() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

232
			case is_link(/** @scrutinizer ignore-type */ $this->ca_info) && is_dir(readlink($this->ca_info)): // @codeCoverageIgnore
Loading history...
233
				$this->curl_options[CURLOPT_CAPATH] = $this->ca_info;
234
				unset($this->curl_options[CURLOPT_CAINFO]);
235
236
				return true;
237
238
			case is_file($this->ca_info):
0 ignored issues
show
Bug introduced by
It seems like $this->ca_info can also be of type null; however, parameter $filename of is_file() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

238
			case is_file(/** @scrutinizer ignore-type */ $this->ca_info):
Loading history...
239
			case is_link($this->ca_info) && is_file(readlink($this->ca_info)): // @codeCoverageIgnore
240
				$this->curl_options[CURLOPT_CAINFO] = $this->ca_info;
241
				unset($this->curl_options[CURLOPT_CAPATH]);
242
243
				return true;
244
		}
245
246
		return false;
247
	}
248
249
	/**
250
	 * @internal
251
	 */
252
	protected function setCaBundleCurl(string $ca):bool{
253
254
		// just check if the file/path exists
255
		switch(true){
256
			case is_dir($ca):
257
			case is_link($ca) && is_dir(readlink($ca)): // @codeCoverageIgnore
258
				unset($this->curl_options[CURLOPT_CAINFO]);
259
260
				return true;
261
262
			case is_file($ca):
263
			case is_link($ca) && is_file(readlink($ca)): // @codeCoverageIgnore
264
265
				return true;
266
		}
267
268
		return false;
269
	}
270
271
}
272