Completed
Push — master ( 4f6108...97a3dc )
by Carlos
01:33
created

Serializer::readInt()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 1
b 0
f 0
nc 2
nop 3
dl 0
loc 7
ccs 5
cts 5
cp 1
crap 2
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Afonso\Dns;
6
7
class Serializer
8
{
9 6
    public function serializeRequest(Request $request): string
10
    {
11 6
        $bytes = [];
12
13
        /*
14
         * Header
15
         */
16
        // ID (0x00 - 0x15)
17 6
        $bytes = array_merge($bytes, [0xAA, 0xAA]);
18
19
        // QR, OPCODE, AA, TC, RD, RA, RCODE (0x16 - 0x31)
20 6
        $bytes += array_merge($bytes, [0x01, 0x00]);
21
22
        // QDCOUNT (0x32 - 0x47)
23 6
        $bytes += array_merge($bytes, [0x00, 0x01]);
24
25
        // ANCOUNT (0x48 - 0x63)
26 6
        $bytes += array_merge($bytes, [0x00, 0x00]);
27
28
        // NSCOUNT (0x64 - 0x79)
29 6
        $bytes += array_merge($bytes, [0x00, 0x00]);
30
31
        // ARCOUNT (0x80 - 0x95)
32 6
        $bytes += array_merge($bytes, [0x00, 0x00]);
33
34
        /*
35
         * Question
36
         */
37
        // QNAME
38 6
        $qname = [];
39 6
        $labels = explode('.', $request->getName());
40 6
        foreach ($labels as $label) {
41 6
            $labelLength = strlen($label);
42 6
            if ($labelLength > 63) {
43 3
                throw new \InvalidArgumentException(
44 3
                    'At least one of the labels of the specified domain exceeds the allowed maximum length'
45
                );
46
            }
47 3
            $qname = array_merge($qname, [$labelLength]);
48 3
            $qname = array_merge($qname, array_map('ord', str_split($label)));
49
        }
50 3
        $qname = array_merge($qname, [0x00]);
51 3
        $bytes += array_merge($bytes, $qname);
52
53
        // QTYPE
54 3
        $bytes += array_merge($bytes, [0x00, $request->getType()]);
55
56
        // QCLASS
57 3
        $bytes += array_merge($bytes, [0x00, 0x01]);
58
59 3
        return implode(array_map('chr', $bytes));
60
    }
61
62 21
    public function deserializeResponse(string $response): Response
63
    {
64 21
        $responseObj = new Response();
65
66 21
        $bytes = array_map('ord', str_split($response));
67
68
        // To-Do: check that response is actually a response (QR)
69
70
        /*
71
         * Header
72
         */
73 21
        $isAuthoritative = (bool) ($bytes[2] & 0x04);
74 21
        $isRecursionAvailable = (bool) ($bytes[3] & 0x80);
75 21
        $type = $bytes[3] & 0x0F;
76
77 21
        [$_, $qdCount] = $this->readInt($bytes, 4, 2);
0 ignored issues
show
Comprehensibility Best Practice introduced by
This list assign is not used and could be removed.
Loading history...
78 21
        [$_, $anCount] = $this->readInt($bytes, 6, 2);
79 21
        [$_, $nsCount] = $this->readInt($bytes, 8, 2);
0 ignored issues
show
Comprehensibility Best Practice introduced by
This list assign is not used and could be removed.
Loading history...
80 21
        [$_, $arCount] = $this->readInt($bytes, 10, 2);
0 ignored issues
show
Comprehensibility Best Practice introduced by
This list assign is not used and could be removed.
Loading history...
81
82 21
        $responseObj->setAuthoritative($isAuthoritative);
83 21
        $responseObj->setRecursionAvailable($isRecursionAvailable);
84 21
        $responseObj->setType($type);
85
86
        // From now on there are variable-length fields, so we need to keep
87
        // track of the current byte position.
88 21
        $ptr = 12;
89
90
        /*
91
         * Question
92
         */
93
        // QNAME
94 21
        while ($bytes[$ptr] > 0x00) {
95 21
            $labelLength = $bytes[$ptr];
96
            // Just skip the label for the time being
97
            // $label = implode('', array_map('chr', array_slice($bytes, $ptr + 1, $labelLength)));
98 21
            $ptr += $labelLength + 1;
99
        }
100 21
        $ptr++; // End of field
101
102
        // QTYPE
103 21
        $ptr += 2;
104
105
        // CLASS
106 21
        $ptr += 2;
107
108
        /*
109
         * Answer
110
         */
111 21
        for ($i = 0; $i < $anCount; $i++) {
112
            // NAME
113 21
            [$ptr, $name] = $this->readNameField($bytes, $ptr);
114
115
            // TYPE
116 21
            [$ptr, $type] = $this->readInt($bytes, $ptr, 2);
117
118
            // CLASS
119 21
            $ptr += 2;
120
121
            // TTL
122 21
            [$ptr, $ttl] = $this->readInt($bytes, $ptr, 4);
123
124
            // RDATA
125 21
            [$ptr, $rdLength] = $this->readInt($bytes, $ptr, 2);
126 21
            switch ($type) {
127
                case Request::RR_TYPE_A:
128 3
                    $value = $bytes[$ptr++] . '.' . $bytes[$ptr++] . '.' . $bytes[$ptr++] . '.' . $bytes[$ptr++];
129 3
                    break;
130
                case Request::RR_TYPE_NS:
131 3
                    [$ptr, $value] = $this->readNameField($bytes, $ptr);
132 3
                    break;
133
                case Request::RR_TYPE_CNAME:
134 3
                    [$ptr, $value] = $this->readNameField($bytes, $ptr);
135 3
                    break;
136
                case Request::RR_TYPE_SOA:
137 3
                    [$ptr, $primaryNs] = $this->readNameField($bytes, $ptr);
138 3
                    [$ptr, $adminMb] = $this->readNameField($bytes, $ptr);
139 3
                    [$ptr, $serialNo] = $this->readInt($bytes, $ptr, 4);
140 3
                    [$ptr, $refreshInterval] = $this->readInt($bytes, $ptr, 4);
141 3
                    [$ptr, $retryInterval] = $this->readInt($bytes, $ptr, 4);
142 3
                    [$ptr, $expirationLimit] = $this->readInt($bytes, $ptr, 4);
143 3
                    [$ptr, $minimumTtl] = $this->readInt($bytes, $ptr, 4);
144 3
                    $value = "{$primaryNs} {$adminMb} {$serialNo} {$refreshInterval}"
145 3
                        . " {$retryInterval} {$expirationLimit} {$minimumTtl}";
146 3
                    break;
147
                case Request::RR_TYPE_PTR:
148 3
                    [$ptr, $value] = $this->readNameField($bytes, $ptr);
149 3
                    break;
150
                case Request::RR_TYPE_MX:
151 3
                    [$ptr, $preference] = $this->readInt($bytes, $ptr, 2);
152 3
                    [$ptr, $exchanger] = $this->readNameField($bytes, $ptr);
153 3
                    $value = "{$preference} {$exchanger}";
154 3
                    break;
155
                case Request::RR_TYPE_AAAA:
156 3
                    $packed = '';
157 3
                    for ($i = 0; $i < 16; $i++) {
158 3
                        $packed .= chr($bytes[$ptr++]);
159
                    }
160 3
                    $value = inet_ntop($packed);
161 3
                    break;
162
                default:
163
                    throw new \RuntimeException("Reading responses for resource type '{$type}' is not implemented");
164
            }
165
166 21
            $record = new ResourceRecord($name, $type, $ttl, $value);
167 21
            $responseObj->addResourceRecord($record);
168
        }
169
170
        /*
171
         * Authority
172
         */
173
        // Intentionally skipped
174
175
        /*
176
         * Additional
177
         */
178
        // Intentionally skipped
179
180 21
        return $responseObj;
181
    }
182
183 21
    private function readNameField(array $bytes, int $ptr): array
184
    {
185 21
        while (true) {
186
            // Zero byte means we hit end of name field. Break the loop.
187 21
            if ($bytes[$ptr] == 0) {
188 21
                $ptr++;
189 21
                break;
190
            }
191
192 21
            $format = $bytes[$ptr] & 0xC0;
193 21
            $length = $offset = 0;
0 ignored issues
show
Unused Code introduced by
The assignment to $length is dead and can be removed.
Loading history...
194
195 21
            if ($format == 0xC0) { // Pointer format
196 21
                $offset = ($bytes[$ptr++] << 8 | $bytes[$ptr++]) & 0x3F;
197 21
                [$_, $label] = $this->readNameField($bytes, $offset);
198 21
                $labels[] = $label;
199
                // Pointer ends the name field. Break the loop.
200 21
                break;
201 21
            } elseif ($format == 0x00) {  // Label format
202 21
                $length = $bytes[$ptr++] & 0x3F;
203 21
                $labels[] = implode('', array_map('chr', array_slice($bytes, $ptr, $length)));
204 21
                $ptr += $length;
205
            } else {
206
                throw new \RuntimeException("Unrecognized format '${format}' in response");
207
            }
208
        }
209
210 21
        return [$ptr, implode('.', $labels)];
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $labels does not seem to be defined for all execution paths leading up to this point.
Loading history...
211
    }
212
213
    /**
214
     * Reads and returns an integer from the given byte array, at the specified
215
     * offset and with the specified length in bytes.
216
     *
217
     * @param int[] $byteArray
218
     * @param int $ptr
219
     * @param int $bytes
220
     * @return int[] An array of size 2, the first item being the updated
221
     * pointer (after the read operation) and the second item being the actual
222
     * integer that was read.
223
     */
224 21
    private function readInt(array $byteArray, int $ptr, int $bytes): array
225
    {
226 21
        $int = 0;
227 21
        for ($i = $bytes - 1; $i >= 0; $i--) {
228 21
            $int |= $byteArray[$ptr++] << 8 * $i;
229
        }
230 21
        return [$ptr, $int];
231
    }
232
}
233