HTTPOptionsTrait   A
last analyzed

Complexity

Total Complexity 24

Size/Duplication

Total Lines 232
Duplicated Lines 0 %

Importance

Changes 8
Bugs 1 Features 0
Metric Value
eloc 69
c 8
b 1
f 0
dl 0
loc 232
rs 10
wmc 24

6 Methods

Rating   Name   Duplication   Size   Complexity  
A set_ca_info() 0 2 1
A checkCaDefaultLocations() 0 39 5
B checkCA() 0 18 7
A setCA() 0 35 5
A set_curl_options() 0 17 4
A HTTPOptionsTrait() 0 4 2
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_dir, is_file, is_link, readlink, trim;
18
19
use const CURLOPT_CAINFO, CURLOPT_CAPATH;
20
21
trait HTTPOptionsTrait{
22
23
	/**
24
	 * A custom user agent string
25
	 */
26
	protected string $user_agent = 'chillerlanHttpInterface/6.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.se/docs/caextract.html
40
	 * @link https://curl.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
	 * @internal
47
	 */
48
	protected bool $ca_info_is_path = false;
49
50
	/**
51
	 * see CURLOPT_SSL_VERIFYPEER
52
	 * requires either HTTPOptions::$ca_info or a properly working system CA file
53
	 *
54
	 * @link https://php.net/manual/function.curl-setopt.php
55
	 */
56
	protected bool $ssl_verifypeer = true;
57
58
	/**
59
	 * options for the curl multi instance
60
	 *
61
	 * @link https://www.php.net/manual/function.curl-multi-setopt.php
62
	 */
63
	protected array $curl_multi_options = [];
64
65
	/**
66
	 * maximum of concurrent requests for curl_multi
67
	 */
68
	protected int $window_size = 5;
69
70
	/**
71
	 * sleep timer (milliseconds) between each fired multi request on startup
72
	 */
73
	protected ?int $sleep = null;
74
75
	/**
76
	 * Timeout value
77
	 *
78
	 * @see \CURLOPT_TIMEOUT
79
	 */
80
	protected int $timeout = 10;
81
82
	/**
83
	 * Number of retries (multi fetch)
84
	 */
85
	protected int $retries = 3;
86
87
	/**
88
	 * cURL extra hardening
89
	 *
90
	 * When set to true, cURL validates that the server staples an OCSP response during the TLS handshake.
91
	 *
92
	 * Use with caution as cURL will refuse a connection if it doesn't receive a valid OCSP response -
93
	 * this does not necessarily mean that the TLS connection is insecure.
94
	 *
95
	 * @see \CURLOPT_SSL_VERIFYSTATUS
96
	 */
97
	protected bool $curl_check_OCSP = false;
98
99
	/**
100
	 * HTTPOptionsTrait constructor
101
	 *
102
	 * @throws \Psr\Http\Client\ClientExceptionInterface
103
	 */
104
	protected function HTTPOptionsTrait():void{
105
106
		if(empty(trim($this->user_agent))){
107
			throw new ClientException('invalid user agent');
108
		}
109
110
	}
111
112
	/**
113
	 *
114
	 */
115
	protected function set_ca_info(string $ca_info = null):void{
116
		$this->setCA($ca_info);
117
	}
118
119
	/**
120
	 *
121
	 */
122
	protected function set_curl_options(array $curl_options):void{
123
		$ca_info = null;
124
125
		// let's check if there's a CA bundle given via the cURL options and move it to the ca_info option instead
126
		foreach([CURLOPT_CAPATH, CURLOPT_CAINFO] as $opt){
127
128
			if(isset($curl_options[$opt])){
129
				$ca_info = $curl_options[$opt];
130
131
				unset($curl_options[$opt]);
132
			}
133
		}
134
135
		$this->curl_options = $curl_options;
136
137
		if($ca_info){
138
			$this->setCA($ca_info);
139
		}
140
141
	}
142
143
	/**
144
	 * @throws \Psr\Http\Client\ClientExceptionInterface
145
	 */
146
	protected function setCA(string $ca_info = null):void{
147
		$this->ca_info = null;
148
149
		// a path/dir/link to a CA bundle is given, let's check that
150
		if($ca_info !== null){
151
152
			if($this->checkCA($ca_info)){
153
				$this->ca_info = $ca_info;
154
155
				return;
156
			}
157
158
			throw new ClientException('invalid path to SSL CA bundle: '.$ca_info);
159
		}
160
161
		// check php.ini options - PHP should find the file by itself
162
		if(file_exists(ini_get('curl.cainfo'))){
163
			return; // @codeCoverageIgnore
164
		}
165
166
		// this is getting weird. as a last resort, we're going to check some default paths for a CA bundle file
167
		if($this->checkCaDefaultLocations()){
168
			return;
169
		}
170
171
		// @codeCoverageIgnoreStart
172
		$msg = 'No system CA bundle could be found in any of the the common system locations. '
173
		       .'In order to verify peer certificates, you will need to supply the path on disk to a certificate bundle via  '
174
		       .'HTTPOptions::$ca_info. If you do not need a specific certificate bundle, '
175
		       .'then you can download a CA bundle over here: https://curl.haxx.se/docs/caextract.html. '
176
		       .'Once you have a CA bundle available on disk, you can set the "curl.cainfo" php.ini setting to point '
177
		       .'to the path of the file, allowing you to omit the $ca_info setting. '
178
		       .'See https://curl.se/docs/sslcerts.html for more information.';
179
180
		throw new ClientException($msg);
181
		// @codeCoverageIgnoreEnd
182
	}
183
184
	/**
185
	 * Check default locations for the CA bundle
186
	 *
187
	 * @internal
188
	 */
189
	protected function checkCaDefaultLocations():bool{
190
191
		$cafiles = [
192
			// check other php.ini settings
193
			ini_get('openssl.cafile'),
194
			// Red Hat, CentOS, Fedora (provided by the ca-certificates package)
195
			'/etc/pki/tls/certs/ca-bundle.crt',
196
			// Ubuntu, Debian (provided by the ca-certificates package)
197
			'/etc/ssl/certs/ca-certificates.crt',
198
			// FreeBSD (provided by the ca_root_nss package)
199
			'/usr/local/share/certs/ca-root-nss.crt',
200
			// SLES 12 (provided by the ca-certificates package)
201
			'/var/lib/ca-certificates/ca-bundle.pem',
202
			// OS X provided by homebrew (using the default path)
203
			'/usr/local/etc/openssl/cert.pem',
204
			// Google app engine
205
			'/etc/ca-certificates.crt',
206
			// Windows?
207
			// http://php.net/manual/en/function.curl-setopt.php#110457
208
			'C:\\Windows\\system32\\curl-ca-bundle.crt',
209
			'C:\\Windows\\curl-ca-bundle.crt',
210
			'C:\\Windows\\system32\\cacert.pem',
211
			'C:\\Windows\\cacert.pem',
212
			// working path
213
			__DIR__.'/cacert.pem',
214
		];
215
216
		foreach($cafiles as $file){
217
218
			if(is_file($file) || (is_link($file) && is_file(readlink($file)))){
219
				$this->ca_info         = $file;
220
				$this->ca_info_is_path = false;
221
222
				return true;
223
			}
224
225
		}
226
227
		return false; // @codeCoverageIgnore
228
	}
229
230
	/**
231
	 * Check whether the given CA info exists and if it is file or dir
232
	 *
233
	 * @phan-suppress PhanTypeMismatchArgumentNullableInternal
234
	 */
235
	protected function checkCA(string $ca = null):bool{
236
		// if you - for whatever obscure reason - need to check Windows .lnk links,
237
		// see http://php.net/manual/en/function.is-link.php#91249
238
		switch(true){
239
			case is_dir($ca):
0 ignored issues
show
Bug introduced by
It seems like $ca 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

239
			case is_dir(/** @scrutinizer ignore-type */ $ca):
Loading history...
240
			case is_link($ca) && is_dir(readlink($ca)): // @codeCoverageIgnore
0 ignored issues
show
Bug introduced by
It seems like $ca 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

240
			case is_link(/** @scrutinizer ignore-type */ $ca) && is_dir(readlink($ca)): // @codeCoverageIgnore
Loading history...
Bug introduced by
It seems like $ca 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

240
			case is_link($ca) && is_dir(readlink(/** @scrutinizer ignore-type */ $ca)): // @codeCoverageIgnore
Loading history...
241
				$this->ca_info_is_path = true;
242
243
				return true;
244
245
			case is_file($ca):
0 ignored issues
show
Bug introduced by
It seems like $ca 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

245
			case is_file(/** @scrutinizer ignore-type */ $ca):
Loading history...
246
			case is_link($ca) && is_file(readlink($ca)): // @codeCoverageIgnore
247
				$this->ca_info_is_path = false;
248
249
				return true;
250
		}
251
252
		return false;
253
	}
254
255
}
256