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