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\Encoder\EncoderInterface;
15use FreeDSx\Asn1\Exception\EncoderException;
16use FreeDSx\Ldap\Exception\ProtocolException;
17use FreeDSx\Ldap\Exception\UnsolicitedNotificationException;
18use FreeDSx\Ldap\Operation\Response\ExtendedResponse;
19use FreeDSx\Ldap\Protocol\Queue\MessageWrapperInterface;
20use FreeDSx\Socket\Exception\ConnectionException;
21use FreeDSx\Socket\Queue\Asn1MessageQueue;
22use FreeDSx\Socket\Queue\Buffer;
23use FreeDSx\Socket\Socket;
24use function strlen;
25use function substr;
26
27/**
28 * The LDAP Queue class for sending and receiving messages.
29 *
30 * @author Chad Sikorra <Chad.Sikorra@gmail.com>
31 */
32class LdapQueue extends Asn1MessageQueue
33{
34    /**
35     * @var int
36     */
37    protected const BUFFER_SIZE = 8192;
38
39    /**
40     * @var int
41     */
42    protected $id = 0;
43
44    /**
45     * @var Socket
46     */
47    protected $socket;
48
49    /**
50     * @var MessageWrapperInterface|null
51     */
52    protected $messageWrapper;
53
54    public function __construct(Socket $socket, EncoderInterface $encoder = null)
55    {
56        parent::__construct($socket, $encoder ?? new LdapEncoder());
57    }
58
59    /**
60     * Encrypt messages sent by the socket for the queue.
61     *
62     * @return $this
63     * @throws ConnectionException
64     */
65    public function encrypt()
66    {
67        $this->socket->block(true);
68        $this->socket->encrypt(true);
69        $this->socket->block(false);
70
71        return $this;
72    }
73
74    /**
75     * @return bool
76     */
77    public function isEncrypted(): bool
78    {
79        return ($this->socket->isConnected() && $this->socket->isEncrypted());
80    }
81
82    /**
83     * Cleanly close the socket and clear buffer contents.
84     */
85    public function close(): void
86    {
87        $this->socket->close();
88        $this->buffer = false;
89        $this->id = 0;
90    }
91
92    /**
93     * Generates a message ID to be sent out the queue.
94     */
95    public function generateId(): int
96    {
97        return ++$this->id;
98    }
99
100    /**
101     * Get the current ID that the queue is on.
102     */
103    public function currentId(): int
104    {
105        return $this->id;
106    }
107
108    /**
109     * @param MessageWrapperInterface|null $messageWrapper
110     * @return $this
111     */
112    public function setMessageWrapper(?MessageWrapperInterface $messageWrapper)
113    {
114        $this->messageWrapper = $messageWrapper;
115
116        return $this;
117    }
118
119    /**
120     * {@inheritDoc}
121     */
122    protected function unwrap($bytes): Buffer
123    {
124        if ($this->messageWrapper === null) {
125            return parent::unwrap($bytes);
126        }
127
128        return $this->messageWrapper->unwrap($bytes);
129    }
130
131    /**
132     * Send LDAP messages out the socket.
133     *
134     * The logic in the loop is to send the messages in chunks of 8192 bytes to lessen the amount of TCP writes we need
135     * to perform if sending out many messages.
136     *
137     * @param LdapMessage ...$messages
138     * @return static
139     * @throws EncoderException
140     */
141    protected function sendLdapMessage(LdapMessage ...$messages): self
142    {
143        $buffer = '';
144
145        foreach ($messages as $message) {
146            $encoded = $this->encoder->encode($message->toAsn1());
147            $buffer .= $this->messageWrapper !== null ? $this->messageWrapper->wrap($encoded) : $encoded;
148            $bufferLen = strlen($buffer);
149            if ($bufferLen >= self::BUFFER_SIZE) {
150                $this->socket->write(substr($buffer, 0, self::BUFFER_SIZE));
151                $buffer = $bufferLen > self::BUFFER_SIZE ? substr($buffer, self::BUFFER_SIZE) : '';
152            }
153        }
154        if (strlen($buffer) > 0) {
155            $this->socket->write($buffer);
156        }
157
158        return $this;
159    }
160
161    /**
162     * @return bool
163     */
164    public function isConnected(): bool
165    {
166        return $this->socket->isConnected();
167    }
168
169    /**
170     * @throws ConnectionException
171     * @throws ProtocolException
172     * @throws UnsolicitedNotificationException
173     */
174    protected function getAndValidateMessage(?int $id): LdapMessage
175    {
176        $message = parent::getMessage($id);
177
178        /**
179         * This logic exists in the queue because an unsolicited notification can be received at any time. So we cannot
180         * rely on logic in the handler determined for the initial request / response.
181         */
182        if ($message->getMessageId() === 0 && $message instanceof LdapMessageResponse && $message->getResponse() instanceof ExtendedResponse) {
183            /** @var ExtendedResponse $response */
184            $response = $message->getResponse();
185            throw new UnsolicitedNotificationException(
186                $response->getDiagnosticMessage(),
187                $response->getResultCode(),
188                null,
189                (string) $response->getName()
190            );
191        }
192        if ($id !== null && $message->getMessageId() !== $id) {
193            throw new ProtocolException(sprintf(
194                'Expected message ID %s, but received %s',
195                $id,
196                $message->getMessageId()
197            ));
198        }
199
200        return  $message;
201    }
202}
203