Completed
Pull Request — master (#60)
by ARCANEDEV
08:31
created

SslChecker::checkResult()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4.0466

Importance

Changes 0
Metric Value
cc 4
eloc 7
nc 2
nop 3
dl 0
loc 11
ccs 6
cts 7
cp 0.8571
crap 4.0466
rs 9.2
c 0
b 0
f 0
1
<?php namespace Arcanedev\Stripe\Http\Curl;
2
3
use Arcanedev\Stripe\Contracts\Http\Curl\SslChecker as SslCheckerContract;
4
use Arcanedev\Stripe\Exceptions\ApiConnectionException;
5
use Arcanedev\Stripe\Stripe;
6
7
/**
8
 * Class     SslChecker
9
 *
10
 * @package  Arcanedev\Stripe\Http\Curl
11
 * @author   ARCANEDEV <[email protected]>
12
 */
13
class SslChecker implements SslCheckerContract
14
{
15
    /* ------------------------------------------------------------------------------------------------
16
     |  Properties
17
     | ------------------------------------------------------------------------------------------------
18
     */
19
    /** @var string */
20
    protected $url = '';
21
22
    /* ------------------------------------------------------------------------------------------------
23
     |  Getters & Setters
24
     | ------------------------------------------------------------------------------------------------
25
     */
26
    /**
27
     * Get URL.
28
     *
29
     * @return string
30
     */
31 6
    public function getUrl()
32
    {
33 6
        return $this->url;
34
    }
35
36
    /**
37
     * Set URL.
38
     *
39
     * @param  string  $url
40
     *
41
     * @return self
42
     */
43 2
    public function setUrl($url)
44
    {
45 2
        $this->url = $this->prepareUrl($url);
46
47 2
        return $this;
48
    }
49
50
    /* ------------------------------------------------------------------------------------------------
51
     |  Main Functions
52
     | ------------------------------------------------------------------------------------------------
53
     */
54
    /**
55
     * Preflight the SSL certificate presented by the backend. This isn't 100%
56
     * bulletproof, in that we're not actually validating the transport used to
57
     * communicate with Stripe, merely that the first attempt to does not use a
58
     * revoked certificate.
59
     *
60
     * Unfortunately the interface to OpenSSL doesn't make it easy to check the
61
     * certificate before sending potentially sensitive data on the wire. This
62
     * approach raises the bar for an attacker significantly.
63
     *
64
     * @param  string  $url
65
     *
66
     * @return bool
67
     */
68
    public function checkCert($url)
69
    {
70
        if ( ! $this->hasStreamExtensions())
71
            return $this->showStreamExtensionWarning();
72
73
        $this->setUrl($url);
74
75
        list($result, $errorNo, $errorStr) = $this->streamSocketClient();
76
77
        $this->checkResult($result, $errorNo, $errorStr);
78
79
        openssl_x509_export(
80
            stream_context_get_params($result)['options']['ssl']['peer_certificate'],
81
            $pemCert
82
        );
83
84
        $this->checkBlackList($pemCert);
85
86
        return true;
87
    }
88
89
    /* ------------------------------------------------------------------------------------------------
90
     |  Check Functions
91
     | ------------------------------------------------------------------------------------------------
92
     */
93
    /**
94
     * Check black list.
95
     *
96
     * @param  string  $pemCert
97
     *
98
     * @throws \Arcanedev\Stripe\Exceptions\ApiConnectionException
99
     */
100 2
    public function checkBlackList($pemCert)
101
    {
102 2
        if ($this->isBlackListed($pemCert)) {
103 2
            throw new ApiConnectionException(
104
                'Invalid server certificate. You tried to connect to a server that has a revoked SSL certificate, '.
105
                'which means we cannot securely send data to that server. '.
106 2
                'Please email [email protected] if you need help connecting to the correct API server.'
107
            );
108
        }
109
    }
110
111
    /**
112
     * Checks if a valid PEM encoded certificate is blacklisted.
113
     *
114
     * @param  string  $cert
115
     *
116
     * @return bool
117
     */
118 4
    public function isBlackListed($cert)
119
    {
120 4
        $lines = explode("\n", trim($cert));
121
122
        // Kludgily remove the PEM padding
123 4
        array_shift($lines);
124 4
        array_pop($lines);
125
126 4
        $fingerprint = sha1(base64_decode(implode('', $lines)));
127
128 4
        return in_array($fingerprint, [
129 4
            '05c0b3643694470a888c6e7feb5c9e24e823dc53',
130
            '5b7dc7fbc98d78bf76d4d4fa6f597a0c901fad5c',
131
        ]);
132
    }
133
134
135
    /**
136
     * Stream Extension exists - Return true if one of the extensions not found.
137
     *
138
     * @return bool
139
     */
140
    private function hasStreamExtensions()
141
    {
142
        return function_exists('stream_context_get_params') &&
143
               function_exists('stream_socket_enable_crypto');
144
    }
145
146
    /**
147
     * Check if has errors or empty result.
148
     *
149
     * @param  mixed     $result
150
     * @param  int|null  $errorNo
151
     * @param  string    $errorStr
152
     *
153
     * @throws \Arcanedev\Stripe\Exceptions\ApiConnectionException
154
     */
155 4
    private function checkResult($result, $errorNo, $errorStr)
156
    {
157
        if (
158 4
            ($errorNo !== 0 && $errorNo !== null) || $result === false
159
        )
160 4
            throw new ApiConnectionException(
161 4
                'Could not connect to Stripe ('.$this->getUrl().').  Please check your internet connection and try again. ' .
162 4
                'If this problem persists, you should check Stripe\'s service status at https://twitter.com/stripestatus. ' .
163 4
                'Reason was: ' . $errorStr
164
            );
165
    }
166
167
    /**
168
     * Check if has SSL Errors
169
     *
170
     * @param  int  $errorNum
171
     *
172
     * @return bool
173
     */
174 300
    public static function hasCertErrors($errorNum)
175
    {
176 300
        return in_array($errorNum, [
177 300
            CURLE_SSL_CACERT,
178 300
            CURLE_SSL_PEER_CERTIFICATE,
179 300
            CURLE_SSL_CACERT_BADFILE
180
        ]);
181
    }
182
183
    /* ------------------------------------------------------------------------------------------------
184
     |  Other Functions
185
     | ------------------------------------------------------------------------------------------------
186
     */
187
    /**
188
     * Prepare SSL URL.
189
     *
190
     * @param  string  $url
191
     *
192
     * @return string
193
     */
194 2
    private function prepareUrl($url)
195
    {
196 2
        $url  = parse_url($url);
197
198 2
        return "ssl://{$url['host']}:" . (isset($url['port']) ? $url['port'] : 443);
199
    }
200
201
    /**
202
     * Open a socket connection.
203
     *
204
     * @return array
205
     */
206
    private function streamSocketClient()
207
    {
208
        $result = stream_socket_client(
209
            $this->getUrl(),
210
            $errorNo,
211
            $errorStr,
212
            30,
213
            STREAM_CLIENT_CONNECT,
214
            stream_context_create([
215
                'ssl' => [
216
                    'capture_peer_cert' => true,
217
                    'verify_peer'       => true,
218
                    'cafile'            => self::caBundle(),
219
                ],
220
            ])
221
        );
222
223
        return [$result, $errorNo, $errorStr];
224
    }
225
226
    /**
227
     * Get the certificates file path.
228
     *
229
     * @return string
230
     */
231 2
    public static function caBundle()
232
    {
233 2
        return realpath(__DIR__.'/../../../data/ca-certificates.crt');
234
    }
235
236
    /**
237
     * Show Stream Extension Warning (stream_socket_enable_crypto is not supported in HHVM).
238
     *
239
     * @return true
240
     */
241 2
    private function showStreamExtensionWarning()
242
    {
243 2
        if ( ! is_testing())
244
            Stripe::getLogger()->error(
245
                'Warning: This version of PHP does not support checking SSL certificates Stripe '.
246
                'cannot guarantee that the server has a certificate which is not blacklisted.'
247
            );
248
249 2
        return true;
250
    }
251
}
252