1<?php 2 3/** 4 * This file is part of the FreeDSx LDAP package. 5 * 6 * (c) Chad Sikorra <Chad.Sikorra@gmail.com> 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 FreeDSx\Ldap\Protocol; 13 14use FreeDSx\Asn1\Exception\EncoderException; 15use FreeDSx\Ldap\Control\Control; 16use FreeDSx\Ldap\Control\ControlBag; 17use FreeDSx\Ldap\Entry\Entry; 18use FreeDSx\Ldap\Exception\BindException; 19use FreeDSx\Ldap\Exception\ConnectionException; 20use FreeDSx\Ldap\Exception\OperationException; 21use FreeDSx\Ldap\Exception\ProtocolException; 22use FreeDSx\Ldap\Exception\ReferralException; 23use FreeDSx\Ldap\Exception\UnsolicitedNotificationException; 24use FreeDSx\Ldap\Operation\Request\RequestInterface; 25use FreeDSx\Ldap\Operation\Response\ExtendedResponse; 26use FreeDSx\Ldap\Operation\Response\SearchResponse; 27use FreeDSx\Ldap\Operations; 28use FreeDSx\Ldap\Protocol\ClientProtocolHandler\ClientProtocolContext; 29use FreeDSx\Ldap\Protocol\Factory\ClientProtocolHandlerFactory; 30use FreeDSx\Ldap\Protocol\Queue\ClientQueue; 31use FreeDSx\Sasl\Exception\SaslException; 32use FreeDSx\Socket\Exception\ConnectionException as SocketException; 33use FreeDSx\Socket\SocketPool; 34 35/** 36 * Handles client specific protocol communication details. 37 * 38 * @author Chad Sikorra <Chad.Sikorra@gmail.com> 39 */ 40class ClientProtocolHandler 41{ 42 public const ROOTDSE_ATTRIBUTES = [ 43 'supportedSaslMechanisms', 44 'supportedControl', 45 'supportedLDAPVersion', 46 ]; 47 48 /** 49 * @var SocketPool 50 */ 51 protected $pool; 52 53 /** 54 * @var ClientQueue|null 55 */ 56 protected $queue; 57 58 /** 59 * @var array 60 */ 61 protected $options; 62 63 /** 64 * @var ControlBag 65 */ 66 protected $controls; 67 68 /** 69 * @var ClientProtocolHandlerFactory 70 */ 71 protected $protocolHandlerFactory; 72 73 /** 74 * @var null|Entry 75 */ 76 protected $rootDse; 77 78 public function __construct(array $options, ClientQueue $queue = null, SocketPool $pool = null, ClientProtocolHandlerFactory $clientProtocolHandlerFactory = null) 79 { 80 $this->options = $options; 81 $this->pool = $pool ?? new SocketPool($options); 82 $this->protocolHandlerFactory = $clientProtocolHandlerFactory ?? new ClientProtocolHandlerFactory(); 83 $this->controls = new ControlBag(); 84 $this->queue = $queue; 85 } 86 87 /** 88 * @return ControlBag 89 */ 90 public function controls(): ControlBag 91 { 92 return $this->controls; 93 } 94 95 /** 96 * Make a single search request to fetch the RootDSE. Handle the various errors that could occur. 97 * 98 * @param bool $reload 99 * @return Entry 100 * @throws ConnectionException 101 * @throws OperationException 102 * @throws SocketException 103 * @throws UnsolicitedNotificationException 104 * @throws EncoderException 105 * @throws BindException 106 * @throws ProtocolException 107 * @throws ReferralException 108 * @throws SaslException 109 */ 110 public function fetchRootDse(bool $reload = false): Entry 111 { 112 if ($reload === false && $this->rootDse !== null) { 113 return $this->rootDse; 114 } 115 $message = $this->send(Operations::read('', ...self::ROOTDSE_ATTRIBUTES)); 116 if ($message === null) { 117 throw new OperationException('Expected a search response for the RootDSE. None received.'); 118 } 119 120 $searchResponse = $message->getResponse(); 121 if (!$searchResponse instanceof SearchResponse) { 122 throw new OperationException('Expected a search response for the RootDSE. None received.'); 123 } 124 125 $entry = $searchResponse->getEntries()->first(); 126 if ($entry === null) { 127 throw new OperationException('Expected a single entry for the RootDSE. None received.'); 128 } 129 $this->rootDse = $entry; 130 131 return $entry; 132 } 133 134 /** 135 * @param RequestInterface $request 136 * @param Control ...$controls 137 * @return LdapMessageResponse|null 138 * @throws ConnectionException 139 * @throws OperationException 140 * @throws SocketException 141 * @throws UnsolicitedNotificationException 142 * @throws EncoderException 143 * @throws BindException 144 * @throws ProtocolException 145 * @throws ReferralException 146 * @throws SaslException 147 */ 148 public function send(RequestInterface $request, Control ...$controls): ?LdapMessageResponse 149 { 150 try { 151 $context = new ClientProtocolContext( 152 $request, 153 $controls, 154 $this, 155 $this->queue(), 156 $this->options 157 ); 158 159 $messageFrom = $this->protocolHandlerFactory->forRequest($request)->handleRequest($context); 160 $messageTo = $context->messageToSend(); 161 if ($messageFrom !== null) { 162 $messageFrom = $this->protocolHandlerFactory->forResponse($messageTo->getRequest(), $messageFrom->getResponse())->handleResponse( 163 $messageTo, 164 $messageFrom, 165 $this->queue(), 166 $this->options 167 ); 168 } 169 170 return $messageFrom; 171 } catch (UnsolicitedNotificationException $exception) { 172 if ($exception->getOid() === ExtendedResponse::OID_NOTICE_OF_DISCONNECTION) { 173 $this->queue()->close(); 174 throw new ConnectionException( 175 sprintf('The remote server has disconnected the session. %s', $exception->getMessage()), 176 $exception->getCode() 177 ); 178 } 179 180 throw $exception; 181 } catch (SocketException $exception) { 182 throw new ConnectionException( 183 $exception->getMessage(), 184 $exception->getCode(), 185 $exception 186 ); 187 } 188 } 189 190 /** 191 * @return bool 192 */ 193 public function isConnected(): bool 194 { 195 return ($this->queue !== null && $this->queue->isConnected()); 196 } 197 198 /** 199 * @throws SocketException 200 */ 201 protected function queue(): ClientQueue 202 { 203 if ($this->queue === null) { 204 $this->queue = new ClientQueue($this->pool); 205 } 206 207 return $this->queue; 208 } 209} 210