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