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:         );
144:         foreach ($conditionalCrypto as $key => $const) {
145:             if (defined($const)) {
146:                 $this->_encryptMethods[$key] = constant($const);
147:             }
148:         }
149: 
150:         
151:         if (isset($this->_encryptMethods['tlsv1_2_client'])) {
152:             $this->_encryptMethods['tls_client'] = STREAM_CRYPTO_METHOD_TLS_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
153:         }
154:         if (isset($this->_encryptMethods['tlsv1_2_server'])) {
155:             $this->_encryptMethods['tls_server'] = STREAM_CRYPTO_METHOD_TLS_SERVER | STREAM_CRYPTO_METHOD_TLSv1_1_SERVER | STREAM_CRYPTO_METHOD_TLSv1_2_SERVER;
156:         }
157:         
158:     }
159: 
160: 161: 162: 163: 164: 165: 
166:     public function connect() {
167:         if ($this->connection) {
168:             $this->disconnect();
169:         }
170: 
171:         $hasProtocol = strpos($this->config['host'], '://') !== false;
172:         if ($hasProtocol) {
173:             list($this->config['protocol'], $this->config['host']) = explode('://', $this->config['host']);
174:         }
175:         $scheme = null;
176:         if (!empty($this->config['protocol'])) {
177:             $scheme = $this->config['protocol'] . '://';
178:         }
179:         if (!empty($this->config['proxy'])) {
180:             $scheme = 'tcp://';
181:         }
182: 
183:         $host = $this->config['host'];
184:         if (isset($this->config['request']['uri']['host'])) {
185:             $host = $this->config['request']['uri']['host'];
186:         }
187:         $this->_setSslContext($host);
188: 
189:         if (!empty($this->config['context'])) {
190:             $context = stream_context_create($this->config['context']);
191:         } else {
192:             $context = stream_context_create();
193:         }
194: 
195:         $connectAs = STREAM_CLIENT_CONNECT;
196:         if ($this->config['persistent']) {
197:             $connectAs |= STREAM_CLIENT_PERSISTENT;
198:         }
199: 
200:         set_error_handler(array($this, '_connectionErrorHandler'));
201:         $this->connection = stream_socket_client(
202:             $scheme . $this->config['host'] . ':' . $this->config['port'],
203:             $errNum,
204:             $errStr,
205:             $this->config['timeout'],
206:             $connectAs,
207:             $context
208:         );
209:         restore_error_handler();
210: 
211:         if (!empty($errNum) || !empty($errStr)) {
212:             $this->setLastError($errNum, $errStr);
213:             throw new SocketException($errStr, $errNum);
214:         }
215: 
216:         if (!$this->connection && $this->_connectionErrors) {
217:             $message = implode("\n", $this->_connectionErrors);
218:             throw new SocketException($message, E_WARNING);
219:         }
220: 
221:         $this->connected = is_resource($this->connection);
222:         if ($this->connected) {
223:             stream_set_timeout($this->connection, $this->config['timeout']);
224: 
225:             if (!empty($this->config['request']) &&
226:                 $this->config['request']['uri']['scheme'] === 'https' &&
227:                 !empty($this->config['proxy'])
228:             ) {
229:                 $req = array();
230:                 $req[] = 'CONNECT ' . $this->config['request']['uri']['host'] . ':' .
231:                     $this->config['request']['uri']['port'] . ' HTTP/1.1';
232:                 $req[] = 'Host: ' . $this->config['host'];
233:                 $req[] = 'User-Agent: php proxy';
234:                 if (!empty($this->config['proxyauth'])) {
235:                     $req[] = 'Proxy-Authorization: ' . $this->config['proxyauth'];
236:                 }
237: 
238:                 fwrite($this->connection, implode("\r\n", $req) . "\r\n\r\n");
239: 
240:                 while (!feof($this->connection)) {
241:                     $s = rtrim(fgets($this->connection, 4096));
242:                     if (preg_match('/^$/', $s)) {
243:                         break;
244:                     }
245:                 }
246: 
247:                 $this->enableCrypto($this->config['cryptoType'], 'client');
248:             }
249:         }
250:         return $this->connected;
251:     }
252: 
253: 254: 255: 256: 257: 258: 
259:     protected function _setSslContext($host) {
260:         foreach ($this->config as $key => $value) {
261:             if (substr($key, 0, 4) !== 'ssl_') {
262:                 continue;
263:             }
264:             $contextKey = substr($key, 4);
265:             if (empty($this->config['context']['ssl'][$contextKey])) {
266:                 $this->config['context']['ssl'][$contextKey] = $value;
267:             }
268:             unset($this->config[$key]);
269:         }
270:         if (version_compare(PHP_VERSION, '5.3.2', '>=')) {
271:             if (!isset($this->config['context']['ssl']['SNI_enabled'])) {
272:                 $this->config['context']['ssl']['SNI_enabled'] = true;
273:             }
274:             if (version_compare(PHP_VERSION, '5.6.0', '>=')) {
275:                 if (empty($this->config['context']['ssl']['peer_name'])) {
276:                     $this->config['context']['ssl']['peer_name'] = $host;
277:                 }
278:             } else {
279:                 if (empty($this->config['context']['ssl']['SNI_server_name'])) {
280:                     $this->config['context']['ssl']['SNI_server_name'] = $host;
281:                 }
282:             }
283:         }
284:         if (empty($this->config['context']['ssl']['cafile'])) {
285:             $this->config['context']['ssl']['cafile'] = CAKE . 'Config' . DS . 'cacert.pem';
286:         }
287:         if (!empty($this->config['context']['ssl']['verify_host'])) {
288:             $this->config['context']['ssl']['CN_match'] = $host;
289:         }
290:         unset($this->config['context']['ssl']['verify_host']);
291:     }
292: 
293: 294: 295: 296: 297: 298: 299: 300: 301: 302: 
303:     protected function _connectionErrorHandler($code, $message) {
304:         $this->_connectionErrors[] = $message;
305:     }
306: 
307: 308: 309: 310: 311: 
312:     public function context() {
313:         if (!$this->connection) {
314:             return null;
315:         }
316:         return stream_context_get_options($this->connection);
317:     }
318: 
319: 320: 321: 322: 323: 
324:     public function host() {
325:         if (Validation::ip($this->config['host'])) {
326:             return gethostbyaddr($this->config['host']);
327:         }
328:         return gethostbyaddr($this->address());
329:     }
330: 
331: 332: 333: 334: 335: 
336:     public function address() {
337:         if (Validation::ip($this->config['host'])) {
338:             return $this->config['host'];
339:         }
340:         return gethostbyname($this->config['host']);
341:     }
342: 
343: 344: 345: 346: 347: 
348:     public function addresses() {
349:         if (Validation::ip($this->config['host'])) {
350:             return array($this->config['host']);
351:         }
352:         return gethostbynamel($this->config['host']);
353:     }
354: 
355: 356: 357: 358: 359: 
360:     public function lastError() {
361:         if (!empty($this->lastError)) {
362:             return $this->lastError['num'] . ': ' . $this->lastError['str'];
363:         }
364:         return null;
365:     }
366: 
367: 368: 369: 370: 371: 372: 373: 
374:     public function setLastError($errNum, $errStr) {
375:         $this->lastError = array('num' => $errNum, 'str' => $errStr);
376:     }
377: 
378: 379: 380: 381: 382: 383: 
384:     public function write($data) {
385:         if (!$this->connected) {
386:             if (!$this->connect()) {
387:                 return false;
388:             }
389:         }
390:         $totalBytes = strlen($data);
391:         for ($written = 0, $rv = 0; $written < $totalBytes; $written += $rv) {
392:             $rv = fwrite($this->connection, substr($data, $written));
393:             if ($rv === false || $rv === 0) {
394:                 return $written;
395:             }
396:         }
397:         return $written;
398:     }
399: 
400: 401: 402: 403: 404: 405: 406: 
407:     public function read($length = 1024) {
408:         if (!$this->connected) {
409:             if (!$this->connect()) {
410:                 return false;
411:             }
412:         }
413: 
414:         if (!feof($this->connection)) {
415:             $buffer = fread($this->connection, $length);
416:             $info = stream_get_meta_data($this->connection);
417:             if ($info['timed_out']) {
418:                 $this->setLastError(E_WARNING, __d('cake_dev', 'Connection timed out'));
419:                 return false;
420:             }
421:             return $buffer;
422:         }
423:         return false;
424:     }
425: 
426: 427: 428: 429: 430: 
431:     public function disconnect() {
432:         if (!is_resource($this->connection)) {
433:             $this->connected = false;
434:             return true;
435:         }
436:         $this->connected = !fclose($this->connection);
437: 
438:         if (!$this->connected) {
439:             $this->connection = null;
440:         }
441:         return !$this->connected;
442:     }
443: 
444: 445: 446: 
447:     public function __destruct() {
448:         $this->disconnect();
449:     }
450: 
451: 452: 453: 454: 455: 456: 
457:     public function reset($state = null) {
458:         if (empty($state)) {
459:             static $initalState = array();
460:             if (empty($initalState)) {
461:                 $initalState = get_class_vars(__CLASS__);
462:             }
463:             $state = $initalState;
464:         }
465: 
466:         foreach ($state as $property => $value) {
467:             $this->{$property} = $value;
468:         }
469:         return true;
470:     }
471: 
472: 473: 474: 475: 476: 477: 478: 479: 480: 481: 482: 
483:     public function enableCrypto($type, $clientOrServer = 'client', $enable = true) {
484:         if (!array_key_exists($type . '_' . $clientOrServer, $this->_encryptMethods)) {
485:             throw new InvalidArgumentException(__d('cake_dev', 'Invalid encryption scheme chosen'));
486:         }
487:         $enableCryptoResult = false;
488:         try {
489:             $enableCryptoResult = stream_socket_enable_crypto($this->connection, $enable,
490:                 $this->_encryptMethods[$type . '_' . $clientOrServer]);
491:         } catch (Exception $e) {
492:             $this->setLastError(null, $e->getMessage());
493:             throw new SocketException($e->getMessage());
494:         }
495:         if ($enableCryptoResult === true) {
496:             $this->encrypted = $enable;
497:             return true;
498:         }
499:         $errorMessage = __d('cake_dev', 'Unable to perform enableCrypto operation on CakeSocket');
500:         $this->setLastError(null, $errorMessage);
501:         throw new SocketException($errorMessage);
502:     }
503: }
504: