10b3fd2d3SAndreas Gohr<?php 2*dad993c5SAndreas Gohr 30b3fd2d3SAndreas Gohr/** 40b3fd2d3SAndreas Gohr * This file is part of the FreeDSx LDAP package. 50b3fd2d3SAndreas Gohr * 60b3fd2d3SAndreas Gohr * (c) Chad Sikorra <Chad.Sikorra@gmail.com> 70b3fd2d3SAndreas Gohr * 80b3fd2d3SAndreas Gohr * For the full copyright and license information, please view the LICENSE 90b3fd2d3SAndreas Gohr * file that was distributed with this source code. 100b3fd2d3SAndreas Gohr */ 110b3fd2d3SAndreas Gohr 120b3fd2d3SAndreas Gohrnamespace FreeDSx\Ldap\Protocol; 130b3fd2d3SAndreas Gohr 14*dad993c5SAndreas Gohruse Exception; 150b3fd2d3SAndreas Gohruse FreeDSx\Asn1\Exception\EncoderException; 160b3fd2d3SAndreas Gohruse FreeDSx\Ldap\Exception\OperationException; 170b3fd2d3SAndreas Gohruse FreeDSx\Ldap\Exception\ProtocolException; 18*dad993c5SAndreas Gohruse FreeDSx\Ldap\Exception\RuntimeException; 190b3fd2d3SAndreas Gohruse FreeDSx\Ldap\Operation\Response\ExtendedResponse; 200b3fd2d3SAndreas Gohruse FreeDSx\Ldap\Operation\ResultCode; 210b3fd2d3SAndreas Gohruse FreeDSx\Ldap\Protocol\Factory\ResponseFactory; 220b3fd2d3SAndreas Gohruse FreeDSx\Ldap\Protocol\Factory\ServerBindHandlerFactory; 230b3fd2d3SAndreas Gohruse FreeDSx\Ldap\Protocol\Factory\ServerProtocolHandlerFactory; 240b3fd2d3SAndreas Gohruse FreeDSx\Ldap\Protocol\Queue\ServerQueue; 25*dad993c5SAndreas Gohruse FreeDSx\Ldap\Server\HandlerFactoryInterface; 26*dad993c5SAndreas Gohruse FreeDSx\Ldap\Server\RequestHistory; 27*dad993c5SAndreas Gohruse FreeDSx\Ldap\LoggerTrait; 280b3fd2d3SAndreas Gohruse FreeDSx\Ldap\Server\Token\TokenInterface; 29*dad993c5SAndreas Gohruse FreeDSx\Socket\Exception\ConnectionException; 30*dad993c5SAndreas Gohruse Throwable; 31*dad993c5SAndreas Gohruse function array_merge; 32*dad993c5SAndreas Gohruse function in_array; 330b3fd2d3SAndreas Gohr 340b3fd2d3SAndreas Gohr/** 350b3fd2d3SAndreas Gohr * Handles server-client specific protocol interactions. 360b3fd2d3SAndreas Gohr * 370b3fd2d3SAndreas Gohr * @author Chad Sikorra <Chad.Sikorra@gmail.com> 380b3fd2d3SAndreas Gohr */ 390b3fd2d3SAndreas Gohrclass ServerProtocolHandler 400b3fd2d3SAndreas Gohr{ 41*dad993c5SAndreas Gohr use LoggerTrait; 42*dad993c5SAndreas Gohr 430b3fd2d3SAndreas Gohr /** 440b3fd2d3SAndreas Gohr * @var array 450b3fd2d3SAndreas Gohr */ 460b3fd2d3SAndreas Gohr protected $options = [ 470b3fd2d3SAndreas Gohr 'allow_anonymous' => false, 480b3fd2d3SAndreas Gohr 'require_authentication' => true, 490b3fd2d3SAndreas Gohr 'request_handler' => null, 500b3fd2d3SAndreas Gohr 'dse_alt_server' => null, 510b3fd2d3SAndreas Gohr 'dse_naming_contexts' => 'dc=FreeDSx,dc=local', 520b3fd2d3SAndreas Gohr 'dse_vendor_name' => 'FreeDSx', 530b3fd2d3SAndreas Gohr 'dse_vendor_version' => null, 540b3fd2d3SAndreas Gohr ]; 550b3fd2d3SAndreas Gohr 560b3fd2d3SAndreas Gohr /** 570b3fd2d3SAndreas Gohr * @var ServerQueue 580b3fd2d3SAndreas Gohr */ 590b3fd2d3SAndreas Gohr protected $queue; 600b3fd2d3SAndreas Gohr 610b3fd2d3SAndreas Gohr /** 620b3fd2d3SAndreas Gohr * @var int[] 630b3fd2d3SAndreas Gohr */ 640b3fd2d3SAndreas Gohr protected $messageIds = []; 650b3fd2d3SAndreas Gohr 660b3fd2d3SAndreas Gohr /** 67*dad993c5SAndreas Gohr * @var HandlerFactoryInterface 680b3fd2d3SAndreas Gohr */ 69*dad993c5SAndreas Gohr protected $handlerFactory; 700b3fd2d3SAndreas Gohr 710b3fd2d3SAndreas Gohr /** 720b3fd2d3SAndreas Gohr * @var ServerAuthorization 730b3fd2d3SAndreas Gohr */ 740b3fd2d3SAndreas Gohr protected $authorizer; 750b3fd2d3SAndreas Gohr 760b3fd2d3SAndreas Gohr /** 770b3fd2d3SAndreas Gohr * @var ServerProtocolHandlerFactory 780b3fd2d3SAndreas Gohr */ 790b3fd2d3SAndreas Gohr protected $protocolHandlerFactory; 800b3fd2d3SAndreas Gohr 810b3fd2d3SAndreas Gohr /** 820b3fd2d3SAndreas Gohr * @var ResponseFactory 830b3fd2d3SAndreas Gohr */ 840b3fd2d3SAndreas Gohr protected $responseFactory; 850b3fd2d3SAndreas Gohr 860b3fd2d3SAndreas Gohr /** 870b3fd2d3SAndreas Gohr * @var ServerBindHandlerFactory 880b3fd2d3SAndreas Gohr */ 890b3fd2d3SAndreas Gohr protected $bindHandlerFactory; 900b3fd2d3SAndreas Gohr 91*dad993c5SAndreas Gohr /** 92*dad993c5SAndreas Gohr * @var array<string, mixed> 93*dad993c5SAndreas Gohr */ 94*dad993c5SAndreas Gohr protected $defaultContext = []; 95*dad993c5SAndreas Gohr 960b3fd2d3SAndreas Gohr public function __construct( 970b3fd2d3SAndreas Gohr ServerQueue $queue, 98*dad993c5SAndreas Gohr HandlerFactoryInterface $handlerFactory, 990b3fd2d3SAndreas Gohr array $options = [], 1000b3fd2d3SAndreas Gohr ServerProtocolHandlerFactory $protocolHandlerFactory = null, 1010b3fd2d3SAndreas Gohr ServerBindHandlerFactory $bindHandlerFactory = null, 1020b3fd2d3SAndreas Gohr ServerAuthorization $authorizer = null, 1030b3fd2d3SAndreas Gohr ResponseFactory $responseFactory = null 1040b3fd2d3SAndreas Gohr ) { 1050b3fd2d3SAndreas Gohr $this->queue = $queue; 106*dad993c5SAndreas Gohr $this->handlerFactory = $handlerFactory; 107*dad993c5SAndreas Gohr $this->options = array_merge($this->options, $options); 1080b3fd2d3SAndreas Gohr $this->authorizer = $authorizer ?? new ServerAuthorization(null, $this->options); 109*dad993c5SAndreas Gohr $this->protocolHandlerFactory = $protocolHandlerFactory ?? new ServerProtocolHandlerFactory( 110*dad993c5SAndreas Gohr $handlerFactory, 111*dad993c5SAndreas Gohr new RequestHistory() 112*dad993c5SAndreas Gohr ); 1130b3fd2d3SAndreas Gohr $this->bindHandlerFactory = $bindHandlerFactory ?? new ServerBindHandlerFactory(); 1140b3fd2d3SAndreas Gohr $this->responseFactory = $responseFactory ?? new ResponseFactory(); 1150b3fd2d3SAndreas Gohr } 1160b3fd2d3SAndreas Gohr 1170b3fd2d3SAndreas Gohr /** 1180b3fd2d3SAndreas Gohr * Listens for messages from the socket and handles the responses/actions needed. 119*dad993c5SAndreas Gohr * 120*dad993c5SAndreas Gohr * @throws EncoderException 1210b3fd2d3SAndreas Gohr */ 122*dad993c5SAndreas Gohr public function handle(array $defaultContext = []): void 1230b3fd2d3SAndreas Gohr { 124*dad993c5SAndreas Gohr $message = null; 125*dad993c5SAndreas Gohr $this->defaultContext = $defaultContext; 126*dad993c5SAndreas Gohr 1270b3fd2d3SAndreas Gohr try { 1280b3fd2d3SAndreas Gohr while ($message = $this->queue->getMessage()) { 1290b3fd2d3SAndreas Gohr $this->dispatchRequest($message); 1300b3fd2d3SAndreas Gohr # If a protocol handler closed the TCP connection, then just break here... 1310b3fd2d3SAndreas Gohr if (!$this->queue->isConnected()) { 1320b3fd2d3SAndreas Gohr break; 1330b3fd2d3SAndreas Gohr } 1340b3fd2d3SAndreas Gohr } 1350b3fd2d3SAndreas Gohr } catch (OperationException $e) { 1360b3fd2d3SAndreas Gohr # OperationExceptions may be thrown by any handler and will be sent back to the client as the response 1370b3fd2d3SAndreas Gohr # specific error code and message associated with the exception. 1380b3fd2d3SAndreas Gohr $this->queue->sendMessage($this->responseFactory->getStandardResponse( 1390b3fd2d3SAndreas Gohr $message, 1400b3fd2d3SAndreas Gohr $e->getCode(), 1410b3fd2d3SAndreas Gohr $e->getMessage() 1420b3fd2d3SAndreas Gohr )); 143*dad993c5SAndreas Gohr } catch (ConnectionException $e) { 144*dad993c5SAndreas Gohr $this->logInfo( 145*dad993c5SAndreas Gohr 'Ending LDAP client due to client connection issues.', 146*dad993c5SAndreas Gohr array_merge( 147*dad993c5SAndreas Gohr ['message' => $e->getMessage()], 148*dad993c5SAndreas Gohr $this->defaultContext 149*dad993c5SAndreas Gohr ) 150*dad993c5SAndreas Gohr ); 1510b3fd2d3SAndreas Gohr } catch (EncoderException | ProtocolException $e) { 1520b3fd2d3SAndreas Gohr # Per RFC 4511, 4.1.1 if the PDU cannot be parsed or is otherwise malformed a disconnect should be sent with a 1530b3fd2d3SAndreas Gohr # result code of protocol error. 1540b3fd2d3SAndreas Gohr $this->sendNoticeOfDisconnect('The message encoding is malformed.'); 155*dad993c5SAndreas Gohr $this->logError( 156*dad993c5SAndreas Gohr 'The client sent a malformed request. Terminating their connection.', 157*dad993c5SAndreas Gohr $this->defaultContext 158*dad993c5SAndreas Gohr ); 159*dad993c5SAndreas Gohr } catch (Exception | Throwable $e) { 160*dad993c5SAndreas Gohr $this->logError( 161*dad993c5SAndreas Gohr 'An unexpected exception was caught while handling the client. Terminating their connection.', 162*dad993c5SAndreas Gohr array_merge( 163*dad993c5SAndreas Gohr $this->defaultContext, 164*dad993c5SAndreas Gohr ['exception' => $e] 165*dad993c5SAndreas Gohr ) 166*dad993c5SAndreas Gohr ); 1670b3fd2d3SAndreas Gohr if ($this->queue->isConnected()) { 1680b3fd2d3SAndreas Gohr $this->sendNoticeOfDisconnect(); 1690b3fd2d3SAndreas Gohr } 1700b3fd2d3SAndreas Gohr } finally { 1710b3fd2d3SAndreas Gohr if ($this->queue->isConnected()) { 1720b3fd2d3SAndreas Gohr $this->queue->close(); 1730b3fd2d3SAndreas Gohr } 1740b3fd2d3SAndreas Gohr } 1750b3fd2d3SAndreas Gohr } 1760b3fd2d3SAndreas Gohr 1770b3fd2d3SAndreas Gohr /** 178*dad993c5SAndreas Gohr * Used asynchronously to end a client session when the server process is shutting down. 179*dad993c5SAndreas Gohr * 180*dad993c5SAndreas Gohr * @throws EncoderException 181*dad993c5SAndreas Gohr */ 182*dad993c5SAndreas Gohr public function shutdown(array $context = []): void 183*dad993c5SAndreas Gohr { 184*dad993c5SAndreas Gohr $this->sendNoticeOfDisconnect( 185*dad993c5SAndreas Gohr 'The server is shutting down.', 186*dad993c5SAndreas Gohr ResultCode::UNAVAILABLE 187*dad993c5SAndreas Gohr ); 188*dad993c5SAndreas Gohr $this->queue->close(); 189*dad993c5SAndreas Gohr $this->logInfo( 190*dad993c5SAndreas Gohr 'Sent notice of disconnect to client and closed the connection.', 191*dad993c5SAndreas Gohr $context 192*dad993c5SAndreas Gohr ); 193*dad993c5SAndreas Gohr } 194*dad993c5SAndreas Gohr 195*dad993c5SAndreas Gohr /** 1960b3fd2d3SAndreas Gohr * Routes requests from the message queue based off the current authorization state and what protocol handler the 1970b3fd2d3SAndreas Gohr * request is mapped to. 1980b3fd2d3SAndreas Gohr * 1990b3fd2d3SAndreas Gohr * @throws OperationException 200*dad993c5SAndreas Gohr * @throws EncoderException 201*dad993c5SAndreas Gohr * @throws RuntimeException 202*dad993c5SAndreas Gohr * @throws ConnectionException 2030b3fd2d3SAndreas Gohr */ 2040b3fd2d3SAndreas Gohr protected function dispatchRequest(LdapMessageRequest $message): void 2050b3fd2d3SAndreas Gohr { 2060b3fd2d3SAndreas Gohr if (!$this->isValidRequest($message)) { 2070b3fd2d3SAndreas Gohr return; 2080b3fd2d3SAndreas Gohr } 2090b3fd2d3SAndreas Gohr 2100b3fd2d3SAndreas Gohr $this->messageIds[] = $message->getMessageId(); 2110b3fd2d3SAndreas Gohr 2120b3fd2d3SAndreas Gohr # Send auth requests to the specific handler for it... 2130b3fd2d3SAndreas Gohr if ($this->authorizer->isAuthenticationRequest($message->getRequest())) { 2140b3fd2d3SAndreas Gohr $this->authorizer->setToken($this->handleAuthRequest($message)); 2150b3fd2d3SAndreas Gohr 2160b3fd2d3SAndreas Gohr return; 2170b3fd2d3SAndreas Gohr } 2180b3fd2d3SAndreas Gohr $request = $message->getRequest(); 219*dad993c5SAndreas Gohr $handler = $this->protocolHandlerFactory->get( 220*dad993c5SAndreas Gohr $request, 221*dad993c5SAndreas Gohr $message->controls() 222*dad993c5SAndreas Gohr ); 2230b3fd2d3SAndreas Gohr 2240b3fd2d3SAndreas Gohr # They are authenticated or authentication is not required, so pass the request along... 2250b3fd2d3SAndreas Gohr if ($this->authorizer->isAuthenticated() || !$this->authorizer->isAuthenticationRequired($request)) { 2260b3fd2d3SAndreas Gohr $handler->handleRequest( 2270b3fd2d3SAndreas Gohr $message, 2280b3fd2d3SAndreas Gohr $this->authorizer->getToken(), 229*dad993c5SAndreas Gohr $this->handlerFactory->makeRequestHandler(), 2300b3fd2d3SAndreas Gohr $this->queue, 2310b3fd2d3SAndreas Gohr $this->options 2320b3fd2d3SAndreas Gohr ); 2330b3fd2d3SAndreas Gohr # Authentication is required, but they have not authenticated... 2340b3fd2d3SAndreas Gohr } else { 2350b3fd2d3SAndreas Gohr $this->queue->sendMessage($this->responseFactory->getStandardResponse( 2360b3fd2d3SAndreas Gohr $message, 2370b3fd2d3SAndreas Gohr ResultCode::INSUFFICIENT_ACCESS_RIGHTS, 2380b3fd2d3SAndreas Gohr 'Authentication required.' 2390b3fd2d3SAndreas Gohr )); 2400b3fd2d3SAndreas Gohr } 2410b3fd2d3SAndreas Gohr } 2420b3fd2d3SAndreas Gohr 2430b3fd2d3SAndreas Gohr /** 2440b3fd2d3SAndreas Gohr * Checks that the message ID is valid. It cannot be zero or a message ID that was already used. 245*dad993c5SAndreas Gohr * 246*dad993c5SAndreas Gohr * @throws EncoderException 247*dad993c5SAndreas Gohr * @throws EncoderException 2480b3fd2d3SAndreas Gohr */ 2490b3fd2d3SAndreas Gohr protected function isValidRequest(LdapMessageRequest $message): bool 2500b3fd2d3SAndreas Gohr { 2510b3fd2d3SAndreas Gohr if ($message->getMessageId() === 0) { 2520b3fd2d3SAndreas Gohr $this->queue->sendMessage($this->responseFactory->getExtendedError( 2530b3fd2d3SAndreas Gohr 'The message ID 0 cannot be used in a client request.', 2540b3fd2d3SAndreas Gohr ResultCode::PROTOCOL_ERROR 2550b3fd2d3SAndreas Gohr )); 2560b3fd2d3SAndreas Gohr 2570b3fd2d3SAndreas Gohr return false; 2580b3fd2d3SAndreas Gohr } 259*dad993c5SAndreas Gohr if (in_array($message->getMessageId(), $this->messageIds, true)) { 2600b3fd2d3SAndreas Gohr $this->queue->sendMessage($this->responseFactory->getExtendedError( 2610b3fd2d3SAndreas Gohr sprintf('The message ID %s is not valid.', $message->getMessageId()), 2620b3fd2d3SAndreas Gohr ResultCode::PROTOCOL_ERROR 2630b3fd2d3SAndreas Gohr )); 2640b3fd2d3SAndreas Gohr 2650b3fd2d3SAndreas Gohr return false; 2660b3fd2d3SAndreas Gohr } 2670b3fd2d3SAndreas Gohr 2680b3fd2d3SAndreas Gohr return true; 2690b3fd2d3SAndreas Gohr } 2700b3fd2d3SAndreas Gohr 2710b3fd2d3SAndreas Gohr /** 2720b3fd2d3SAndreas Gohr * Sends a bind request to the bind handler and returns the token. 2730b3fd2d3SAndreas Gohr * 2740b3fd2d3SAndreas Gohr * @throws OperationException 275*dad993c5SAndreas Gohr * @throws RuntimeException 2760b3fd2d3SAndreas Gohr */ 2770b3fd2d3SAndreas Gohr protected function handleAuthRequest(LdapMessageRequest $message): TokenInterface 2780b3fd2d3SAndreas Gohr { 2790b3fd2d3SAndreas Gohr if (!$this->authorizer->isAuthenticationTypeSupported($message->getRequest())) { 2800b3fd2d3SAndreas Gohr throw new OperationException( 2810b3fd2d3SAndreas Gohr 'The requested authentication type is not supported.', 2820b3fd2d3SAndreas Gohr ResultCode::AUTH_METHOD_UNSUPPORTED 2830b3fd2d3SAndreas Gohr ); 2840b3fd2d3SAndreas Gohr } 2850b3fd2d3SAndreas Gohr 2860b3fd2d3SAndreas Gohr return $this->bindHandlerFactory->get($message->getRequest())->handleBind( 2870b3fd2d3SAndreas Gohr $message, 288*dad993c5SAndreas Gohr $this->handlerFactory->makeRequestHandler(), 2890b3fd2d3SAndreas Gohr $this->queue, 2900b3fd2d3SAndreas Gohr $this->options 2910b3fd2d3SAndreas Gohr ); 2920b3fd2d3SAndreas Gohr } 2930b3fd2d3SAndreas Gohr 294*dad993c5SAndreas Gohr /** 295*dad993c5SAndreas Gohr * @param string $message 296*dad993c5SAndreas Gohr * @throws EncoderException 297*dad993c5SAndreas Gohr */ 298*dad993c5SAndreas Gohr protected function sendNoticeOfDisconnect( 299*dad993c5SAndreas Gohr string $message = '', 300*dad993c5SAndreas Gohr int $reasonCode = ResultCode::PROTOCOL_ERROR 301*dad993c5SAndreas Gohr ): void { 3020b3fd2d3SAndreas Gohr $this->queue->sendMessage($this->responseFactory->getExtendedError( 3030b3fd2d3SAndreas Gohr $message, 304*dad993c5SAndreas Gohr $reasonCode, 3050b3fd2d3SAndreas Gohr ExtendedResponse::OID_NOTICE_OF_DISCONNECTION 3060b3fd2d3SAndreas Gohr )); 3070b3fd2d3SAndreas Gohr } 3080b3fd2d3SAndreas Gohr} 309