Socket   F
last analyzed

Complexity

Total Complexity 60

Size/Duplication

Total Lines 465
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 0
Metric Value
dl 0
loc 465
rs 3.6
c 0
b 0
f 0
wmc 60
lcom 1
cbo 4

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
C connect() 0 60 12
B _setSslContext() 0 28 8
A _connectionErrorHandler() 0 4 1
A context() 0 8 2
A host() 0 8 2
A address() 0 8 2
A addresses() 0 8 2
A lastError() 0 8 2
A setLastError() 0 4 1
A write() 0 17 6
A read() 0 20 5
A disconnect() 0 15 3
A __destruct() 0 4 1
A reset() 0 16 4
B enableCrypto() 0 42 8

How to fix   Complexity   

Complex Class

Complex classes like Socket often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Socket, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5
 *
6
 * Licensed under The MIT License
7
 * For full copyright and license information, please see the LICENSE.txt
8
 * Redistributions of files must retain the above copyright notice.
9
 *
10
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11
 * @link          https://cakephp.org CakePHP(tm) Project
12
 * @since         1.2.0
13
 * @license       https://opensource.org/licenses/mit-license.php MIT License
14
 */
15
namespace Cake\Network;
16
17
use Cake\Core\Exception\Exception as CakeException;
18
use Cake\Core\InstanceConfigTrait;
19
use Cake\Network\Exception\SocketException;
20
use Cake\Validation\Validation;
21
use Exception;
22
use InvalidArgumentException;
23
24
/**
25
 * CakePHP network socket connection class.
26
 *
27
 * Core base class for network communication.
28
 */
