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