Completed
Push — master ( 388c50...c3616e )
by ARCANEDEV
8s
created

SslChecker::showStreamExtensionWarning()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3.7085

Importance

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