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