29
class Socket
30
{
31
    use InstanceConfigTrait;
32
33
    /**
34
     * Object description
35
     *
36
     * @var string
37
     */
38
    public $description = 'Remote DataSource Network Socket Interface';
39
40
    /**
41
     * Default configuration settings for the socket connection
42
     *
43
     * @var array
44
     */
45
    protected $_defaultConfig = [
46
        'persistent' => false,
47
        'host' => 'localhost',
48
        'protocol' => 'tcp',
49
        'port' => 80,
50
        'timeout' => 30,
51
    ];
52
53
    /**
54
     * Reference to socket connection resource
55
     *
56
     * @var resource|null
57
     */
58
    public $connection;
59
60
    /**
61
     * This boolean contains the current state of the Socket class
62
     *
63
     * @var bool
64
     */
65
    public $connected = false;
66
67
    /**
68
     * This variable contains an array with the last error number (num) and string (str)
69
     *
70
     * @var array
71
     */
72
    public $lastError = [];
73
74
    /**
75
     * True if the socket stream is encrypted after a Cake\Network\Socket::enableCrypto() call
76
     *
77
     * @var bool
78
     */
79
    public $encrypted = false;
80
81
    /**
82
     * Contains all the encryption methods available
83
     *
84
     * SSLv2 and SSLv3 are deprecated, and should not be used as they
85
     * have several published vulnerablilities.
86
     *
87
     * @var array
88
     */
89
    protected $_encryptMethods = [
90
        // @codingStandardsIgnoreStart
91
        // @deprecated Will be removed in 4.0.0
92
        'sslv2_client' => STREAM_CRYPTO_METHOD_SSLv2_CLIENT,
93
        // @deprecated Will be removed in 4.0.0
94
        'sslv3_client' => STREAM_CRYPTO_METHOD_SSLv3_CLIENT,
95
        'sslv23_client' => STREAM_CRYPTO_METHOD_SSLv23_CLIENT,
96
        'tls_client' => STREAM_CRYPTO_METHOD_TLS_CLIENT,
97
        'tlsv10_client' => STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT,
98
        'tlsv11_client' => STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT,
99
        'tlsv12_client' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT,
100
        // @deprecated Will be removed in 4.0.0
101
        'sslv2_server' => STREAM_CRYPTO_METHOD_SSLv2_SERVER,
102
        // @deprecated Will be removed in 4.0.0
103
        'sslv3_server' => STREAM_CRYPTO_METHOD_SSLv3_SERVER,
104
        'sslv23_server' => STREAM_CRYPTO_METHOD_SSLv23_SERVER,
105
        'tls_server' => STREAM_CRYPTO_METHOD_TLS_SERVER,
106
        'tlsv10_server' => STREAM_CRYPTO_METHOD_TLSv1_0_SERVER,
107
        'tlsv11_server' => STREAM_CRYPTO_METHOD_TLSv1_1_SERVER,
108
        'tlsv12_server' => STREAM_CRYPTO_METHOD_TLSv1_2_SERVER
109
        // @codingStandardsIgnoreEnd
110
    ];
111
112
    /**
113
     * Used to capture connection warnings which can happen when there are
114
     * SSL errors for example.
115
     *
116
     * @var array
117
     */
118
    protected $_connectionErrors = [];
119
120
    /**
121
     * Constructor.
122
     *
123
     * @param array $config Socket configuration, which will be merged with the base configuration
124
     * @see \Cake\Network\Socket::$_baseConfig
125
     */
126
    public function __construct(array $config = [])
127
    {
128
        $this->setConfig($config);
129
    }
130
131
    /**
132
     * Connect the socket to the given host and port.
133
     *
134
     * @return bool Success
135
     * @throws \Cake\Network\Exception\SocketException
136
     */
137
    public function connect()
138
    {
139
        if ($this->connection) {
140
            $this->disconnect();
141
        }
142
143
        $hasProtocol = strpos($this->_config['host'], '://') !== false;
144
        if ($hasProtocol) {
145
            list($this->_config['protocol'], $this->_config['host']) = explode('://', $this->_config['host']);
146
        }
147
        $scheme = null;
148
        if (!empty($this->_config['protocol'])) {
149
            $scheme = $this->_config['protocol'] . '://';
150
        }
151
152
        $this->_setSslContext($this->_config['host']);
153
        if (!empty($this->_config['context'])) {
154
            $context = stream_context_create($this->_config['context']);
155
        } else {
156
            $context = stream_context_create();
157
        }
158
159
        $connectAs = STREAM_CLIENT_CONNECT;
160
        if ($this->_config['persistent']) {
161
            $connectAs |= STREAM_CLIENT_PERSISTENT;
162
        }
163
164
        set_error_handler([$this, '_connectionErrorHandler']);
165
        $remoteSocketTarget = $scheme . $this->_config['host'];
166
        $port = (int)$this->_config['port'];
167
        if ($port > 0) {
168
            $remoteSocketTarget .= ':' . $port;
169
        }
170
        $this->connection = stream_socket_client(
171
            $remoteSocketTarget,
172
            $errNum,
173
            $errStr,
174
            $this->_config['timeout'],
175
            $connectAs,
176
            $context
177
        );
178
        restore_error_handler();
179
180
        if (!empty($errNum) || !empty($errStr)) {
181
            $this->setLastError($errNum, $errStr);
182
            throw new SocketException($errStr, $errNum);
183
        }
184
185
        if (!$this->connection && $this->_connectionErrors) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->_connectionErrors of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
186
            $message = implode("\n", $this->_connectionErrors);
187
            throw new SocketException($message, E_WARNING);
188
        }
189
190
        $this->connected = is_resource($this->connection);
191
        if ($this->connected) {
192
            stream_set_timeout($this->connection, $this->_config['timeout']);
193
        }
194
195
        return $this->connected;
196
    }
197
198
    /**
199
     * Configure the SSL context options.
200
     *
201
     * @param string $host The host name being connected to.
202
     * @return void
203
     */
204
    protected function _setSslContext($host)
205
    {
206
        foreach ($this->_config as $key => $value) {
207
            if (substr($key, 0, 4) !== 'ssl_') {
208
                continue;
209
            }
210
            $contextKey = substr($key, 4);
211
            if (empty($this->_config['context']['ssl'][$contextKey])) {
212
                $this->_config['context']['ssl'][$contextKey] = $value;
213
            }
214
            unset($this->_config[$key]);
215
        }
216
        if (!isset($this->_config['context']['ssl']['SNI_enabled'])) {
217
            $this->_config['context']['ssl']['SNI_enabled'] = true;
218
        }
219
        if (empty($this->_config['context']['ssl']['peer_name'])) {
220
            $this->_config['context']['ssl']['peer_name'] = $host;
221
        }
222
        if (empty($this->_config['context']['ssl']['cafile'])) {
223
            $dir = dirname(dirname(__DIR__));
224
            $this->_config['context']['ssl']['cafile'] = $dir . DIRECTORY_SEPARATOR .
225
                'config' . DIRECTORY_SEPARATOR . 'cacert.pem';
226
        }
227
        if (!empty($this->_config['context']['ssl']['verify_host'])) {
228
            $this->_config['context']['ssl']['CN_match'] = $host;
229
        }
230
        unset($this->_config['context']['ssl']['verify_host']);
231
    }
