統計
| ブランチ: | リビジョン:

pictcode / lib / Cake / Network / CakeSocket.php @ 0b1b8047

履歴 | 表示 | アノテート | ダウンロード (11.938 KB)

1
<?php
2
/**
3
 * CakePHP Socket connection class.
4
 *
5
 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
6
 * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
7
 *
8
 * Licensed under The MIT License
9
 * For full copyright and license information, please see the LICENSE.txt
10
 * Redistributions of files must retain the above copyright notice.
11
 *
12
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
13
 * @link          http://cakephp.org CakePHP(tm) Project
14
 * @package       Cake.Network
15
 * @since         CakePHP(tm) v 1.2.0
16
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
17
 */
18

    
19
App::uses('Validation', 'Utility');
20

    
21
/**
22
 * CakePHP network socket connection class.
23
 *
24
 * Core base class for network communication.
25
 *
26
 * @package       Cake.Network
27
 */
28
class CakeSocket {
29

    
30
/**
31
 * Object description
32
 *
33
 * @var string
34
 */
35
        public $description = 'Remote DataSource Network Socket Interface';
36

    
37
/**
38
 * Base configuration settings for the socket connection
39
 *
40
 * @var array
41
 */
42
        protected $_baseConfig = array(
43
                'persistent' => false,
44
                'host' => 'localhost',
45
                'protocol' => 'tcp',
46
                'port' => 80,
47
                'timeout' => 30
48
        );
49

    
50
/**
51
 * Configuration settings for the socket connection
52
 *
53
 * @var array
54
 */
55
        public $config = array();
56

    
57
/**
58
 * Reference to socket connection resource
59
 *
60
 * @var resource
61
 */
62
        public $connection = null;
63

    
64
/**
65
 * This boolean contains the current state of the CakeSocket class
66
 *
67
 * @var bool
68
 */
69
        public $connected = false;
70

    
71
/**
72
 * This variable contains an array with the last error number (num) and string (str)
73
 *
74
 * @var array
75
 */
76
        public $lastError = array();
77

    
78
/**
79
 * True if the socket stream is encrypted after a CakeSocket::enableCrypto() call
80
 *
81
 * @var bool
82
 */
83
        public $encrypted = false;
84

    
85
/**
86
 * Contains all the encryption methods available
87
 *
88
 * @var array
89
 */
90
        protected $_encryptMethods = array(
91
                // @codingStandardsIgnoreStart
92
                'sslv2_client' => STREAM_CRYPTO_METHOD_SSLv2_CLIENT,
93
                'sslv3_client' => STREAM_CRYPTO_METHOD_SSLv3_CLIENT,
94
                'sslv23_client' => STREAM_CRYPTO_METHOD_SSLv23_CLIENT,
95
                'tls_client' => STREAM_CRYPTO_METHOD_TLS_CLIENT,
96
                'sslv2_server' => STREAM_CRYPTO_METHOD_SSLv2_SERVER,
97
                'sslv3_server' => STREAM_CRYPTO_METHOD_SSLv3_SERVER,
98
                'sslv23_server' => STREAM_CRYPTO_METHOD_SSLv23_SERVER,
99
                'tls_server' => STREAM_CRYPTO_METHOD_TLS_SERVER
100
                // @codingStandardsIgnoreEnd
101
        );
102

    
103
/**
104
 * Used to capture connection warnings which can happen when there are
105
 * SSL errors for example.
106
 *
107
 * @var array
108
 */
109
        protected $_connectionErrors = array();
110

    
111
/**
112
 * Constructor.
113
 *
114
 * @param array $config Socket configuration, which will be merged with the base configuration
115
 * @see CakeSocket::$_baseConfig
116
 */
117
        public function __construct($config = array()) {
118
                $this->config = array_merge($this->_baseConfig, $config);
119
        }
120

    
121
/**
122
 * Connects the socket to the given host and port.
123
 *
124
 * @return bool Success
125
 * @throws SocketException
126
 */
127
        public function connect() {
128
                if ($this->connection) {
129
                        $this->disconnect();
130
                }
131

    
132
                $hasProtocol = strpos($this->config['host'], '://') !== false;
133
                if ($hasProtocol) {
134
                        list($this->config['protocol'], $this->config['host']) = explode('://', $this->config['host']);
135
                }
136
                $scheme = null;
137
                if (!empty($this->config['protocol'])) {
138
                        $scheme = $this->config['protocol'] . '://';
139
                }
140
                if (!empty($this->config['proxy'])) {
141
                        $scheme = 'tcp://';
142
                }
143

    
144
                $host = $this->config['host'];
145
                if (isset($this->config['request']['uri']['host'])) {
146
                        $host = $this->config['request']['uri']['host'];
147
                }
148
                $this->_setSslContext($host);
149

    
150
                if (!empty($this->config['context'])) {
151
                        $context = stream_context_create($this->config['context']);
152
                } else {
153
                        $context = stream_context_create();
154
                }
155

    
156
                $connectAs = STREAM_CLIENT_CONNECT;
157
                if ($this->config['persistent']) {
158
                        $connectAs |= STREAM_CLIENT_PERSISTENT;
159
                }
160

    
161
                set_error_handler(array($this, '_connectionErrorHandler'));
162
                $this->connection = stream_socket_client(
163
                        $scheme . $this->config['host'] . ':' . $this->config['port'],
164
                        $errNum,
165
                        $errStr,
166
                        $this->config['timeout'],
167
                        $connectAs,
168
                        $context
169
                );
170
                restore_error_handler();
171

    
172
                if (!empty($errNum) || !empty($errStr)) {
173
                        $this->setLastError($errNum, $errStr);
174
                        throw new SocketException($errStr, $errNum);
175
                }
176

    
177
                if (!$this->connection && $this->_connectionErrors) {
178
                        $message = implode("\n", $this->_connectionErrors);
179
                        throw new SocketException($message, E_WARNING);
180
                }
181

    
182
                $this->connected = is_resource($this->connection);
183
                if ($this->connected) {
184
                        stream_set_timeout($this->connection, $this->config['timeout']);
185

    
186
                        if (!empty($this->config['request']) &&
187
                                $this->config['request']['uri']['scheme'] === 'https' &&
188
                                !empty($this->config['proxy'])
189
                        ) {
190
                                $req = array();
191
                                $req[] = 'CONNECT ' . $this->config['request']['uri']['host'] . ':' .
192
                                        $this->config['request']['uri']['port'] . ' HTTP/1.1';
193
                                $req[] = 'Host: ' . $this->config['host'];
194
                                $req[] = 'User-Agent: php proxy';
195
                                if (!empty($this->config['proxyauth'])) {
196
                                        $req[] = 'Proxy-Authorization: ' . $this->config['proxyauth'];
197
                                }
198

    
199
                                fwrite($this->connection, implode("\r\n", $req) . "\r\n\r\n");
200

    
201
                                while (!feof($this->connection)) {
202
                                        $s = rtrim(fgets($this->connection, 4096));
203
                                        if (preg_match('/^$/', $s)) {
204
                                                break;
205
                                        }
206
                                }
207

    
208
                                $this->enableCrypto('tls', 'client');
209
                        }
210
                }
211
                return $this->connected;
212
        }
213

    
214
/**
215
 * Configure the SSL context options.
216
 *
217
 * @param string $host The host name being connected to.
218
 * @return void
219
 */
220
        protected function _setSslContext($host) {
221
                foreach ($this->config as $key => $value) {
222
                        if (substr($key, 0, 4) !== 'ssl_') {
223
                                continue;
224
                        }
225
                        $contextKey = substr($key, 4);
226
                        if (empty($this->config['context']['ssl'][$contextKey])) {
227
                                $this->config['context']['ssl'][$contextKey] = $value;
228
                        }
229
                        unset($this->config[$key]);
230
                }
231
                if (version_compare(PHP_VERSION, '5.3.2', '>=')) {
232
                        if (!isset($this->config['context']['ssl']['SNI_enabled'])) {
233
                                $this->config['context']['ssl']['SNI_enabled'] = true;
234
                        }
235
                        if (version_compare(PHP_VERSION, '5.6.0', '>=')) {
236
                                if (empty($this->config['context']['ssl']['peer_name'])) {
237
                                        $this->config['context']['ssl']['peer_name'] = $host;
238
                                }
239
                        } else {
240
                                if (empty($this->config['context']['ssl']['SNI_server_name'])) {
241
                                        $this->config['context']['ssl']['SNI_server_name'] = $host;
242
                                }
243
                        }
244
                }
245
                if (empty($this->config['context']['ssl']['cafile'])) {
246
                        $this->config['context']['ssl']['cafile'] = CAKE . 'Config' . DS . 'cacert.pem';
247
                }
248
                if (!empty($this->config['context']['ssl']['verify_host'])) {
249
                        $this->config['context']['ssl']['CN_match'] = $host;
250
                }
251
                unset($this->config['context']['ssl']['verify_host']);
252
        }
253

    
254
/**
255
 * socket_stream_client() does not populate errNum, or $errStr when there are
256
 * connection errors, as in the case of SSL verification failure.
257
 *
258
 * Instead we need to handle those errors manually.
259
 *
260
 * @param int $code Code.
261
 * @param string $message Message.
262
 * @return void
263
 */
264
        protected function _connectionErrorHandler($code, $message) {
265
                $this->_connectionErrors[] = $message;
266
        }
267

    
268
/**
269
 * Gets the connection context.
270
 *
271
 * @return null|array Null when there is no connection, an array when there is.
272
 */
273
        public function context() {
274
                if (!$this->connection) {
275
                        return null;
276
                }
277
                return stream_context_get_options($this->connection);
278
        }
279

    
280
/**
281
 * Gets the host name of the current connection.
282
 *
283
 * @return string Host name
284
 */
285
        public function host() {
286
                if (Validation::ip($this->config['host'])) {
287
                        return gethostbyaddr($this->config['host']);
288
                }
289
                return gethostbyaddr($this->address());
290
        }
291

    
292
/**
293
 * Gets the IP address of the current connection.
294
 *
295
 * @return string IP address
296
 */
297
        public function address() {
298
                if (Validation::ip($this->config['host'])) {
299
                        return $this->config['host'];
300
                }
301
                return gethostbyname($this->config['host']);
302
        }
303

    
304
/**
305
 * Gets all IP addresses associated with the current connection.
306
 *
307
 * @return array IP addresses
308
 */
309
        public function addresses() {
310
                if (Validation::ip($this->config['host'])) {
311
                        return array($this->config['host']);
312
                }
313
                return gethostbynamel($this->config['host']);
314
        }
315

    
316
/**
317
 * Gets the last error as a string.
318
 *
319
 * @return string|null Last error
320
 */
321
        public function lastError() {
322
                if (!empty($this->lastError)) {
323
                        return $this->lastError['num'] . ': ' . $this->lastError['str'];
324
                }
325
                return null;
326
        }
327

    
328
/**
329
 * Sets the last error.
330
 *
331
 * @param int $errNum Error code
332
 * @param string $errStr Error string
333
 * @return void
334
 */
335
        public function setLastError($errNum, $errStr) {
336
                $this->lastError = array('num' => $errNum, 'str' => $errStr);
337
        }
338

    
339
/**
340
 * Writes data to the socket.
341
 *
342
 * @param string $data The data to write to the socket
343
 * @return bool Success
344
 */
345
        public function write($data) {
346
                if (!$this->connected) {
347
                        if (!$this->connect()) {
348
                                return false;
349
                        }
350
                }
351
                $totalBytes = strlen($data);
352
                for ($written = 0, $rv = 0; $written < $totalBytes; $written += $rv) {
353
                        $rv = fwrite($this->connection, substr($data, $written));
354
                        if ($rv === false || $rv === 0) {
355
                                return $written;
356
                        }
357
                }
358
                return $written;
359
        }
360

    
361
/**
362
 * Reads data from the socket. Returns false if no data is available or no connection could be
363
 * established.
364
 *
365
 * @param int $length Optional buffer length to read; defaults to 1024
366
 * @return mixed Socket data
367
 */
368
        public function read($length = 1024) {
369
                if (!$this->connected) {
370
                        if (!$this->connect()) {
371
                                return false;
372
                        }
373
                }
374

    
375
                if (!feof($this->connection)) {
376
                        $buffer = fread($this->connection, $length);
377
                        $info = stream_get_meta_data($this->connection);
378
                        if ($info['timed_out']) {
379
                                $this->setLastError(E_WARNING, __d('cake_dev', 'Connection timed out'));
380
                                return false;
381
                        }
382
                        return $buffer;
383
                }
384
                return false;
385
        }
386

    
387
/**
388
 * Disconnects the socket from the current connection.
389
 *
390
 * @return bool Success
391
 */
392
        public function disconnect() {
393
                if (!is_resource($this->connection)) {
394
                        $this->connected = false;
395
                        return true;
396
                }
397
                $this->connected = !fclose($this->connection);
398

    
399
                if (!$this->connected) {
400
                        $this->connection = null;
401
                }
402
                return !$this->connected;
403
        }
404

    
405
/**
406
 * Destructor, used to disconnect from current connection.
407
 */
408
        public function __destruct() {
409
                $this->disconnect();
410
        }
411

    
412
/**
413
 * Resets the state of this Socket instance to it's initial state (before Object::__construct got executed)
414
 *
415
 * @param array $state Array with key and values to reset
416
 * @return bool True on success
417
 */
418
        public function reset($state = null) {
419
                if (empty($state)) {
420
                        static $initalState = array();
421
                        if (empty($initalState)) {
422
                                $initalState = get_class_vars(__CLASS__);
423
                        }
424
                        $state = $initalState;
425
                }
426

    
427
                foreach ($state as $property => $value) {
428
                        $this->{$property} = $value;
429
                }
430
                return true;
431
        }
432

    
433
/**
434
 * Encrypts current stream socket, using one of the defined encryption methods.
435
 *
436
 * @param string $type Type which can be one of 'sslv2', 'sslv3', 'sslv23' or 'tls'.
437
 * @param string $clientOrServer Can be one of 'client', 'server'. Default is 'client'.
438
 * @param bool $enable Enable or disable encryption. Default is true (enable)
439
 * @return bool True on success
440
 * @throws InvalidArgumentException When an invalid encryption scheme is chosen.
441
 * @throws SocketException When attempting to enable SSL/TLS fails.
442
 * @see stream_socket_enable_crypto
443
 */
444
        public function enableCrypto($type, $clientOrServer = 'client', $enable = true) {
445
                if (!array_key_exists($type . '_' . $clientOrServer, $this->_encryptMethods)) {
446
                        throw new InvalidArgumentException(__d('cake_dev', 'Invalid encryption scheme chosen'));
447
                }
448
                $enableCryptoResult = false;
449
                try {
450
                        $enableCryptoResult = stream_socket_enable_crypto($this->connection, $enable,
451
                                $this->_encryptMethods[$type . '_' . $clientOrServer]);
452
                } catch (Exception $e) {
453
                        $this->setLastError(null, $e->getMessage());
454
                        throw new SocketException($e->getMessage());
455
                }
456
                if ($enableCryptoResult === true) {
457
                        $this->encrypted = $enable;
458
                        return true;
459
                }
460
                $errorMessage = __d('cake_dev', 'Unable to perform enableCrypto operation on CakeSocket');
461
                $this->setLastError(null, $errorMessage);
462
                throw new SocketException($errorMessage);
463
        }
464
}