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