1<?php
2
3/*
4 * This file is part of MailSo.
5 *
6 * (c) 2014 Usenko Timur
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace MailSo\Net;
13
14/**
15 * @category MailSo
16 * @package Net
17 */
18abstract class NetClient
19{
20	/**
21	 * @var resource
22	 */
23	protected $rConnect;
24
25	/**
26	 * @var bool
27	 */
28	protected $bUnreadBuffer;
29
30	/**
31	 * @var bool
32	 */
33	protected $bRunningCallback;
34
35	/**
36	 * @var string
37	 */
38	protected $sResponseBuffer;
39
40	/**
41	 * @var int
42	 */
43	protected $iSecurityType;
44
45	/**
46	 * @var string
47	 */
48	protected $sConnectedHost;
49
50	/**
51	 * @var int
52	 */
53	protected $iConnectedPort;
54
55	/**
56	 * @var bool
57	 */
58	protected $bSecure;
59
60	/**
61	 * @var int
62	 */
63	protected $iConnectTimeOut;
64
65	/**
66	 * @var int
67	 */
68	protected $iSocketTimeOut;
69
70	/**
71	 * @var int
72	 */
73	protected $iStartConnectTime;
74
75	/**
76	 * @var \MailSo\Log\Logger
77	 */
78	protected $oLogger;
79
80	/**
81	 * @var bool
82	 */
83	public $__AUTOLOGOUT__;
84
85	/**
86	 * @access protected
87	 */
88	protected function __construct()
89	{
90		$this->rConnect = null;
91		$this->bUnreadBuffer = false;
92		$this->bRunningCallback = false;
93		$this->oLogger = null;
94
95		$this->__AUTOLOGOUT__ = true;
96
97		$this->sResponseBuffer = '';
98
99		$this->iSecurityType = \MailSo\Net\Enumerations\ConnectionSecurityType::NONE;
100		$this->sConnectedHost = '';
101		$this->iConnectedPort = 0;
102
103		$this->bSecure = false;
104
105		$this->iConnectTimeOut = 10;
106		$this->iSocketTimeOut = 10;
107
108		$this->Clear();
109	}
110
111	/**
112	 * @return void
113	 */
114	public function __destruct()
115	{
116		try
117		{
118			if ($this->__AUTOLOGOUT__)
119			{
120				$this->LogoutAndDisconnect();
121			}
122			else
123			{
124				$this->Disconnect();
125			}
126		}
127		catch (\Exception $oException) {}
128	}
129
130	/**
131	 * @return void
132	 */
133	public function Clear()
134	{
135		$this->sResponseBuffer = '';
136
137		$this->sConnectedHost = '';
138		$this->iConnectedPort = 0;
139
140		$this->iStartConnectTime = 0;
141		$this->bSecure = false;
142	}
143
144	/**
145	 * @return string
146	 */
147	public function GetConnectedHost()
148	{
149		return $this->sConnectedHost;
150	}
151
152	/**
153	 * @return int
154	 */
155	public function GetConnectedPort()
156	{
157		return $this->iConnectedPort;
158	}
159
160	/**
161	 * @param int $iConnectTimeOut = 10
162	 * @param int $iSocketTimeOut = 10
163	 *
164	 * @return void
165	 */
166	public function SetTimeOuts($iConnectTimeOut = 10, $iSocketTimeOut = 10)
167	{
168		$this->iConnectTimeOut = 5 < $iConnectTimeOut ? $iConnectTimeOut : 5;
169		$this->iSocketTimeOut = 5 < $iSocketTimeOut ? $iSocketTimeOut : 5;
170	}
171
172	/**
173	 * @return resource|null
174	 */
175	public function ConnectionResource()
176	{
177		return $this->rConnect;
178	}
179
180	/**
181	 * @param int $iErrNo
182	 * @param string $sErrStr
183	 * @param string $sErrFile
184	 * @param int $iErrLine
185	 *
186	 * @return bool
187	 */
188	public function capturePhpErrorWithException($iErrNo, $sErrStr, $sErrFile, $iErrLine)
189	{
190		throw new \MailSo\Base\Exceptions\Exception($sErrStr, $iErrNo);
191	}
192
193	/**
194	 * @param string $sServerName
195	 * @param int $iPort
196	 * @param int $iSecurityType = \MailSo\Net\Enumerations\ConnectionSecurityType::AUTO_DETECT
197	 * @param bool $bVerifySsl = false
198	 * @param bool $bAllowSelfSigned = true
199	 * @param string $sClientCert = ''
200	 *
201	 * @return void
202	 *
203	 * @throws \MailSo\Base\Exceptions\InvalidArgumentException
204	 * @throws \MailSo\Net\Exceptions\SocketAlreadyConnectedException
205	 * @throws \MailSo\Net\Exceptions\SocketCanNotConnectToHostException
206	 */
207	public function Connect($sServerName, $iPort,
208		$iSecurityType = \MailSo\Net\Enumerations\ConnectionSecurityType::AUTO_DETECT,
209		$bVerifySsl = false, $bAllowSelfSigned = true,
210		$sClientCert = '')
211	{
212		if (!\MailSo\Base\Validator::NotEmptyString($sServerName, true) || !\MailSo\Base\Validator::PortInt($iPort))
213		{
214			$this->writeLogException(
215				new \MailSo\Base\Exceptions\InvalidArgumentException(),
216				\MailSo\Log\Enumerations\Type::ERROR, true);
217		}
218
219		if ($this->IsConnected())
220		{
221			$this->writeLogException(
222				new Exceptions\SocketAlreadyConnectedException(),
223				\MailSo\Log\Enumerations\Type::ERROR, true);
224		}
225
226		$sServerName = \trim($sServerName);
227
228		$sErrorStr = '';
229		$iErrorNo = 0;
230
231		$this->sConnectedHost = $sServerName;
232		$this->iConnectedPort = $iPort;
233		$this->iSecurityType = $iSecurityType;
234		$this->bSecure = \MailSo\Net\Enumerations\ConnectionSecurityType::UseSSL(
235			$this->iConnectedPort, $this->iSecurityType);
236
237		if (!\preg_match('/^[a-z0-9._]{2,8}:\/\//i', $this->sConnectedHost))
238		{
239			$this->sConnectedHost = ($this->bSecure ? 'ssl://' : 'tcp://').$this->sConnectedHost;
240//			$this->sConnectedHost = ($this->bSecure ? 'ssl://' : '').$this->sConnectedHost;
241		}
242
243		if (!$this->bSecure && \MailSo\Net\Enumerations\ConnectionSecurityType::SSL === $this->iSecurityType)
244		{
245			$this->writeLogException(
246				new \MailSo\Net\Exceptions\SocketUnsuppoterdSecureConnectionException('SSL isn\'t supported: ('.\implode(', ', \stream_get_transports()).')'),
247				\MailSo\Log\Enumerations\Type::ERROR, true);
248		}
249
250		$this->iStartConnectTime = \microtime(true);
251		$this->writeLog('Start connection to "'.$this->sConnectedHost.':'.$this->iConnectedPort.'"',
252			\MailSo\Log\Enumerations\Type::NOTE);
253
254//		$this->rConnect = @\fsockopen($this->sConnectedHost, $this->iConnectedPort,
255//			$iErrorNo, $sErrorStr, $this->iConnectTimeOut);
256
257		$bVerifySsl = !!$bVerifySsl;
258		$bAllowSelfSigned = $bVerifySsl ? !!$bAllowSelfSigned : true;
259		$aStreamContextSettings = array(
260			'ssl' => array(
261				'verify_host' => $bVerifySsl,
262				'verify_peer' => $bVerifySsl,
263				'verify_peer_name' => $bVerifySsl,
264				'allow_self_signed' => $bAllowSelfSigned
265			)
266		);
267
268		if (!empty($sClientCert))
269		{
270			$aStreamContextSettings['ssl']['local_cert'] = $sClientCert;
271		}
272
273		\MailSo\Hooks::Run('Net.NetClient.StreamContextSettings/Filter', array(&$aStreamContextSettings));
274
275		$rStreamContext = \stream_context_create($aStreamContextSettings);
276
277		\set_error_handler(array(&$this, 'capturePhpErrorWithException'));
278
279		try
280		{
281			$this->rConnect = \stream_socket_client($this->sConnectedHost.':'.$this->iConnectedPort,
282				$iErrorNo, $sErrorStr, $this->iConnectTimeOut, STREAM_CLIENT_CONNECT, $rStreamContext);
283		}
284		catch (\Exception $oExc)
285		{
286			$sErrorStr = $oExc->getMessage();
287			$iErrorNo = $oExc->getCode();
288		}
289
290		\restore_error_handler();
291
292		$this->writeLog('Connected ('.(\is_resource($this->rConnect) ? 'success' : 'unsuccess').')',
293			\MailSo\Log\Enumerations\Type::NOTE);
294
295		if (!\is_resource($this->rConnect))
296		{
297			$this->writeLogException(
298				new Exceptions\SocketCanNotConnectToHostException(
299					\MailSo\Base\Utils::ConvertSystemString($sErrorStr), (int) $iErrorNo,
300					'Can\'t connect to host "'.$this->sConnectedHost.':'.$this->iConnectedPort.'"'
301				), \MailSo\Log\Enumerations\Type::NOTICE, true);
302		}
303
304		$this->writeLog((\microtime(true) - $this->iStartConnectTime).' (raw connection)',
305			\MailSo\Log\Enumerations\Type::TIME);
306
307		if ($this->rConnect)
308		{
309			if (\MailSo\Base\Utils::FunctionExistsAndEnabled('stream_set_timeout'))
310			{
311				@\stream_set_timeout($this->rConnect, $this->iSocketTimeOut);
312			}
313		}
314	}
315
316	public function EnableCrypto()
317	{
318		$bError = true;
319		if (\is_resource($this->rConnect) &&
320			\MailSo\Base\Utils::FunctionExistsAndEnabled('stream_socket_enable_crypto'))
321		{
322			switch (true)
323			{
324				case defined('STREAM_CRYPTO_METHOD_ANY_CLIENT') &&
325					@\stream_socket_enable_crypto($this->rConnect, true, STREAM_CRYPTO_METHOD_ANY_CLIENT):
326				case defined('STREAM_CRYPTO_METHOD_TLS_CLIENT') &&
327					@\stream_socket_enable_crypto($this->rConnect, true, STREAM_CRYPTO_METHOD_TLS_CLIENT):
328				case defined('STREAM_CRYPTO_METHOD_SSLv23_CLIENT') &&
329					@\stream_socket_enable_crypto($this->rConnect, true, STREAM_CRYPTO_METHOD_SSLv23_CLIENT):
330					$bError = false;
331					break;
332			}
333		}
334
335		if ($bError)
336		{
337			$this->writeLogException(
338				new \MailSo\Net\Exceptions\Exception('Cannot enable STARTTLS.'),
339				\MailSo\Log\Enumerations\Type::ERROR, true);
340		}
341	}
342
343	/**
344	 * @return void
345	 */
346	public function Disconnect()
347	{
348		if (\is_resource($this->rConnect))
349		{
350			$bResult = \fclose($this->rConnect);
351
352			$this->writeLog('Disconnected from "'.$this->sConnectedHost.':'.$this->iConnectedPort.'" ('.
353				(($bResult) ? 'success' : 'unsuccess').')', \MailSo\Log\Enumerations\Type::NOTE);
354
355			if (0 !== $this->iStartConnectTime)
356			{
357				$this->writeLog((\microtime(true) - $this->iStartConnectTime).' (net session)',
358					\MailSo\Log\Enumerations\Type::TIME);
359
360				$this->iStartConnectTime = 0;
361			}
362
363			$this->rConnect = null;
364		}
365	}
366
367	/**
368	 * @retun void
369	 *
370	 * @throws \MailSo\Net\Exceptions\Exception
371	 */
372	public function LogoutAndDisconnect()
373	{
374		if (\method_exists($this, 'Logout') && !$this->bUnreadBuffer && !$this->bRunningCallback)
375		{
376			$this->Logout();
377		}
378
379		$this->Disconnect();
380	}
381
382	/**
383	 * @param bool $bThrowExceptionOnFalse = false
384	 *
385	 * @return bool
386	 */
387	public function IsConnected($bThrowExceptionOnFalse = false)
388	{
389		$bResult = \is_resource($this->rConnect);
390		if (!$bResult && $bThrowExceptionOnFalse)
391		{
392			$this->writeLogException(
393				new Exceptions\SocketConnectionDoesNotAvailableException(),
394				\MailSo\Log\Enumerations\Type::ERROR, true);
395		}
396
397		return $bResult;
398	}
399
400	/**
401	 * @return void
402	 *
403	 * @throws \MailSo\Net\Exceptions\SocketConnectionDoesNotAvailableException
404	 */
405	public function IsConnectedWithException()
406	{
407		$this->IsConnected(true);
408	}
409
410	/**
411	 * @return array|bool
412	 */
413	public function StreamContextParams()
414	{
415		return \is_resource($this->rConnect) && \MailSo\Base\Utils::FunctionExistsAndEnabled('stream_context_get_options')
416			? \stream_context_get_params($this->rConnect) : false;
417	}
418
419	/**
420	 * @param string $sRaw
421	 * @param bool $bWriteToLog = true
422	 * @param string $sFakeRaw = ''
423	 *
424	 * @return void
425	 *
426	 * @throws \MailSo\Net\Exceptions\SocketConnectionDoesNotAvailableException
427	 * @throws \MailSo\Net\Exceptions\SocketWriteException
428	 */
429	protected function sendRaw($sRaw, $bWriteToLog = true, $sFakeRaw = '')
430	{
431		if ($this->bUnreadBuffer)
432		{
433			$this->writeLogException(
434				new Exceptions\SocketUnreadBufferException(),
435				\MailSo\Log\Enumerations\Type::ERROR, true);
436		}
437
438		$bFake = 0 < \strlen($sFakeRaw);
439		$sRaw .= "\r\n";
440
441		if ($this->oLogger && $this->oLogger->IsShowSecter())
442		{
443			$bFake = false;
444		}
445
446		if ($bFake)
447		{
448			$sFakeRaw .= "\r\n";
449		}
450
451		$mResult = @\fwrite($this->rConnect, $sRaw);
452		if (false === $mResult)
453		{
454			$this->IsConnected(true);
455
456			$this->writeLogException(
457				new Exceptions\SocketWriteException(),
458				\MailSo\Log\Enumerations\Type::ERROR, true);
459		}
460		else
461		{
462			\MailSo\Base\Loader::IncStatistic('NetWrite', $mResult);
463
464			if ($bWriteToLog)
465			{
466				$this->writeLogWithCrlf('> '.($bFake ? $sFakeRaw : $sRaw), //.' ['.$iWriteSize.']',
467					$bFake ? \MailSo\Log\Enumerations\Type::SECURE : \MailSo\Log\Enumerations\Type::INFO);
468			}
469		}
470	}
471
472	/**
473	 * @param mixed $mReadLen = null
474	 * @param bool $bForceLogin = false
475	 *
476	 * @return void
477	 *
478	 * @throws \MailSo\Net\Exceptions\SocketConnectionDoesNotAvailableException
479	 * @throws \MailSo\Net\Exceptions\SocketReadException
480	 */
481	protected function getNextBuffer($mReadLen = null, $bForceLogin = false)
482	{
483		if (null === $mReadLen)
484		{
485			$this->sResponseBuffer = @\fgets($this->rConnect);
486		}
487		else
488		{
489			$this->sResponseBuffer = '';
490			$iRead = $mReadLen;
491			while (0 < $iRead)
492			{
493				$sAddRead = @\fread($this->rConnect, $iRead);
494				if (false === $sAddRead)
495				{
496					$this->sResponseBuffer = false;
497					break;
498				}
499
500				$this->sResponseBuffer .= $sAddRead;
501				$iRead -= \strlen($sAddRead);
502			}
503		}
504
505		if (false === $this->sResponseBuffer)
506		{
507			$this->IsConnected(true);
508			$this->bUnreadBuffer = true;
509
510			$aSocketStatus = @\stream_get_meta_data($this->rConnect);
511			if (isset($aSocketStatus['timed_out']) && $aSocketStatus['timed_out'])
512			{
513				$this->writeLogException(
514					new Exceptions\SocketReadTimeoutException(),
515						\MailSo\Log\Enumerations\Type::ERROR, true);
516			}
517			else
518			{
519				$this->writeLog('Stream Meta: '.
520					\print_r($aSocketStatus, true), \MailSo\Log\Enumerations\Type::ERROR);
521
522				$this->writeLogException(
523					new Exceptions\SocketReadException(),
524						\MailSo\Log\Enumerations\Type::ERROR, true);
525			}
526		}
527		else
528		{
529			$iReadedLen = \strlen($this->sResponseBuffer);
530			if (null === $mReadLen || $bForceLogin)
531			{
532				$iLimit = 5000; // 5KB
533				if ($iLimit < $iReadedLen)
534				{
535					$this->writeLogWithCrlf('[cutted:'.$iReadedLen.'] < '.\substr($this->sResponseBuffer, 0, $iLimit).'...',
536						\MailSo\Log\Enumerations\Type::INFO);
537				}
538				else
539				{
540					$this->writeLogWithCrlf('< '.$this->sResponseBuffer, //.' ['.$iReadedLen.']',
541						\MailSo\Log\Enumerations\Type::INFO);
542				}
543			}
544			else
545			{
546				$this->writeLog('Received '.$iReadedLen.'/'.$mReadLen.' bytes.',
547					\MailSo\Log\Enumerations\Type::INFO);
548			}
549
550			\MailSo\Base\Loader::IncStatistic('NetRead', $iReadedLen);
551		}
552	}
553
554	/**
555	 * @return string
556	 */
557	protected function getLogName()
558	{
559		return 'NET';
560	}
561
562	/**
563	 * @param string $sDesc
564	 * @param int $iDescType = \MailSo\Log\Enumerations\Type::INFO
565	 *
566	 * @return void
567	 */
568	protected function writeLog($sDesc, $iDescType = \MailSo\Log\Enumerations\Type::INFO, $bDiplayCrLf = false)
569	{
570		if ($this->oLogger)
571		{
572			$this->oLogger->Write($sDesc, $iDescType, $this->getLogName(), true, $bDiplayCrLf);
573		}
574	}
575
576	/**
577	 * @param string $sDesc
578	 * @param int $iDescType = \MailSo\Log\Enumerations\Type::INFO
579	 *
580	 * @return void
581	 */
582	protected function writeLogWithCrlf($sDesc, $iDescType = \MailSo\Log\Enumerations\Type::INFO)
583	{
584		$this->writeLog($sDesc, $iDescType, true);
585	}
586
587	/**
588	 * @param \Exception $oException
589	 * @param int $iDescType = \MailSo\Log\Enumerations\Type::NOTICE
590	 * @param bool $bThrowException = false
591	 *
592	 * @return void
593	 */
594	protected function writeLogException($oException,
595		$iDescType = \MailSo\Log\Enumerations\Type::NOTICE, $bThrowException = false)
596	{
597		if ($this->oLogger)
598		{
599			if ($oException instanceof Exceptions\SocketCanNotConnectToHostException)
600			{
601				$this->oLogger->Write('Socket: ['.$oException->getSocketCode().'] '.$oException->getSocketMessage(), $iDescType, $this->getLogName());
602			}
603
604			$this->oLogger->WriteException($oException, $iDescType, $this->getLogName());
605		}
606
607		if ($bThrowException)
608		{
609			throw $oException;
610		}
611	}
612
613	/**
614	 * @param \MailSo\Log\Logger $oLogger
615	 *
616	 * @return void
617	 *
618	 * @throws \MailSo\Base\Exceptions\InvalidArgumentException
619	 */
620	public function SetLogger($oLogger)
621	{
622		if (!($oLogger instanceof \MailSo\Log\Logger))
623		{
624			throw new \MailSo\Base\Exceptions\InvalidArgumentException();
625		}
626
627		$this->oLogger = $oLogger;
628	}
629
630	/**
631	 * @return \MailSo\Log\Logger|null
632	 */
633	public function Logger()
634	{
635		return $this->oLogger;
636	}
637}
638