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:     );
 49: 
 50:  51:  52:  53:  54: 
 55:     public $config = array();
 56: 
 57:  58:  59:  60:  61: 
 62:     public $connection = null;
 63: 
 64:  65:  66:  67:  68: 
 69:     public $connected = false;
 70: 
 71:  72:  73:  74:  75: 
 76:     public $lastError = array();
 77: 
 78:  79:  80:  81:  82: 
 83:     public $encrypted = false;
 84: 
 85:  86:  87:  88:  89: 
 90:     protected $_encryptMethods = array(
 91:         
 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:         
101:     );
102: 
103: 104: 105: 106: 107: 108: 
109:     protected $_connectionErrors = array();
110: 
111: 112: 113: 114: 115: 116: 
117:     public function __construct($config = array()) {
118:         $this->config = array_merge($this->_baseConfig, $config);
119:     }
120: 
121: 122: 123: 124: 125: 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: 216: 217: 218: 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: 256: 257: 258: 259: 260: 261: 262: 263: 
264:     protected function _connectionErrorHandler($code, $message) {
265:         $this->_connectionErrors[] = $message;
266:     }
267: 
268: 269: 270: 271: 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: 282: 283: 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: 294: 295: 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: 306: 307: 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: 318: 319: 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: 330: 331: 332: 333: 334: 
335:     public function setLastError($errNum, $errStr) {
336:         $this->lastError = array('num' => $errNum, 'str' => $errStr);
337:     }
338: 
339: 340: 341: 342: 343: 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: 363: 364: 365: 366: 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: 389: 390: 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: 407: 
408:     public function __destruct() {
409:         $this->disconnect();
410:     }
411: 
412: 413: 414: 415: 416: 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: 435: 436: 437: 438: 439: 440: 441: 442: 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: }
465: