1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 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: 26: 27: 28:
29: class Socket
30: {
31: use InstanceConfigTrait;
32:
33: 34: 35: 36: 37:
38: public $description = 'Remote DataSource Network Socket Interface';
39:
40: 41: 42: 43: 44:
45: protected $_defaultConfig = [
46: 'persistent' => false,
47: 'host' => 'localhost',
48: 'protocol' => 'tcp',
49: 'port' => 80,
50: 'timeout' => 30
51: ];
52:
53: 54: 55: 56: 57:
58: public $connection;
59:
60: 61: 62: 63: 64:
65: public $connected = false;
66:
67: 68: 69: 70: 71:
72: public $lastError = [];
73:
74: 75: 76: 77: 78:
79: public $encrypted = false;
80:
81: 82: 83: 84: 85: 86: 87: 88:
89: protected $_encryptMethods = [
90:
91:
92: 'sslv2_client' => STREAM_CRYPTO_METHOD_SSLv2_CLIENT,
93:
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:
101: 'sslv2_server' => STREAM_CRYPTO_METHOD_SSLv2_SERVER,
102:
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:
110: ];
111:
112: 113: 114: 115: 116: 117:
118: protected $_connectionErrors = [];
119:
120: 121: 122: 123: 124: 125:
126: public function __construct(array $config = [])
127: {
128: $this->setConfig($config);
129: }
130:
131: 132: 133: 134: 135: 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) {
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: 200: 201: 202: 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: 235: 236: 237: 238: 239: 240: 241: 242:
243: protected function _connectionErrorHandler($code, $message)
244: {
245: $this->_connectionErrors[] = $message;
246: }
247:
248: 249: 250: 251: 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: 264: 265: 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: 278: 279: 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: 292: 293: 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: 306: 307: 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: 320: 321: 322: 323: 324:
325: public function setLastError($errNum, $errStr)
326: {
327: $this->lastError = ['num' => $errNum, 'str' => $errStr];
328: }
329:
330: 331: 332: 333: 334: 335: 336: 337: 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: 359: 360: 361: 362: 363: 364: 365: 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: 390: 391: 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: 411:
412: public function __destruct()
413: {
414: $this->disconnect();
415: }
416:
417: 418: 419: 420: 421: 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: 442: 443: 444: 445: 446: 447: 448: 449: 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:
459:
460:
461:
462: if (version_compare(PHP_VERSION, '5.6.7', '>=')) {
463: if ($method == STREAM_CRYPTO_METHOD_TLS_CLIENT) {
464:
465: $method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
466:
467: }
468: if ($method == STREAM_CRYPTO_METHOD_TLS_SERVER) {
469:
470: $method |= STREAM_CRYPTO_METHOD_TLSv1_1_SERVER | STREAM_CRYPTO_METHOD_TLSv1_2_SERVER;
471:
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: