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\Asn1\Exception\EncoderException;
14use FreeDSx\Ldap\Exception\OperationException;
15use FreeDSx\Ldap\Exception\ProtocolException;
16use FreeDSx\Ldap\Operation\Response\ExtendedResponse;
17use FreeDSx\Ldap\Operation\ResultCode;
18use FreeDSx\Ldap\Protocol\Factory\ResponseFactory;
19use FreeDSx\Ldap\Protocol\Factory\ServerBindHandlerFactory;
20use FreeDSx\Ldap\Protocol\Factory\ServerProtocolHandlerFactory;
21use FreeDSx\Ldap\Protocol\Queue\ServerQueue;
22use FreeDSx\Ldap\Server\RequestHandler\RequestHandlerInterface;
23use FreeDSx\Ldap\Server\Token\TokenInterface;
24
25/**
26 * Handles server-client specific protocol interactions.
27 *
28 * @author Chad Sikorra <Chad.Sikorra@gmail.com>
29 */
30class ServerProtocolHandler
31{
32    /**
33     * @var array
34     */
35    protected $options = [
36        'allow_anonymous' => false,
37        'require_authentication' => true,
38        'request_handler' => null,
39        'dse_alt_server' => null,
40        'dse_naming_contexts' => 'dc=FreeDSx,dc=local',
41        'dse_vendor_name' => 'FreeDSx',
42        'dse_vendor_version' => null,
43    ];
44
45    /**
46     * @var ServerQueue
47     */
48    protected $queue;
49
50    /**
51     * @var int[]
52     */
53    protected $messageIds = [];
54
55    /**
56     * @var RequestHandlerInterface
57     */
58    protected $dispatcher;
59
60    /**
61     * @var ServerAuthorization
62     */
63    protected $authorizer;
64
65    /**
66     * @var ServerProtocolHandlerFactory
67     */
68    protected $protocolHandlerFactory;
69
70    /**
71     * @var ResponseFactory
72     */
73    protected $responseFactory;
74
75    /**
76     * @var ServerBindHandlerFactory
77     */
78    protected $bindHandlerFactory;
79
80    public function __construct(
81        ServerQueue $queue,
82        RequestHandlerInterface $dispatcher,
83        array $options = [],
84        ServerProtocolHandlerFactory $protocolHandlerFactory = null,
85        ServerBindHandlerFactory $bindHandlerFactory = null,
86        ServerAuthorization $authorizer = null,
87        ResponseFactory $responseFactory = null
88    ) {
89        $this->queue = $queue;
90        $this->dispatcher = $dispatcher;
91        $this->options = \array_merge($this->options, $options);
92        $this->authorizer = $authorizer ?? new ServerAuthorization(null, $this->options);
93        $this->protocolHandlerFactory = $protocolHandlerFactory ?? new ServerProtocolHandlerFactory();
94        $this->bindHandlerFactory = $bindHandlerFactory ?? new ServerBindHandlerFactory();
95        $this->responseFactory = $responseFactory ?? new ResponseFactory();
96    }
97
98    /**
99     * Listens for messages from the socket and handles the responses/actions needed.
100     */
101    public function handle(): void
102    {
103        try {
104            while ($message = $this->queue->getMessage()) {
105                $this->dispatchRequest($message);
106                # If a protocol handler closed the TCP connection, then just break here...
107                if (!$this->queue->isConnected()) {
108                    break;
109                }
110            }
111        } catch (OperationException $e) {
112            # OperationExceptions may be thrown by any handler and will be sent back to the client as the response
113            # specific error code and message associated with the exception.
114            if (isset($message)) {
115                $this->queue->sendMessage($this->responseFactory->getStandardResponse(
116                    $message,
117                    $e->getCode(),
118                    $e->getMessage()
119                ));
120            }
121        } catch (EncoderException | ProtocolException $e) {
122            # Per RFC 4511, 4.1.1 if the PDU cannot be parsed or is otherwise malformed a disconnect should be sent with a
123            # result code of protocol error.
124            $this->sendNoticeOfDisconnect('The message encoding is malformed.');
125        } catch (\Exception | \Throwable $e) {
126            if ($this->queue->isConnected()) {
127                $this->sendNoticeOfDisconnect();
128            }
129        } finally {
130            if ($this->queue->isConnected()) {
131                $this->queue->close();
132            }
133        }
134    }
135
136    /**
137     * Routes requests from the message queue based off the current authorization state and what protocol handler the
138     * request is mapped to.
139     *
140     * @throws OperationException
141     */
142    protected function dispatchRequest(LdapMessageRequest $message): void
143    {
144        if (!$this->isValidRequest($message)) {
145            return;
146        }
147
148        $this->messageIds[] = $message->getMessageId();
149
150        # Send auth requests to the specific handler for it...
151        if ($this->authorizer->isAuthenticationRequest($message->getRequest())) {
152            $this->authorizer->setToken($this->handleAuthRequest($message));
153
154            return;
155        }
156        $request = $message->getRequest();
157        $handler = $this->protocolHandlerFactory->get($request);
158
159        # They are authenticated or authentication is not required, so pass the request along...
160        if ($this->authorizer->isAuthenticated() || !$this->authorizer->isAuthenticationRequired($request)) {
161            $handler->handleRequest(
162                $message,
163                $this->authorizer->getToken(),
164                $this->dispatcher,
165                $this->queue,
166                $this->options
167            );
168        # Authentication is required, but they have not authenticated...
169        } else {
170            $this->queue->sendMessage($this->responseFactory->getStandardResponse(
171                $message,
172                ResultCode::INSUFFICIENT_ACCESS_RIGHTS,
173                'Authentication required.'
174            ));
175        }
176    }
177
178    /**
179     * Checks that the message ID is valid. It cannot be zero or a message ID that was already used.
180     */
181    protected function isValidRequest(LdapMessageRequest $message): bool
182    {
183        if ($message->getMessageId() === 0) {
184            $this->queue->sendMessage($this->responseFactory->getExtendedError(
185                'The message ID 0 cannot be used in a client request.',
186                ResultCode::PROTOCOL_ERROR
187            ));
188
189            return false;
190        }
191        if (\in_array($message->getMessageId(), $this->messageIds, true)) {
192            $this->queue->sendMessage($this->responseFactory->getExtendedError(
193                sprintf('The message ID %s is not valid.', $message->getMessageId()),
194                ResultCode::PROTOCOL_ERROR
195            ));
196
197            return false;
198        }
199
200        return true;
201    }
202
203    /**
204     * Sends a bind request to the bind handler and returns the token.
205     *
206     * @throws OperationException
207     */
208    protected function handleAuthRequest(LdapMessageRequest $message): TokenInterface
209    {
210        if (!$this->authorizer->isAuthenticationTypeSupported($message->getRequest())) {
211            throw new OperationException(
212                'The requested authentication type is not supported.',
213                ResultCode::AUTH_METHOD_UNSUPPORTED
214            );
215        }
216
217        return $this->bindHandlerFactory->get($message->getRequest())->handleBind(
218            $message,
219            $this->dispatcher,
220            $this->queue,
221            $this->options
222        );
223    }
224
225    protected function sendNoticeOfDisconnect(string $message = ''): void
226    {
227        $this->queue->sendMessage($this->responseFactory->getExtendedError(
228            $message,
229            ResultCode::PROTOCOL_ERROR,
230            ExtendedResponse::OID_NOTICE_OF_DISCONNECTION
231        ));
232    }
233}
234