1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
15: namespace Cake\Network;
16:
17: use Cake\Core\InstanceConfigTrait;
18: use Cake\Network\Exception\SocketException;
19: use Cake\Validation\Validation;
20: use Exception;
21: use InvalidArgumentException;
22:
23: 24: 25: 26: 27:
28: class Socket
29: {
30: use InstanceConfigTrait;
31:
32: 33: 34: 35: 36:
37: public $description = 'Remote DataSource Network Socket Interface';
38:
39: 40: 41: 42: 43:
44: protected $_defaultConfig = [
45: 'persistent' => false,
46: 'host' => 'localhost',
47: 'protocol' => 'tcp',
48: 'port' => 80,
49: 'timeout' => 30
50: ];
51:
52: 53: 54: 55: 56:
57: public $connection;
58:
59: 60: 61: 62: 63:
64: public $connected = false;
65:
66: 67: 68: 69: 70:
71: public $lastError = [];
72:
73: 74: 75: 76: 77:
78: public $encrypted = false;
79:
80: 81: 82: 83: 84: 85: 86: 87:
88: protected $_encryptMethods = [
89:
90:
91: 'sslv2_client' => STREAM_CRYPTO_METHOD_SSLv2_CLIENT,
92:
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: 'tlsv10_client' => STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT,
97: 'tlsv11_client' => STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT,
98: 'tlsv12_client' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT,
99:
100: 'sslv2_server' => STREAM_CRYPTO_METHOD_SSLv2_SERVER,
101:
102: 'sslv3_server' => STREAM_CRYPTO_METHOD_SSLv3_SERVER,
103: 'sslv23_server' => STREAM_CRYPTO_METHOD_SSLv23_SERVER,
104: 'tls_server' => STREAM_CRYPTO_METHOD_TLS_SERVER,
105: 'tlsv10_server' => STREAM_CRYPTO_METHOD_TLSv1_0_SERVER,
106: 'tlsv11_server' => STREAM_CRYPTO_METHOD_TLSv1_1_SERVER,
107: 'tlsv12_server' => STREAM_CRYPTO_METHOD_TLSv1_2_SERVER
108:
109: ];
110:
111: 112: 113: 114: 115: 116:
117: protected $_connectionErrors = [];
118:
119: 120: 121: 122: 123: 124:
125: public function __construct(array $config = [])
126: {
127: $this->setConfig($config);
128: }
129:
130: 131: 132: 133: 134: 135:
136: public function connect()
137: {
138: if ($this->connection) {
139: $this->disconnect();
140: }
141:
142: $hasProtocol = strpos($this->_config['host'], '://') !== false;
143: if ($hasProtocol) {
144: list($this->_config['protocol'], $this->_config['host']) = explode('://', $this->_config['host']);
145: }
146: $scheme = null;
147: if (!empty($this->_config['protocol'])) {
148: $scheme = $this->_config['protocol'] . '://';
149: }
150:
151: $this->_setSslContext($this->_config['host']);
152: if (!empty($this->_config['context'])) {
153: $context = stream_context_create($this->_config['context']);
154: } else {
155: $context = stream_context_create();
156: }
157:
158: $connectAs = STREAM_CLIENT_CONNECT;
159: if ($this->_config['persistent']) {
160: $connectAs |= STREAM_CLIENT_PERSISTENT;
161: }
162:
163: set_error_handler([$this, '_connectionErrorHandler']);
164: $this->connection = stream_socket_client(
165: $scheme . $this->_config['host'] . ':' . $this->_config['port'],
166: $errNum,
167: $errStr,
168: $this->_config['timeout'],
169: $connectAs,
170: $context
171: );
172: restore_error_handler();
173:
174: if (!empty($errNum) || !empty($errStr)) {
175: $this->setLastError($errNum, $errStr);
176: throw new SocketException($errStr, $errNum);
177: }
178:
179: if (!$this->connection && $this->_connectionErrors) {
180: $message = implode("\n", $this->_connectionErrors);
181: throw new SocketException($message, E_WARNING);
182: }
183:
184: $this->connected = is_resource($this->connection);
185: if ($this->connected) {
186: stream_set_timeout($this->connection, $this->_config['timeout']);
187: }
188:
189: return $this->connected;
190: }
191:
192: 193: 194: 195: 196: 197:
198: protected function _setSslContext($host)
199: {
200: foreach ($this->_config as $key => $value) {
201: if (substr($key, 0, 4) !== 'ssl_') {
202: continue;
203: }
204: $contextKey = substr($key, 4);
205: if (empty($this->_config['context']['ssl'][$contextKey])) {
206: $this->_config['context']['ssl'][$contextKey] = $value;
207: }
208: unset($this->_config[$key]);
209: }
210: if (!isset($this->_config['context']['ssl']['SNI_enabled'])) {
211: $this->_config['context']['ssl']['SNI_enabled'] = true;
212: }
213: if (empty($this->_config['context']['ssl']['peer_name'])) {
214: $this->_config['context']['ssl']['peer_name'] = $host;
215: }
216: if (empty($this->_config['context']['ssl']['cafile'])) {
217: $dir = dirname(dirname(__DIR__));
218: $this->_config['context']['ssl']['cafile'] = $dir . DIRECTORY_SEPARATOR .
219: 'config' . DIRECTORY_SEPARATOR . 'cacert.pem';
220: }
221: if (!empty($this->_config['context']['ssl']['verify_host'])) {
222: $this->_config['context']['ssl']['CN_match'] = $host;
223: }
224: unset($this->_config['context']['ssl']['verify_host']);
225: }
226:
227: 228: 229: 230: 231: 232: 233: 234: 235: 236:
237: protected function _connectionErrorHandler($code, $message)
238: {
239: $this->_connectionErrors[] = $message;
240: }
241:
242: 243: 244: 245: 246:
247: public function context()
248: {
249: if (!$this->connection) {
250: return null;
251: }
252:
253: return stream_context_get_options($this->connection);
254: }
255:
256: 257: 258: 259: 260:
261: public function host()
262: {
263: if (Validation::ip($this->_config['host'])) {
264: return gethostbyaddr($this->_config['host']);
265: }
266:
267: return gethostbyaddr($this->address());
268: }
269:
270: 271: 272: 273: 274:
275: public function address()
276: {
277: if (Validation::ip($this->_config['host'])) {
278: return $this->_config['host'];
279: }
280:
281: return gethostbyname($this->_config['host']);
282: }
283:
284: 285: 286: 287: 288:
289: public function addresses()
290: {
291: if (Validation::ip($this->_config['host'])) {
292: return [$this->_config['host']];
293: }
294:
295: return gethostbynamel($this->_config['host']);
296: }
297:
298: 299: 300: 301: 302:
303: public function lastError()
304: {
305: if (!empty($this->lastError)) {
306: return $this->lastError['num'] . ': ' . $this->lastError['str'];
307: }
308:
309: return null;
310: }
311:
312: 313: 314: 315: 316: 317: 318:
319: public function setLastError($errNum, $errStr)
320: {
321: $this->lastError = ['num' => $errNum, 'str' => $errStr];
322: }
323:
324: 325: 326: 327: 328: 329: 330: 331: 332:
333: public function write($data)
334: {
335: if (!$this->connected && !$this->connect()) {
336: return false;
337: }
338: $totalBytes = strlen($data);
339: $written = 0;
340: while ($written < $totalBytes) {
341: $rv = fwrite($this->connection, substr($data, $written));
342: if ($rv === false || $rv === 0) {
343: return $written;
344: }
345: $written += $rv;
346: }
347:
348: return $written;
349: }
350:
351: 352: 353: 354: 355: 356: 357: 358: 359: 360:
361: public function read($length = 1024)
362: {
363: if (!$this->connected && !$this->connect()) {
364: return false;
365: }
366:
367: if (!feof($this->connection)) {
368: $buffer = fread($this->connection, $length);
369: $info = stream_get_meta_data($this->connection);
370: if ($info['timed_out']) {
371: $this->setLastError(E_WARNING, 'Connection timed out');
372:
373: return false;
374: }
375:
376: return $buffer;
377: }
378:
379: return false;
380: }
381:
382: 383: 384: 385: 386:
387: public function disconnect()
388: {
389: if (!is_resource($this->connection)) {
390: $this->connected = false;
391:
392: return true;
393: }
394: $this->connected = !fclose($this->connection);
395:
396: if (!$this->connected) {
397: $this->connection = null;
398: }
399:
400: return !$this->connected;
401: }
402:
403: 404: 405:
406: public function __destruct()
407: {
408: $this->disconnect();
409: }
410:
411: 412: 413: 414: 415: 416:
417: public function reset($state = null)
418: {
419: if (empty($state)) {
420: static $initalState = [];
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:
431: return true;
432: }
433:
434: 435: 436: 437: 438: 439: 440: 441: 442: 443: 444:
445: public function enableCrypto($type, $clientOrServer = 'client', $enable = true)
446: {
447: if (!array_key_exists($type . '_' . $clientOrServer, $this->_encryptMethods)) {
448: throw new InvalidArgumentException('Invalid encryption scheme chosen');
449: }
450: $method = $this->_encryptMethods[$type . '_' . $clientOrServer];
451:
452:
453:
454:
455:
456: if (version_compare(PHP_VERSION, '5.6.7', '>=')) {
457: if ($method == STREAM_CRYPTO_METHOD_TLS_CLIENT) {
458:
459: $method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
460:
461: }
462: if ($method == STREAM_CRYPTO_METHOD_TLS_SERVER) {
463:
464: $method |= STREAM_CRYPTO_METHOD_TLSv1_1_SERVER | STREAM_CRYPTO_METHOD_TLSv1_2_SERVER;
465:
466: }
467: }
468:
469: try {
470: $enableCryptoResult = stream_socket_enable_crypto($this->connection, $enable, $method);
471: } catch (Exception $e) {
472: $this->setLastError(null, $e->getMessage());
473: throw new SocketException($e->getMessage(), null, $e);
474: }
475: if ($enableCryptoResult === true) {
476: $this->encrypted = $enable;
477:
478: return true;
479: }
480: $errorMessage = 'Unable to perform enableCrypto operation on the current socket';
481: $this->setLastError(null, $errorMessage);
482: throw new SocketException($errorMessage);
483: }
484: }
485: