Completed
Push — master ( a03528...3c4fae )
by smiley
03:53
created

HTTPOptionsTrait::checkCaDefaultLocations()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
nc 3
nop 0
dl 0
loc 40
rs 8.9688
c 0
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
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
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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