232
233
    /**
234
     * socket_stream_client() does not populate errNum, or $errStr when there are
235
     * connection errors, as in the case of SSL verification failure.
236
     *
237
     * Instead we need to handle those errors manually.
238
     *
239
     * @param int $code Code number.
240
     * @param string $message Message.
241
     * @return void
242
     */
243
    protected function _connectionErrorHandler($code, $message)
244
    {
245
        $this->_connectionErrors[] = $message;
246
    }
247
248
    /**
249
     * Get the connection context.
250
     *
251
     * @return array|null Null when there is no connection, an array when there is.
252
     */
253
    public function context()
254
    {
255
        if (!$this->connection) {
256
            return null;
257
        }
258
259
        return stream_context_get_options($this->connection);
260
    }
261
262
    /**
263
     * Get the host name of the current connection.
264
     *
265
     * @return string Host name
266
     */
267
    public function host()
268
    {
269
        if (Validation::ip($this->_config['host'])) {
270
            return gethostbyaddr($this->_config['host']);
271
        }
272
273
        return gethostbyaddr($this->address());
274
    }
275
276
    /**
277
     * Get the IP address of the current connection.
278
     *
279
     * @return string IP address
280
     */
281
    public function address()
282
    {
283
        if (Validation::ip($this->_config['host'])) {
284
            return $this->_config['host'];
285
        }
286
287
        return gethostbyname($this->_config['host']);
288
    }
289
290
    /**
291
     * Get all IP addresses associated with the current connection.
292
     *
293
     * @return array IP addresses
294
     */
295
    public function addresses()
296
    {
297
        if (Validation::ip($this->_config['host'])) {
298
            return [$this->_config['host']];
299
        }
300
301
        return gethostbynamel($this->_config['host']);
302
    }
303
304
    /**
305
     * Get the last error as a string.
306
     *
307
     * @return string|null Last error
308
     */
309
    public function lastError()
310
    {
311
        if (!empty($this->lastError)) {
312
            return $this->lastError['num'] . ': ' . $this->lastError['str'];
313
        }
314
315
        return null;
316
    }
317
318
    /**
319
     * Set the last error.
320
     *
321
     * @param int $errNum Error code
322
     * @param string $errStr Error string
323
     * @return void
324
     */
325
    public function setLastError($errNum, $errStr)
326
    {
327
        $this->lastError = ['num' => $errNum, 'str' => $errStr];
328
    }
329
330
    /**
331
     * Write data to the socket.
332
     *
333
     * The bool false return value is deprecated and will be int 0 in the next major.
334
     * Please code respectively to be future proof.
335
     *
336
     * @param string $data The data to write to the socket.
337
     * @return int|false Bytes written.
338
     */
339
    public function write($data)
340
    {
341
        if (!$this->connected && !$this->connect()) {
342
            return false;
343
        }
344
        $totalBytes = strlen($data);
345
        $written = 0;
346
        while ($written < $totalBytes) {
347
            $rv = fwrite($this->connection, substr($data, $written));
348
            if ($rv === false || $rv === 0) {
349
                return $written;
350
            }
351
            $written += $rv;
352
        }
353
354
        return $written;
355
    }
356
357
    /**
358
     * Read data from the socket. Returns false if no data is available or no connection could be
359
     * established.
360
     *
361
     * The bool false return value is deprecated and will be null in the next major.
362
     * Please code respectively to be future proof.
363
     *
364
     * @param int $length Optional buffer length to read; defaults to 1024
365
     * @return mixed Socket data
366
     */
367
    public function read($length = 1024)
