Serializer   A
last analyzed

Complexity

Total Complexity 22

Size/Duplication

Total Lines 244
Duplicated Lines 0 %

Test Coverage

Coverage 98.35%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 128
c 5
b 0
f 0
dl 0
loc 244
ccs 119
cts 121
cp 0.9835
rs 10
wmc 22

4 Methods

Rating   Name   Duplication   Size   Complexity  
A serializeRequest() 0 51 3
A readInt() 0 7 2
C deserializeResponse() 0 139 12
A readNameField() 0 28 5
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 ResourceRecord::TYPE_A:
128 3
                    $address = $bytes[$ptr++] . '.' . $bytes[$ptr++] . '.' . $bytes[$ptr++] . '.' . $bytes[$ptr++];
129 3
                    $record = new AResourceRecord($name, $ttl, $address);
130 3
                    break;
131
                case ResourceRecord::TYPE_NS:
132 3
                    [$ptr, $nameserver] = $this->readNameField($bytes, $ptr);
133 3
                    $record = new NsResourceRecord($name, $ttl, $nameserver);
134 3
                    break;
135
                case ResourceRecord::TYPE_CNAME:
136 3
                    [$ptr, $canonicalName] = $this->readNameField($bytes, $ptr);
137 3
                    $record= new CnameResourceRecord($name, $ttl, $canonicalName);
138 3
                    break;
139
                case ResourceRecord::TYPE_PTR:
140 3
                    [$ptr, $targetName] = $this->readNameField($bytes, $ptr);
141 3
                    $record = new PtrResourceRecord($name, $ttl, $targetName);
142 3
                    break;
143
                case ResourceRecord::TYPE_SOA:
144 3
                    [$ptr, $primaryNs] = $this->readNameField($bytes, $ptr);
145 3
                    [$ptr, $adminMb] = $this->readNameField($bytes, $ptr);
146 3
                    [$ptr, $serialNo] = $this->readInt($bytes, $ptr, 4);
147 3
                    [$ptr, $refreshInterval] = $this->readInt($bytes, $ptr, 4);
148 3
                    [$ptr, $retryInterval] = $this->readInt($bytes, $ptr, 4);
149 3
                    [$ptr, $expirationLimit] = $this->readInt($bytes, $ptr, 4);
150 3
                    [$ptr, $minimumTtl] = $this->readInt($bytes, $ptr, 4);
151 3
                    $record = new SoaResourceRecord(
152 3
                        $name,
153 1
                        $ttl,
154 1
                        $primaryNs,
155 1
                        $adminMb,
156 1
                        $serialNo,
157 1
                        $refreshInterval,
158 1
                        $retryInterval,
159 1
                        $expirationLimit,
160 1
                        $minimumTtl
161
                    );
162 3
                    break;
163
                case ResourceRecord::TYPE_MX:
164 3
                    [$ptr, $preference] = $this->readInt($bytes, $ptr, 2);
165 3
                    [$ptr, $exchanger] = $this->readNameField($bytes, $ptr);
166 3
                    $record= new MxResourceRecord($name, $ttl, $preference, $exchanger);
167 3
                    break;
168
                case ResourceRecord::TYPE_SRV:
169 3
                    [$ptr, $priority] = $this->readInt($bytes, $ptr, 2);
170 3
                    [$ptr, $weight] = $this->readInt($bytes, $ptr, 2);
171 3
                    [$ptr, $port] = $this->readInt($bytes, $ptr, 2);
172 3
                    [$ptr, $target] = $this->readNameField($bytes, $ptr);
173 3
                    $record = new SrvResourceRecord($name, $ttl, $priority, $weight, $port, $target);
174 3
                    break;
175
                case ResourceRecord::TYPE_AAAA:
176 3
                    $packed = '';
177 3
                    for ($i = 0; $i < 16; $i++) {
178 3
                        $packed .= chr($bytes[$ptr++]);
179
                    }
180 3
                    $address = inet_ntop($packed);
181 3
                    $record = new AaaaResourceRecord($name, $ttl, $address);
182 3
                    break;
183
                default:
184
                    throw new \RuntimeException("Reading responses for resource type '{$type}' is not implemented");
185
            }
186
187 24
            $responseObj->addResourceRecord($record);
188
        }
189
190
        /*
191
         * Authority
192
         */
193
        // Intentionally skipped
194
195
        /*
196
         * Additional
197
         */
198
        // Intentionally skipped
199
200 24
        return $responseObj;
201
    }
202
203 24
    private function readNameField(array $bytes, int $ptr): array
204
    {
205 24
        while (true) {
206
            // Zero byte means we hit end of name field. Break the loop.
207 24
            if ($bytes[$ptr] == 0) {
208 24
                $ptr++;
209 24
                break;
210
            }
211
212 24
            $format = $bytes[$ptr] & 0xC0;
213 24
            $length = $offset = 0;
0 ignored issues
show
Unused Code introduced by
The assignment to $length is dead and can be removed.
Loading history...
214
215 24
            if ($format == 0xC0) { // Pointer format
216 24
                $offset = ($bytes[$ptr++] << 8 | $bytes[$ptr++]) & 0x3F;
217 24
                [$_, $label] = $this->readNameField($bytes, $offset);
218 24
                $labels[] = $label;
219
                // Pointer ends the name field. Break the loop.
220 24
                break;
221 24
            } elseif ($format == 0x00) {  // Label format
222 24
                $length = $bytes[$ptr++] & 0x3F;
223 24
                $labels[] = implode('', array_map('chr', array_slice($bytes, $ptr, $length)));
224 24
                $ptr += $length;
225
            } else {
226
                throw new \RuntimeException("Unrecognized format '${format}' in response");
227
            }
228
        }
229
230 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...
231
    }
232
233
    /**
234
     * Reads and returns an integer from the given byte array, at the specified
235
     * offset and with the specified length in bytes.
236
     *
237
     * @param int[] $byteArray
238
     * @param int $ptr
239
     * @param int $bytes
240
     * @return int[] An array of size 2, the first item being the updated
241
     * pointer (after the read operation) and the second item being the actual
242
     * integer that was read.
243
     */
244 24
    private function readInt(array $byteArray, int $ptr, int $bytes): array
245
    {
246 24
        $int = 0;
247 24
        for ($i = $bytes - 1; $i >= 0; $i--) {
248 24
            $int |= $byteArray[$ptr++] << 8 * $i;
249
        }
250 24
        return [$ptr, $int];
251
    }
252
}
253