Passed
Push — gh-pages ( 22b0fe...eb2d91 )
by
unknown
12:27 queued 10:15
created

HTTPOptionsTrait::setCaBundleCurl()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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

229
			case is_dir(/** @scrutinizer ignore-type */ $this->ca_info):
Loading history...
230
			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

230
			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

230
			case is_link(/** @scrutinizer ignore-type */ $this->ca_info) && is_dir(readlink($this->ca_info)): // @codeCoverageIgnore
Loading history...
231
				$this->curl_options[CURLOPT_CAPATH] = $this->ca_info;
232
				unset($this->curl_options[CURLOPT_CAINFO]);
233
234
				return true;
235
236
			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

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