Skip to content

Commit 71b0827

Browse files
committed
:octocat: more comprehensive checks for #1
1 parent 07734db commit 71b0827

File tree

3 files changed

+243
-10
lines changed

3 files changed

+243
-10
lines changed

src/HTTPOptions.php

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
* @property string $user_agent
1919
* @property array $curl_options
2020
* @property string $ca_info
21+
* @property bool $ssl_verifypeer
2122
*/
2223
class HTTPOptions extends SettingsContainerAbstract{
2324
use HTTPOptionsTrait;

src/HTTPOptionsTrait.php

+126-10
Original file line numberDiff line numberDiff line change
@@ -30,35 +30,151 @@ trait HTTPOptionsTrait{
3030
protected $curl_options = [];
3131

3232
/**
33-
* CA Root Certificates for use with CURL/SSL (if not configured in php.ini)
33+
* CA Root Certificates for use with CURL/SSL (if not configured in php.ini or available in a default path)
3434
*
3535
* @var string
36+
*
37+
* @link https://curl.haxx.se/docs/caextract.html
3638
* @link https://curl.haxx.se/ca/cacert.pem
3739
* @link https://raw.githubusercontent.com/bagder/ca-bundle/master/ca-bundle.crt
3840
*/
3941
protected $ca_info = null;
4042

43+
/**
44+
* see CURLOPT_SSL_VERIFYPEER
45+
* requires either HTTPOptions::$ca_info or a properly working system CA file
46+
*
47+
* @var bool
48+
* @link http://php.net/manual/en/function.curl-setopt.php
49+
*/
50+
protected $ssl_verifypeer = true;
51+
4152
/**
4253
* HTTPOptionsTrait constructor
4354
*
44-
* @throws \chillerlan\HTTP\ClientException
55+
* @throws \Psr\Http\Client\ClientExceptionInterface
4556
*/
4657
protected function HTTPOptionsTrait():void{
4758

4859
if(!is_array($this->curl_options)){
4960
$this->curl_options = [];
5061
}
5162

52-
// we cannot verify a peer against a non-existent ca file, so turn it off in that case
53-
/* if(!$this->ca_info || !is_file($this->ca_info)
54-
|| (isset($this->curl_options[CURLOPT_CAINFO]) && !is_file($this->curl_options[CURLOPT_CAINFO]))){
63+
if(!is_string($this->user_agent) || empty(trim($this->user_agent))){
64+
throw new ClientException('invalid user agent');
65+
}
66+
67+
$this->setCA();
68+
}
69+
70+
/**
71+
* @return void
72+
* @throws \Psr\Http\Client\ClientExceptionInterface
73+
*/
74+
protected function setCA():void{
75+
76+
// disable verification if wanted so
77+
if($this->ssl_verifypeer !== true || (isset($this->curl_options[CURLOPT_SSL_VERIFYPEER]) && !$this->curl_options[CURLOPT_SSL_VERIFYPEER])){
78+
unset($this->curl_options[CURLOPT_CAINFO], $this->curl_options[CURLOPT_CAPATH]);
79+
80+
$this->curl_options[CURLOPT_SSL_VERIFYHOST] = 0;
81+
$this->curl_options[CURLOPT_SSL_VERIFYPEER] = false;
82+
83+
return;
84+
}
85+
86+
$this->curl_options[CURLOPT_SSL_VERIFYHOST] = 2;
87+
$this->curl_options[CURLOPT_SSL_VERIFYPEER] = true;
88+
89+
// a path/dir/link to a CA bundle is given, let's check that
90+
if(is_string($this->ca_info)){
91+
92+
// if you - for whatever obscure reason - need to check Windows .lnk links,
93+
// see http://php.net/manual/en/function.is-link.php#91249
94+
switch(true){
95+
case is_dir($this->ca_info):
96+
case is_link($this->ca_info) && is_dir(readlink($this->ca_info)): // @codeCoverageIgnore
97+
$this->curl_options[CURLOPT_CAPATH] = $this->ca_info;
98+
unset($this->curl_options[CURLOPT_CAINFO]);
99+
return;
100+
101+
case is_file($this->ca_info):
102+
case is_link($this->ca_info) && is_file(readlink($this->ca_info)): // @codeCoverageIgnore
103+
$this->curl_options[CURLOPT_CAINFO] = $this->ca_info;
104+
unset($this->curl_options[CURLOPT_CAPATH]);
105+
return;
106+
}
107+
108+
throw new ClientException('invalid path to SSL CA bundle (HTTPOptions::$ca_info): '.$this->ca_info);
109+
}
110+
111+
// we somehow landed here, so let's check if there's a CA bundle given via the cURL options
112+
$ca = $this->curl_options[CURLOPT_CAPATH] ?? $this->curl_options[CURLOPT_CAINFO] ?? false;
113+
114+
if($ca){
115+
116+
// just check if the file/path exists
117+
switch(true){
118+
case is_dir($ca):
119+
case is_link($ca) && is_dir(readlink($ca)): // @codeCoverageIgnore
120+
unset($this->curl_options[CURLOPT_CAINFO]);
121+
return;
122+
123+
case is_file($ca):
124+
case is_link($ca) && is_file(readlink($ca)): // @codeCoverageIgnore
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+
$cafiles = [
138+
// check other php.ini settings
139+
ini_get('openssl.cafile'),
140+
// Red Hat, CentOS, Fedora (provided by the ca-certificates package)
141+
'/etc/pki/tls/certs/ca-bundle.crt',
142+
// Ubuntu, Debian (provided by the ca-certificates package)
143+
'/etc/ssl/certs/ca-certificates.crt',
144+
// FreeBSD (provided by the ca_root_nss package)
145+
'/usr/local/share/certs/ca-root-nss.crt',
146+
// SLES 12 (provided by the ca-certificates package)
147+
'/var/lib/ca-certificates/ca-bundle.pem',
148+
// OS X provided by homebrew (using the default path)
149+
'/usr/local/etc/openssl/cert.pem',
150+
// Google app engine
151+
'/etc/ca-certificates.crt',
152+
// Windows?
153+
// http://php.net/manual/en/function.curl-setopt.php#110457
154+
'C:\\Windows\\system32\\curl-ca-bundle.crt',
155+
'C:\\Windows\\curl-ca-bundle.crt',
156+
'C:\\Windows\\system32\\cacert.pem',
157+
'C:\\Windows\\cacert.pem',
158+
// working path
159+
__DIR__.'/cacert.pem',
160+
];
161+
162+
foreach($cafiles as $file){
163+
if(is_file($file) || (is_link($file) && is_file(readlink($file)))){
164+
$this->curl_options[CURLOPT_CAINFO] = $file;
165+
return;
166+
}
167+
}
55168

56-
$this->curl_options += [
57-
CURLOPT_SSL_VERIFYPEER => false,
58-
CURLOPT_CAINFO => null,
59-
];
60-
}*/
169+
$msg = 'No system CA bundle could be found in any of the the common system locations. '
170+
.'In order to verify peer certificates, you will need to supply the path on disk to a certificate bundle via '
171+
.'HTTPOptions::$ca_info or HTTPOptions::$curl_options. If you do not need a specific certificate bundle, '
172+
.'then you can download a CA bundle over here: https://curl.haxx.se/docs/caextract.html. '
173+
.'Once you have a CA bundle available on disk, you can set the "curl.cainfo" php.ini setting to point '
174+
.'to the path to the file, allowing you to omit the $ca_info or $curl_options setting. '
175+
.'See http://curl.haxx.se/docs/sslcerts.html for more information.';
61176

177+
throw new ClientException($msg); // @codeCoverageIgnore
62178
}
63179

64180
}

tests/HTTPOptionsTest.php

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<?php
2+
/**
3+
* Class HTTPOptionsTest
4+
*
5+
* @filesource HTTPOptionsTest.php
6+
* @created 14.11.2018
7+
* @package chillerlan\HTTPTest
8+
* @author smiley <smiley@chillerlan.net>
9+
* @copyright 2018 smiley
10+
* @license MIT
11+
*/
12+
13+
namespace chillerlan\HTTPTest;
14+
15+
use chillerlan\HTTP\HTTPOptions;
16+
use PHPUnit\Framework\TestCase;
17+
18+
class HTTPOptionsTest extends TestCase{
19+
20+
public function testConvertInvalidCurlOptionsValueToArray(){
21+
$this->assertTrue(is_array((new HTTPOptions(['curl_options' => 'foo']))->curl_options)); //coverage
22+
}
23+
24+
/**
25+
* @expectedException \Psr\Http\Client\ClientExceptionInterface
26+
* @expectedExceptionMessage invalid user agent
27+
*/
28+
public function testInvalidUserAgentException(){
29+
new HTTPOptions(['user_agent' => false]);
30+
}
31+
32+
public function testCaDisable(){
33+
$o = new HTTPOptions([
34+
'ssl_verifypeer' => false,
35+
'curl_options' => [
36+
CURLOPT_CAINFO => 'foo',
37+
CURLOPT_CAPATH => 'bar',
38+
],
39+
]);
40+
41+
$this->assertSame(0, $o->curl_options[CURLOPT_SSL_VERIFYHOST]);
42+
$this->assertSame(false, $o->curl_options[CURLOPT_SSL_VERIFYPEER]);
43+
$this->assertArrayNotHasKey(CURLOPT_CAINFO, $o->curl_options);
44+
$this->assertArrayNotHasKey(CURLOPT_CAPATH, $o->curl_options);
45+
}
46+
47+
public function testCaInfoFile(){
48+
$file = __DIR__.'/cacert.pem';
49+
$o = new HTTPOptions(['ca_info' => $file]);
50+
51+
$this->assertSame($file, $o->curl_options[CURLOPT_CAINFO]);
52+
$this->assertSame(2, $o->curl_options[CURLOPT_SSL_VERIFYHOST]);
53+
$this->assertSame(true, $o->curl_options[CURLOPT_SSL_VERIFYPEER]);
54+
$this->assertArrayNotHasKey(CURLOPT_CAPATH, $o->curl_options);
55+
}
56+
57+
public function testCaInfoDir(){
58+
$dir = __DIR__;
59+
$o = new HTTPOptions(['ca_info' => $dir]);
60+
61+
$this->assertSame($dir, $o->curl_options[CURLOPT_CAPATH]);
62+
$this->assertSame(2, $o->curl_options[CURLOPT_SSL_VERIFYHOST]);
63+
$this->assertSame(true, $o->curl_options[CURLOPT_SSL_VERIFYPEER]);
64+
$this->assertArrayNotHasKey(CURLOPT_CAINFO, $o->curl_options);
65+
}
66+
67+
/**
68+
* @expectedException \Psr\Http\Client\ClientExceptionInterface
69+
* @expectedExceptionMessage invalid path to SSL CA bundle (HTTPOptions::$ca_info): foo
70+
*/
71+
public function testCaInfoInvalidException(){
72+
new HTTPOptions(['ca_info' => 'foo']);
73+
}
74+
75+
public function testCurloptCaInfoFile(){
76+
$file = __DIR__.'/cacert.pem';
77+
$o = new HTTPOptions(['curl_options' => [CURLOPT_CAINFO => $file]]);
78+
79+
$this->assertSame($file, $o->curl_options[CURLOPT_CAINFO]);
80+
$this->assertSame(2, $o->curl_options[CURLOPT_SSL_VERIFYHOST]);
81+
$this->assertSame(true, $o->curl_options[CURLOPT_SSL_VERIFYPEER]);
82+
$this->assertArrayNotHasKey(CURLOPT_CAPATH, $o->curl_options);
83+
}
84+
85+
public function testCurloptCaInfoDir(){
86+
$dir = __DIR__;
87+
$o = new HTTPOptions(['curl_options' => [CURLOPT_CAPATH => $dir]]);
88+
89+
$this->assertSame($dir, $o->curl_options[CURLOPT_CAPATH]);
90+
$this->assertSame(2, $o->curl_options[CURLOPT_SSL_VERIFYHOST]);
91+
$this->assertSame(true, $o->curl_options[CURLOPT_SSL_VERIFYPEER]);
92+
$this->assertArrayNotHasKey(CURLOPT_CAINFO, $o->curl_options);
93+
}
94+
95+
/**
96+
* @expectedException \Psr\Http\Client\ClientExceptionInterface
97+
* @expectedExceptionMessage invalid path to SSL CA bundle (CURLOPT_CAPATH/CURLOPT_CAINFO): foo
98+
*/
99+
public function testCurloptCaInfoInvalidException(){
100+
new HTTPOptions(['curl_options' => [CURLOPT_CAINFO => 'foo']]);
101+
}
102+
103+
public function testCaInfoFallback(){
104+
105+
if(file_exists(ini_get('curl.cainfo'))){
106+
$this->markTestSkipped('curl.cainfo set');
107+
}
108+
109+
$o = new HTTPOptions;
110+
111+
$this->assertFileExists($o->curl_options[CURLOPT_CAINFO]);
112+
$this->assertSame(2, $o->curl_options[CURLOPT_SSL_VERIFYHOST]);
113+
$this->assertSame(true, $o->curl_options[CURLOPT_SSL_VERIFYPEER]);
114+
}
115+
116+
}

0 commit comments

Comments
 (0)