Completed
Push — master ( 97184e...2b435f )
by Carlos
01:30
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 implements SerializerInterface
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 24
    public function deserializeResponse(string $response): Response
63
    {
64 24
        $responseObj = new Response();
65
66 24
        $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 24
        $isAuthoritative = (bool) ($bytes[2] & 0x04);
74 24
        $isRecursionAvailable = (bool) ($bytes[3] & 0x80);
75 24
        $type = $bytes[3] & 0x0F;
76
77 24
        [$_, $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 24
        [$_, $anCount] = $this->readInt($bytes, 6, 2);
79 24
        [$_, $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 24
        [$_, $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 24
        $responseObj->setAuthoritative($isAuthoritative);
83 24
        $responseObj->setRecursionAvailable($isRecursionAvailable);
84 24
        $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 24
        $ptr = 12;
89
90
        /*
91
         * Question
92
         */
93
        // QNAME
94 24
        while ($bytes[$ptr] > 0x00) {
95 24
            $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 24
            $ptr += $labelLength + 1;
99
        }
100 24
        $ptr++; // End of field
101
102
        // QTYPE
103 24
        $ptr += 2;
104
105
        // CLASS
106 24
        $ptr += 2;
107
108
        /*
109
         * Answer
110
         */
111 24
        for ($i = 0; $i < $anCount; $i++) {
112
            // NAME
113 24
            [$ptr, $name] = $this->readNameField($bytes, $ptr);
114
115
            // TYPE
116 24
            [$ptr, $type] = $this->readInt($bytes, $ptr, 2);
117
118
            // CLASS
119 24
            $ptr += 2;
120
121
            // TTL
122 24
            [$ptr, $ttl] = $this->readInt($bytes, $ptr, 4);
123
124
            // RDATA
125 24
            [$ptr, $rdLength] = $this->readInt($bytes, $ptr, 2);
126 24
            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
                case Request::RR_TYPE_CNAME:
132
                case Request::RR_TYPE_PTR:
133 9
                    [$ptr, $value] = $this->readNameField($bytes, $ptr);
134 9
                    break;
135
                case Request::RR_TYPE_SOA:
136 3
                    [$ptr, $primaryNs] = $this->readNameField($bytes, $ptr);
137 3
                    [$ptr, $adminMb] = $this->readNameField($bytes, $ptr);
138 3
                    [$ptr, $serialNo] = $this->readInt($bytes, $ptr, 4);
139 3
                    [$ptr, $refreshInterval] = $this->readInt($bytes, $ptr, 4);
140 3
                    [$ptr, $retryInterval] = $this->readInt($bytes, $ptr, 4);
141 3
                    [$ptr, $expirationLimit] = $this->readInt($bytes, $ptr, 4);
142 3
                    [$ptr, $minimumTtl] = $this->readInt($bytes, $ptr, 4);
143 3
                    $value = "{$primaryNs} {$adminMb} {$serialNo} {$refreshInterval}"
144 3
                        . " {$retryInterval} {$expirationLimit} {$minimumTtl}";
145 3
                    break;
146
                case Request::RR_TYPE_MX:
147 3
                    [$ptr, $preference] = $this->readInt($bytes, $ptr, 2);
148 3
                    [$ptr, $exchanger] = $this->readNameField($bytes, $ptr);
149 3
                    $value = "{$preference} {$exchanger}";
150 3
                    break;
151
                case Request::RR_TYPE_SRV:
152 3
                    [$ptr, $priority] = $this->readInt($bytes, $ptr, 2);
153 3
                    [$ptr, $weight] = $this->readInt($bytes, $ptr, 2);
154 3
                    [$ptr, $port] = $this->readInt($bytes, $ptr, 2);
155 3
                    [$ptr, $target] = $this->readNameField($bytes, $ptr);
156 3
                    $value = "{$priority} {$weight} {$port} {$target}";
157 3
                    break;
158
                case Request::RR_TYPE_AAAA:
159 3
                    $packed = '';
160 3
                    for ($i = 0; $i < 16; $i++) {
161 3
                        $packed .= chr($bytes[$ptr++]);
162
                    }
163 3
                    $value = inet_ntop($packed);
164 3
                    break;
165
                default:
166
                    throw new \RuntimeException("Reading responses for resource type '{$type}' is not implemented");
167
            }
168
169 24
            $record = new ResourceRecord($name, $type, $ttl, $value);
170 24
            $responseObj->addResourceRecord($record);
171
        }
172
173
        /*
174
         * Authority
175
         */
176
        // Intentionally skipped
177
178
        /*
179
         * Additional
180
         */
181
        // Intentionally skipped
182
183 24
        return $responseObj;
184
    }
185
186 24
    private function readNameField(array $bytes, int $ptr): array
187
    {
188 24
        while (true) {
189
            // Zero byte means we hit end of name field. Break the loop.
190 24
            if ($bytes[$ptr] == 0) {
191 24
                $ptr++;
192 24
                break;
193
            }
194
195 24
            $format = $bytes[$ptr] & 0xC0;
196 24
            $length = $offset = 0;
0 ignored issues
show
Unused Code introduced by
The assignment to $length is dead and can be removed.
Loading history...
197
198 24
            if ($format == 0xC0) { // Pointer format
199 24
                $offset = ($bytes[$ptr++] << 8 | $bytes[$ptr++]) & 0x3F;
200 24
                [$_, $label] = $this->readNameField($bytes, $offset);
201 24
                $labels[] = $label;
202
                // Pointer ends the name field. Break the loop.
203 24
                break;
204 24
            } elseif ($format == 0x00) {  // Label format
205 24
                $length = $bytes[$ptr++] & 0x3F;
206 24
                $labels[] = implode('', array_map('chr', array_slice($bytes, $ptr, $length)));
207 24
                $ptr += $length;
208
            } else {
209
                throw new \RuntimeException("Unrecognized format '${format}' in response");
210
            }
211
        }
212
213 24
        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...
214
    }
215
216
    /**
217
     * Reads and returns an integer from the given byte array, at the specified
218
     * offset and with the specified length in bytes.
219
     *
220
     * @param int[] $byteArray
221
     * @param int $ptr
222
     * @param int $bytes
223
     * @return int[] An array of size 2, the first item being the updated
224
     * pointer (after the read operation) and the second item being the actual
225
     * integer that was read.
226
     */
227 24
    private function readInt(array $byteArray, int $ptr, int $bytes): array
228
    {
229 24
        $int = 0;
230 24
        for ($i = $bytes - 1; $i >= 0; $i--) {
231 24
            $int |= $byteArray[$ptr++] << 8 * $i;
232
        }
233 24
        return [$ptr, $int];
234
    }
235
}
236