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\Asn1;
15use FreeDSx\Asn1\Exception\EncoderException;
16use FreeDSx\Asn1\Exception\PartialPduException;
17use FreeDSx\Asn1\Type\AbstractType;
18use FreeDSx\Asn1\Type\IncompleteType;
19use FreeDSx\Asn1\Type\IntegerType;
20use FreeDSx\Asn1\Type\OctetStringType;
21use FreeDSx\Asn1\Type\SequenceOfType;
22use FreeDSx\Asn1\Type\SequenceType;
23use FreeDSx\Ldap\Control;
24use FreeDSx\Ldap\Control\ControlBag;
25use FreeDSx\Ldap\Exception\ProtocolException;
26use FreeDSx\Ldap\Exception\RuntimeException;
27use FreeDSx\Ldap\Operation\Request;
28use FreeDSx\Ldap\Operation\Response;
29use FreeDSx\Socket\PduInterface;
30use function count;
31
32/**
33 * The LDAP Message envelope (PDU). RFC 4511, 4.1.1
34 *
35 * LDAPMessage ::= SEQUENCE {
36 *     messageID       MessageID,
37 *     protocolOp      CHOICE {
38 *         bindRequest           BindRequest,
39 *         bindResponse          BindResponse,
40 *         unbindRequest         UnbindRequest,
41 *         searchRequest         SearchRequest,
42 *         searchResEntry        SearchResultEntry,
43 *         searchResDone         SearchResultDone,
44 *         searchResRef          SearchResultReference,
45 *         modifyRequest         ModifyRequest,
46 *         modifyResponse        ModifyResponse,
47 *         addRequest            AddRequest,
48 *         addResponse           AddResponse,
49 *         delRequest            DelRequest,
50 *         delResponse           DelResponse,
51 *         modDNRequest          ModifyDNRequest,
52 *         modDNResponse         ModifyDNResponse,
53 *         compareRequest        CompareRequest,
54 *         compareResponse       CompareResponse,
55 *         abandonRequest        AbandonRequest,
56 *         extendedReq           ExtendedRequest,
57 *         extendedResp          ExtendedResponse,
58 *         ...,
59 *         intermediateResponse  IntermediateResponse },
60 *     controls       [0] Controls OPTIONAL }
61 *
62 * MessageID ::= INTEGER (0 ..  maxInt)
63 *
64 * maxInt INTEGER ::= 2147483647 -- (2^^31 - 1) --
65 *
66 * @author Chad Sikorra <Chad.Sikorra@gmail.com>
67 */
68abstract class LdapMessage implements ProtocolElementInterface, PduInterface
69{
70    /**
71     * @var int
72     */
73    protected $messageId;
74
75    /**
76     * @var ControlBag
77     */
78    protected $controls;
79
80    /**
81     * @param int $messageId
82     * @param Control\Control ...$controls
83     */
84    public function __construct(int $messageId, Control\Control ...$controls)
85    {
86        $this->messageId = $messageId;
87        $this->controls = new ControlBag(...$controls);
88    }
89
90    /**
91     * @return int
92     */
93    public function getMessageId(): int
94    {
95        return $this->messageId;
96    }
97
98    /**
99     * Get the controls for this specific message.
100     *
101     * @return ControlBag
102     */
103    public function controls(): ControlBag
104    {
105        return $this->controls;
106    }
107
108    /**
109     * @return AbstractType
110     * @psalm-return SequenceType
111     * @throws EncoderException
112     */
113    public function toAsn1(): AbstractType
114    {
115        $asn1 = Asn1::sequence(
116            Asn1::integer($this->messageId),
117            $this->getOperationAsn1()
118        );
119
120        if (count($this->controls->toArray()) !== 0) {
121            /** @var SequenceOfType $controls */
122            $controls = Asn1::context(0, Asn1::sequenceOf());
123            foreach ($this->controls->toArray() as $control) {
124                $controls->addChild($control->toAsn1());
125            }
126            $asn1->addChild($controls);
127        }
128
129        return $asn1;
130    }
131
132    /**
133     * {@inheritDoc}
134     * @return self
135     * @throws EncoderException
136     * @throws PartialPduException
137     * @throws RuntimeException
138     */
139    public static function fromAsn1(AbstractType $type)
140    {
141        if (!$type instanceof SequenceType) {
142            throw new ProtocolException(sprintf(
143                'Expected an ASN1 sequence type, but got: %s',
144                get_class($type)
145            ));
146        }
147        $count = count($type->getChildren());
148        if ($count < 2) {
149            throw new ProtocolException(sprintf(
150                'Expected an ASN1 sequence with at least 2 elements, but it has %s',
151                count($type->getChildren())
152            ));
153        }
154
155        $controls = [];
156        if ($count > 2) {
157            for ($i = 2; $i < $count; $i++) {
158                $child = $type->getChild($i);
159                if ($child !== null && $child->getTagClass() === AbstractType::TAG_CLASS_CONTEXT_SPECIFIC && $child->getTagNumber() === 0) {
160                    if (!$child instanceof IncompleteType) {
161                        throw new ProtocolException('The ASN1 structure for the controls is malformed.');
162                    }
163                    /** @var SequenceOfType $child */
164                    $child = (new LdapEncoder())->complete($child, AbstractType::TAG_TYPE_SEQUENCE);
165
166                    foreach ($child->getChildren() as $control) {
167                        if (!($control instanceof SequenceType && $control->getChild(0) !== null && $control->getChild(0) instanceof OctetStringType)) {
168                            throw new ProtocolException('The control either is not a sequence or has no OID value attached.');
169                        }
170                        switch ($control->getChild(0)->getValue()) {
171                            case Control\Control::OID_PAGING:
172                                $controls[] = Control\PagingControl::fromAsn1($control);
173                                break;
174                            case Control\Control::OID_SORTING_RESPONSE:
175                                $controls[] = Control\Sorting\SortingResponseControl::fromAsn1($control);
176                                break;
177                            case Control\Control::OID_VLV_RESPONSE:
178                                $controls[] = Control\Vlv\VlvResponseControl::fromAsn1($control);
179                                break;
180                            case Control\Control::OID_DIR_SYNC:
181                                $controls[] = Control\Ad\DirSyncResponseControl::fromAsn1($control);
182                                break;
183                            default:
184                                $controls[] = Control\Control::fromAsn1($control);
185                                break;
186                        }
187                    }
188                }
189            }
190        }
191
192        $messageId = $type->getChild(0);
193        if (!($messageId !== null && $messageId instanceof IntegerType)) {
194            throw new ProtocolException('Expected an LDAP message ID as an ASN.1 integer type. None received.');
195        }
196        /** @var SequenceType|null $opAsn1 */
197        $opAsn1 = $type->getChild(1);
198        if ($opAsn1 === null) {
199            throw new ProtocolException('The LDAP message is malformed.');
200        }
201
202        switch ($opAsn1->getTagNumber()) {
203            case 0:
204                $operation = Request\BindRequest::fromAsn1($opAsn1);
205                break;
206            case 1:
207                $operation = Response\BindResponse::fromAsn1($opAsn1);
208                break;
209            case 2:
210                $operation = Request\UnbindRequest::fromAsn1($opAsn1);
211                break;
212            case 3:
213                $operation = Request\SearchRequest::fromAsn1($opAsn1);
214                break;
215            case 4:
216                $operation = Response\SearchResultEntry::fromAsn1($opAsn1);
217                break;
218            case 5:
219                $operation = Response\SearchResultDone::fromAsn1($opAsn1);
220                break;
221            case 6:
222                $operation = Request\ModifyRequest::fromAsn1($opAsn1);
223                break;
224            case 7:
225                $operation = Response\ModifyResponse::fromAsn1($opAsn1);
226                break;
227            case 8:
228                $operation = Request\AddRequest::fromAsn1($opAsn1);
229                break;
230            case 9:
231                $operation = Response\AddResponse::fromAsn1($opAsn1);
232                break;
233            case 10:
234                $operation = Request\DeleteRequest::fromAsn1($opAsn1);
235                break;
236            case 11:
237                $operation = Response\DeleteResponse::fromAsn1($opAsn1);
238                break;
239            case 12:
240                $operation = Request\ModifyDnRequest::fromAsn1($opAsn1);
241                break;
242            case 13:
243                $operation = Response\ModifyDnResponse::fromAsn1($opAsn1);
244                break;
245            case 14:
246                $operation = Request\CompareRequest::fromAsn1($opAsn1);
247                break;
248            case 15:
249                $operation = Response\CompareResponse::fromAsn1($opAsn1);
250                break;
251            case 19:
252                $operation = Response\SearchResultReference::fromAsn1($opAsn1);
253                break;
254            case 23:
255                $operation = Request\ExtendedRequest::fromAsn1($opAsn1);
256                break;
257            case 24:
258                $operation = Response\ExtendedResponse::fromAsn1($opAsn1);
259                break;
260            case 25:
261                $operation = Response\IntermediateResponse::fromAsn1($opAsn1);
262                break;
263            default:
264                throw new ProtocolException(sprintf(
265                    'The tag %s for the LDAP operation is not supported.',
266                    $opAsn1->getTagNumber()
267                ));
268        }
269
270        return new static(
271            $messageId->getValue(),
272            $operation,
273            ...$controls
274        );
275    }
276
277    /**
278     * @return AbstractType
279     */
280    abstract protected function getOperationAsn1(): AbstractType;
281}
282