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 Exception; 15use FreeDSx\Asn1\Exception\EncoderException; 16use FreeDSx\Ldap\Exception\OperationException; 17use FreeDSx\Ldap\Exception\ProtocolException; 18use FreeDSx\Ldap\Exception\RuntimeException; 19use FreeDSx\Ldap\Operation\Response\ExtendedResponse; 20use FreeDSx\Ldap\Operation\ResultCode; 21use FreeDSx\Ldap\Protocol\Factory\ResponseFactory; 22use FreeDSx\Ldap\Protocol\Factory\ServerBindHandlerFactory; 23use FreeDSx\Ldap\Protocol\Factory\ServerProtocolHandlerFactory; 24use FreeDSx\Ldap\Protocol\Queue\ServerQueue; 25use FreeDSx\Ldap\Server\HandlerFactoryInterface; 26use FreeDSx\Ldap\Server\RequestHistory; 27use FreeDSx\Ldap\LoggerTrait; 28use FreeDSx\Ldap\Server\Token\TokenInterface; 29use FreeDSx\Socket\Exception\ConnectionException; 30use Throwable; 31use function array_merge; 32use function in_array; 33 34/** 35 * Handles server-client specific protocol interactions. 36 * 37 * @author Chad Sikorra <Chad.Sikorra@gmail.com> 38 */ 39class ServerProtocolHandler 40{ 41 use LoggerTrait; 42 43 /** 44 * @var array 45 */ 46 protected $options = [ 47 'allow_anonymous' => false, 48 'require_authentication' => true, 49 'request_handler' => null, 50 'dse_alt_server' => null, 51 'dse_naming_contexts' => 'dc=FreeDSx,dc=local', 52 'dse_vendor_name' => 'FreeDSx', 53 'dse_vendor_version' => null, 54 ]; 55 56 /** 57 * @var ServerQueue 58 */ 59 protected $queue; 60 61 /** 62 * @var int[] 63 */ 64 protected $messageIds = []; 65 66 /** 67 * @var HandlerFactoryInterface 68 */ 69 protected $handlerFactory; 70 71 /** 72 * @var ServerAuthorization 73 */ 74 protected $authorizer; 75 76 /** 77 * @var ServerProtocolHandlerFactory 78 */ 79 protected $protocolHandlerFactory; 80 81 /** 82 * @var ResponseFactory 83 */ 84 protected $responseFactory; 85 86 /** 87 * @var ServerBindHandlerFactory 88 */ 89 protected $bindHandlerFactory; 90 91 /** 92 * @var array<string, mixed> 93 */ 94 protected $defaultContext = []; 95 96 public function __construct( 97 ServerQueue $queue, 98 HandlerFactoryInterface $handlerFactory, 99 array $options = [], 100 ServerProtocolHandlerFactory $protocolHandlerFactory = null, 101 ServerBindHandlerFactory $bindHandlerFactory = null, 102 ServerAuthorization $authorizer = null, 103 ResponseFactory $responseFactory = null 104 ) { 105 $this->queue = $queue; 106 $this->handlerFactory = $handlerFactory; 107 $this->options = array_merge($this->options, $options); 108 $this->authorizer = $authorizer ?? new ServerAuthorization(null, $this->options); 109 $this->protocolHandlerFactory = $protocolHandlerFactory ?? new ServerProtocolHandlerFactory( 110 $handlerFactory, 111 new RequestHistory() 112 ); 113 $this->bindHandlerFactory = $bindHandlerFactory ?? new ServerBindHandlerFactory(); 114 $this->responseFactory = $responseFactory ?? new ResponseFactory(); 115 } 116 117 /** 118 * Listens for messages from the socket and handles the responses/actions needed. 119 * 120 * @throws EncoderException 121 */ 122 public function handle(array $defaultContext = []): void 123 { 124 $message = null; 125 $this->defaultContext = $defaultContext; 126 127 try { 128 while ($message = $this->queue->getMessage()) { 129 $this->dispatchRequest($message); 130 # If a protocol handler closed the TCP connection, then just break here... 131 if (!$this->queue->isConnected()) { 132 break; 133 } 134 } 135 } catch (OperationException $e) { 136 # OperationExceptions may be thrown by any handler and will be sent back to the client as the response 137 # specific error code and message associated with the exception. 138 $this->queue->sendMessage($this->responseFactory->getStandardResponse( 139 $message, 140 $e->getCode(), 141 $e->getMessage() 142 )); 143 } catch (ConnectionException $e) { 144 $this->logInfo( 145 'Ending LDAP client due to client connection issues.', 146 array_merge( 147 ['message' => $e->getMessage()], 148 $this->defaultContext 149 ) 150 ); 151 } catch (EncoderException | ProtocolException $e) { 152 # Per RFC 4511, 4.1.1 if the PDU cannot be parsed or is otherwise malformed a disconnect should be sent with a 153 # result code of protocol error. 154 $this->sendNoticeOfDisconnect('The message encoding is malformed.'); 155 $this->logError( 156 'The client sent a malformed request. Terminating their connection.', 157 $this->defaultContext 158 ); 159 } catch (Exception | Throwable $e) { 160 $this->logError( 161 'An unexpected exception was caught while handling the client. Terminating their connection.', 162 array_merge( 163 $this->defaultContext, 164 ['exception' => $e] 165 ) 166 ); 167 if ($this->queue->isConnected()) { 168 $this->sendNoticeOfDisconnect(); 169 } 170 } finally { 171 if ($this->queue->isConnected()) { 172 $this->queue->close(); 173 } 174 } 175 } 176 177 /** 178 * Used asynchronously to end a client session when the server process is shutting down. 179 * 180 * @throws EncoderException 181 */ 182 public function shutdown(array $context = []): void 183 { 184 $this->sendNoticeOfDisconnect( 185 'The server is shutting down.', 186 ResultCode::UNAVAILABLE 187 ); 188 $this->queue->close(); 189 $this->logInfo( 190 'Sent notice of disconnect to client and closed the connection.', 191 $context 192 ); 193 } 194 195 /** 196 * Routes requests from the message queue based off the current authorization state and what protocol handler the 197 * request is mapped to. 198 * 199 * @throws OperationException 200 * @throws EncoderException 201 * @throws RuntimeException 202 * @throws ConnectionException 203 */ 204 protected function dispatchRequest(LdapMessageRequest $message): void 205 { 206 if (!$this->isValidRequest($message)) { 207 return; 208 } 209 210 $this->messageIds[] = $message->getMessageId(); 211 212 # Send auth requests to the specific handler for it... 213 if ($this->authorizer->isAuthenticationRequest($message->getRequest())) { 214 $this->authorizer->setToken($this->handleAuthRequest($message)); 215 216 return; 217 } 218 $request = $message->getRequest(); 219 $handler = $this->protocolHandlerFactory->get( 220 $request, 221 $message->controls() 222 ); 223 224 # They are authenticated or authentication is not required, so pass the request along... 225 if ($this->authorizer->isAuthenticated() || !$this->authorizer->isAuthenticationRequired($request)) { 226 $handler->handleRequest( 227 $message, 228 $this->authorizer->getToken(), 229 $this->handlerFactory->makeRequestHandler(), 230 $this->queue, 231 $this->options 232 ); 233 # Authentication is required, but they have not authenticated... 234 } else { 235 $this->queue->sendMessage($this->responseFactory->getStandardResponse( 236 $message, 237 ResultCode::INSUFFICIENT_ACCESS_RIGHTS, 238 'Authentication required.' 239 )); 240 } 241 } 242 243 /** 244 * Checks that the message ID is valid. It cannot be zero or a message ID that was already used. 245 * 246 * @throws EncoderException 247 * @throws EncoderException 248 */ 249 protected function isValidRequest(LdapMessageRequest $message): bool 250 { 251 if ($message->getMessageId() === 0) { 252 $this->queue->sendMessage($this->responseFactory->getExtendedError( 253 'The message ID 0 cannot be used in a client request.', 254 ResultCode::PROTOCOL_ERROR 255 )); 256 257 return false; 258 } 259 if (in_array($message->getMessageId(), $this->messageIds, true)) { 260 $this->queue->sendMessage($this->responseFactory->getExtendedError( 261 sprintf('The message ID %s is not valid.', $message->getMessageId()), 262 ResultCode::PROTOCOL_ERROR 263 )); 264 265 return false; 266 } 267 268 return true; 269 } 270 271 /** 272 * Sends a bind request to the bind handler and returns the token. 273 * 274 * @throws OperationException 275 * @throws RuntimeException 276 */ 277 protected function handleAuthRequest(LdapMessageRequest $message): TokenInterface 278 { 279 if (!$this->authorizer->isAuthenticationTypeSupported($message->getRequest())) { 280 throw new OperationException( 281 'The requested authentication type is not supported.', 282 ResultCode::AUTH_METHOD_UNSUPPORTED 283 ); 284 } 285 286 return $this->bindHandlerFactory->get($message->getRequest())->handleBind( 287 $message, 288 $this->handlerFactory->makeRequestHandler(), 289 $this->queue, 290 $this->options 291 ); 292 } 293 294 /** 295 * @param string $message 296 * @throws EncoderException 297 */ 298 protected function sendNoticeOfDisconnect( 299 string $message = '', 300 int $reasonCode = ResultCode::PROTOCOL_ERROR 301 ): void { 302 $this->queue->sendMessage($this->responseFactory->getExtendedError( 303 $message, 304 $reasonCode, 305 ExtendedResponse::OID_NOTICE_OF_DISCONNECTION 306 )); 307 } 308} 309