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