368
    {
369
        if (!$this->connected && !$this->connect()) {
370
            return false;
371
        }
372
373
        if (!feof($this->connection)) {
374
            $buffer = fread($this->connection, $length);
375
            $info = stream_get_meta_data($this->connection);
376
            if ($info['timed_out']) {
377
                $this->setLastError(E_WARNING, 'Connection timed out');
378
379
                return false;
380
            }
381
382
            return $buffer;
383
        }
384
385
        return false;
386
    }
387
388
    /**
389
     * Disconnect the socket from the current connection.
390
     *
391
     * @return bool Success
392
     */
393
    public function disconnect()
394
    {
395
        if (!is_resource($this->connection)) {
396
            $this->connected = false;
397
398
            return true;
399
        }
400
        $this->connected = !fclose($this->connection);
401
402
        if (!$this->connected) {
403
            $this->connection = null;
404
        }
405
406
        return !$this->connected;
407
    }
408
409
    /**
410
     * Destructor, used to disconnect from current connection.
411
     */
412
    public function __destruct()
413
    {
414
        $this->disconnect();
415
    }
416
417
    /**
418
     * Resets the state of this Socket instance to it's initial state (before Object::__construct got executed)
419
     *
420
     * @param array|null $state Array with key and values to reset
421
     * @return bool True on success
422
     */
423
    public function reset($state = null)
424
    {
425
        if (empty($state)) {
426
            static $initalState = [];
427
            if (empty($initalState)) {
428
                $initalState = get_class_vars(__CLASS__);
429
            }
430
            $state = $initalState;
431
        }
432
433
        foreach ($state as $property => $value) {
434
            $this->{$property} = $value;
435
        }
436
437
        return true;
438
    }
439
440
    /**
441
     * Encrypts current stream socket, using one of the defined encryption methods
442
     *
443
     * @param string $type can be one of 'ssl2', 'ssl3', 'ssl23' or 'tls'
444
     * @param string $clientOrServer can be one of 'client', 'server'. Default is 'client'
445
     * @param bool $enable enable or disable encryption. Default is true (enable)
446
     * @return bool True on success
447
     * @throws \InvalidArgumentException When an invalid encryption scheme is chosen.
448
     * @throws \Cake\Network\Exception\SocketException When attempting to enable SSL/TLS fails
449
     * @see stream_socket_enable_crypto
450
     */
451
    public function enableCrypto($type, $clientOrServer = 'client', $enable = true)
452
    {
453
        if (!array_key_exists($type . '_' . $clientOrServer, $this->_encryptMethods)) {
454
            throw new InvalidArgumentException('Invalid encryption scheme chosen');
455
        }
456
        $method = $this->_encryptMethods[$type . '_' . $clientOrServer];
457
458
        // Prior to PHP 5.6.7 TLS_CLIENT was any version of TLS. This was changed in 5.6.7
459
        // to fix backwards compatibility issues, and now only resolves to TLS1.0
460
        //
461
        // See https://github.com/php/php-src/commit/10bc5fd4c4c8e1dd57bd911b086e9872a56300a0
462
        if (version_compare(PHP_VERSION, '5.6.7', '>=')) {
463
            if ($method == STREAM_CRYPTO_METHOD_TLS_CLIENT) {
464
                // @codingStandardsIgnoreStart
465
                $method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
466
                // @codingStandardsIgnoreEnd
467
            }
468
            if ($method == STREAM_CRYPTO_METHOD_TLS_SERVER) {
469
                // @codingStandardsIgnoreStart
470
                $method |= STREAM_CRYPTO_METHOD_TLSv1_1_SERVER | STREAM_CRYPTO_METHOD_TLSv1_2_SERVER;
471
                // @codingStandardsIgnoreEnd
472
            }
473
        }
474
475
        try {
476
            if ($this->connection === null) {
477
                throw new CakeException('You must call connect() first.');
478
            }
479
            $enableCryptoResult = stream_socket_enable_crypto($this->connection, $enable, $method);
480
        } catch (Exception $e) {
481
            $this->setLastError(null, $e->getMessage());
482
            throw new SocketException($e->getMessage(), null, $e);
483
        }
484
        if ($enableCryptoResult === true) {
485
            $this->encrypted = $enable;
486
487
            return true;
488
        }
489
        $errorMessage = 'Unable to perform enableCrypto operation on the current socket';
490
        $this->setLastError(null, $errorMessage);
491
        throw new SocketException($errorMessage);
492
    }
493
}
494