* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace FreeDSx\Ldap\Protocol; use Exception; use FreeDSx\Asn1\Exception\EncoderException; use FreeDSx\Ldap\Exception\OperationException; use FreeDSx\Ldap\Exception\ProtocolException; use FreeDSx\Ldap\Exception\RuntimeException; use FreeDSx\Ldap\Operation\Response\ExtendedResponse; use FreeDSx\Ldap\Operation\ResultCode; use FreeDSx\Ldap\Protocol\Factory\ResponseFactory; use FreeDSx\Ldap\Protocol\Factory\ServerBindHandlerFactory; use FreeDSx\Ldap\Protocol\Factory\ServerProtocolHandlerFactory; use FreeDSx\Ldap\Protocol\Queue\ServerQueue; use FreeDSx\Ldap\Server\HandlerFactoryInterface; use FreeDSx\Ldap\Server\RequestHistory; use FreeDSx\Ldap\LoggerTrait; use FreeDSx\Ldap\Server\Token\TokenInterface; use FreeDSx\Socket\Exception\ConnectionException; use Throwable; use function array_merge; use function in_array; /** * Handles server-client specific protocol interactions. * * @author Chad Sikorra */ class ServerProtocolHandler { use LoggerTrait; /** * @var array */ protected $options = [ 'allow_anonymous' => false, 'require_authentication' => true, 'request_handler' => null, 'dse_alt_server' => null, 'dse_naming_contexts' => 'dc=FreeDSx,dc=local', 'dse_vendor_name' => 'FreeDSx', 'dse_vendor_version' => null, ]; /** * @var ServerQueue */ protected $queue; /** * @var int[] */ protected $messageIds = []; /** * @var HandlerFactoryInterface */ protected $handlerFactory; /** * @var ServerAuthorization */ protected $authorizer; /** * @var ServerProtocolHandlerFactory */ protected $protocolHandlerFactory; /** * @var ResponseFactory */ protected $responseFactory; /** * @var ServerBindHandlerFactory */ protected $bindHandlerFactory; /** * @var array */ protected $defaultContext = []; public function __construct( ServerQueue $queue, HandlerFactoryInterface $handlerFactory, array $options = [], ServerProtocolHandlerFactory $protocolHandlerFactory = null, ServerBindHandlerFactory $bindHandlerFactory = null, ServerAuthorization $authorizer = null, ResponseFactory $responseFactory = null ) { $this->queue = $queue; $this->handlerFactory = $handlerFactory; $this->options = array_merge($this->options, $options); $this->authorizer = $authorizer ?? new ServerAuthorization(null, $this->options); $this->protocolHandlerFactory = $protocolHandlerFactory ?? new ServerProtocolHandlerFactory( $handlerFactory, new RequestHistory() ); $this->bindHandlerFactory = $bindHandlerFactory ?? new ServerBindHandlerFactory(); $this->responseFactory = $responseFactory ?? new ResponseFactory(); } /** * Listens for messages from the socket and handles the responses/actions needed. * * @throws EncoderException */ public function handle(array $defaultContext = []): void { $message = null; $this->defaultContext = $defaultContext; try { while ($message = $this->queue->getMessage()) { $this->dispatchRequest($message); # If a protocol handler closed the TCP connection, then just break here... if (!$this->queue->isConnected()) { break; } } } catch (OperationException $e) { # OperationExceptions may be thrown by any handler and will be sent back to the client as the response # specific error code and message associated with the exception. $this->queue->sendMessage($this->responseFactory->getStandardResponse( $message, $e->getCode(), $e->getMessage() )); } catch (ConnectionException $e) { $this->logInfo( 'Ending LDAP client due to client connection issues.', array_merge( ['message' => $e->getMessage()], $this->defaultContext ) ); } catch (EncoderException | ProtocolException $e) { # Per RFC 4511, 4.1.1 if the PDU cannot be parsed or is otherwise malformed a disconnect should be sent with a # result code of protocol error. $this->sendNoticeOfDisconnect('The message encoding is malformed.'); $this->logError( 'The client sent a malformed request. Terminating their connection.', $this->defaultContext ); } catch (Exception | Throwable $e) { $this->logError( 'An unexpected exception was caught while handling the client. Terminating their connection.', array_merge( $this->defaultContext, ['exception' => $e] ) ); if ($this->queue->isConnected()) { $this->sendNoticeOfDisconnect(); } } finally { if ($this->queue->isConnected()) { $this->queue->close(); } } } /** * Used asynchronously to end a client session when the server process is shutting down. * * @throws EncoderException */ public function shutdown(array $context = []): void { $this->sendNoticeOfDisconnect( 'The server is shutting down.', ResultCode::UNAVAILABLE ); $this->queue->close(); $this->logInfo( 'Sent notice of disconnect to client and closed the connection.', $context ); } /** * Routes requests from the message queue based off the current authorization state and what protocol handler the * request is mapped to. * * @throws OperationException * @throws EncoderException * @throws RuntimeException * @throws ConnectionException */ protected function dispatchRequest(LdapMessageRequest $message): void { if (!$this->isValidRequest($message)) { return; } $this->messageIds[] = $message->getMessageId(); # Send auth requests to the specific handler for it... if ($this->authorizer->isAuthenticationRequest($message->getRequest())) { $this->authorizer->setToken($this->handleAuthRequest($message)); return; } $request = $message->getRequest(); $handler = $this->protocolHandlerFactory->get( $request, $message->controls() ); # They are authenticated or authentication is not required, so pass the request along... if ($this->authorizer->isAuthenticated() || !$this->authorizer->isAuthenticationRequired($request)) { $handler->handleRequest( $message, $this->authorizer->getToken(), $this->handlerFactory->makeRequestHandler(), $this->queue, $this->options ); # Authentication is required, but they have not authenticated... } else { $this->queue->sendMessage($this->responseFactory->getStandardResponse( $message, ResultCode::INSUFFICIENT_ACCESS_RIGHTS, 'Authentication required.' )); } } /** * Checks that the message ID is valid. It cannot be zero or a message ID that was already used. * * @throws EncoderException * @throws EncoderException */ protected function isValidRequest(LdapMessageRequest $message): bool { if ($message->getMessageId() === 0) { $this->queue->sendMessage($this->responseFactory->getExtendedError( 'The message ID 0 cannot be used in a client request.', ResultCode::PROTOCOL_ERROR )); return false; } if (in_array($message->getMessageId(), $this->messageIds, true)) { $this->queue->sendMessage($this->responseFactory->getExtendedError( sprintf('The message ID %s is not valid.', $message->getMessageId()), ResultCode::PROTOCOL_ERROR )); return false; } return true; } /** * Sends a bind request to the bind handler and returns the token. * * @throws OperationException * @throws RuntimeException */ protected function handleAuthRequest(LdapMessageRequest $message): TokenInterface { if (!$this->authorizer->isAuthenticationTypeSupported($message->getRequest())) { throw new OperationException( 'The requested authentication type is not supported.', ResultCode::AUTH_METHOD_UNSUPPORTED ); } return $this->bindHandlerFactory->get($message->getRequest())->handleBind( $message, $this->handlerFactory->makeRequestHandler(), $this->queue, $this->options ); } /** * @param string $message * @throws EncoderException */ protected function sendNoticeOfDisconnect( string $message = '', int $reasonCode = ResultCode::PROTOCOL_ERROR ): void { $this->queue->sendMessage($this->responseFactory->getExtendedError( $message, $reasonCode, ExtendedResponse::OID_NOTICE_OF_DISCONNECTION )); } }