xref: /plugin/pureldap/vendor/freedsx/ldap/src/FreeDSx/Ldap/Protocol/ServerProtocolHandler.php (revision dad993c57a70866aa1db59c43f043769c2eb7ed0)
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