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\Ldap\Control\Control;
14use FreeDSx\Ldap\Control\ControlBag;
15use FreeDSx\Ldap\Entry\Entry;
16use FreeDSx\Ldap\Exception\ConnectionException;
17use FreeDSx\Ldap\Exception\OperationException;
18use FreeDSx\Ldap\Exception\UnsolicitedNotificationException;
19use FreeDSx\Ldap\Operation\Request\RequestInterface;
20use FreeDSx\Ldap\Operation\Response\ExtendedResponse;
21use FreeDSx\Ldap\Operation\Response\SearchResponse;
22use FreeDSx\Ldap\Operations;
23use FreeDSx\Ldap\Protocol\ClientProtocolHandler\ClientProtocolContext;
24use FreeDSx\Ldap\Protocol\Factory\ClientProtocolHandlerFactory;
25use FreeDSx\Ldap\Protocol\Queue\ClientQueue;
26use FreeDSx\Socket\Exception\ConnectionException as SocketException;
27use FreeDSx\Socket\SocketPool;
28
29/**
30 * Handles client specific protocol communication details.
31 *
32 * @author Chad Sikorra <Chad.Sikorra@gmail.com>
33 */
34class ClientProtocolHandler
35{
36    const ROOTDSE_ATTRIBUTES = [
37        'supportedSaslMechanisms',
38        'supportedControl',
39        'supportedLDAPVersion',
40    ];
41
42    /**
43     * @var SocketPool
44     */
45    protected $pool;
46
47    /**
48     * @var ClientQueue|null
49     */
50    protected $queue;
51
52    /**
53     * @var array
54     */
55    protected $options;
56
57    /**
58     * @var ControlBag
59     */
60    protected $controls;
61
62    /**
63     * @var ClientProtocolHandlerFactory
64     */
65    protected $protocolHandlerFactory;
66
67    /**
68     * @var null|Entry
69     */
70    protected $rootDse;
71
72    public function __construct(array $options, ClientQueue $queue = null, SocketPool $pool = null, ClientProtocolHandlerFactory $clientProtocolHandlerFactory = null)
73    {
74        $this->options = $options;
75        $this->pool = $pool ?? new SocketPool($options);
76        $this->protocolHandlerFactory = $clientProtocolHandlerFactory ?? new ClientProtocolHandlerFactory();
77        $this->controls = new ControlBag();
78        $this->queue = $queue;
79    }
80
81    /**
82     * @return ControlBag
83     */
84    public function controls(): ControlBag
85    {
86        return $this->controls;
87    }
88
89    /**
90     * Make a single search request to fetch the RootDSE. Handle the various errors that could occur.
91     *
92     * @throws ConnectionException
93     * @throws OperationException
94     * @throws UnsolicitedNotificationException
95     */
96    public function fetchRootDse(bool $reload = false): Entry
97    {
98        if ($reload === false && $this->rootDse !== null) {
99            return $this->rootDse;
100        }
101        $message = $this->send(Operations::read('', ...self::ROOTDSE_ATTRIBUTES));
102        if ($message === null) {
103            throw new OperationException('Expected a search response for the RootDSE. None received.');
104        }
105
106        $searchResponse = $message->getResponse();
107        if (!$searchResponse instanceof SearchResponse) {
108            throw new OperationException('Expected a search response for the RootDSE. None received.');
109        }
110
111        $entry = $searchResponse->getEntries()->first();
112        if ($entry === null) {
113            throw new OperationException('Expected a single entry for the RootDSE. None received.');
114        }
115        $this->rootDse = $entry;
116
117        return $entry;
118    }
119
120    /**
121     * @throws ConnectionException
122     * @throws OperationException
123     * @throws UnsolicitedNotificationException
124     */
125    public function send(RequestInterface $request, Control ...$controls): ?LdapMessageResponse
126    {
127        try {
128            $context = new ClientProtocolContext(
129                $request,
130                $controls,
131                $this,
132                $this->queue(),
133                $this->options
134            );
135
136            $messageFrom = $this->protocolHandlerFactory->forRequest($request)->handleRequest($context);
137            $messageTo = $context->messageToSend();
138            if ($messageFrom !== null) {
139                $messageFrom = $this->protocolHandlerFactory->forResponse($messageTo->getRequest(), $messageFrom->getResponse())->handleResponse(
140                    $messageTo,
141                    $messageFrom,
142                    $this->queue(),
143                    $this->options
144                );
145            }
146
147            return $messageFrom;
148        } catch (UnsolicitedNotificationException $exception) {
149            if ($exception->getOid() === ExtendedResponse::OID_NOTICE_OF_DISCONNECTION) {
150                $this->queue()->close();
151                throw new ConnectionException(
152                    sprintf('The remote server has disconnected the session. %s', $exception->getMessage()),
153                    $exception->getCode()
154                );
155            }
156
157            throw $exception;
158        } catch (SocketException $exception) {
159            throw new ConnectionException(
160                $exception->getMessage(),
161                $exception->getCode(),
162                $exception
163            );
164        }
165    }
166
167    /**
168     * @return bool
169     */
170    public function isConnected(): bool
171    {
172        return ($this->queue !== null && $this->queue->isConnected());
173    }
174
175    /**
176     * @throws SocketException
177     */
178    protected function queue(): ClientQueue
179    {
180        if ($this->queue === null) {
181            $this->queue = new ClientQueue($this->pool);
182        }
183
184        return $this->queue;
185    }
186}
187