Passed
Pull Request — master (#73)
by
unknown
03:13
created

Parser::ptrHandler()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 5.2596

Importance

Changes 0
Metric Value
cc 4
eloc 6
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 13
ccs 4
cts 7
cp 0.5714
crap 5.2596
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Badcow DNS Library.
7
 *
8
 * (c) Samuel Williams <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Badcow\DNS\Parser;
15
16
use Badcow\DNS\Classes;
17
use Badcow\DNS\Rdata\Factory;
18
use Badcow\DNS\Rdata\PTR;
19
use Badcow\DNS\Rdata\RdataInterface;
20
use Badcow\DNS\Rdata\Types;
21
use Badcow\DNS\ResourceRecord;
22
use Badcow\DNS\Zone;
23
24
class Parser
25
{
26
    /**
27
     * @var string
28
     */
29
    private $string;
30
31
    /**
32
     * @var Zone
33
     */
34
    private $zone;
35
36
    /**
37
     * Array of methods that take an ArrayIterator and return an Rdata object. The array key is the Rdata type.
38
     *
39
     * @var callable[]
40
     */
41
    private $rdataHandlers = [];
42
43
    /**
44
     * @var ResourceRecord
45
     */
46
    private $currentResourceRecord;
47
48
    /**
49
     * @var string
50
     */
51
    private $lastStatedDomain;
52
53
    /**
54
     * @var int
55
     */
56
    private $lastStatedTtl;
57
58
    /**
59
     * @var string
60
     */
61
    private $lastStatedClass;
62
63
    /**
64
     * Parser constructor.
65
     *
66
     * @param array $rdataHandlers
67
     */
68 26
    public function __construct(array $rdataHandlers = [])
69
    {
70 26
        $this->rdataHandlers = array_merge(
71 26
            [PTR::TYPE => [$this, 'ptrHandler']],
72 26
            $rdataHandlers
73
        );
74 26
    }
75
76
    /**
77
     * @param string $name
78
     * @param string $zone
79
     * @param int    $commentOptions
80
     *
81
     * @return Zone
82
     *
83
     * @throws ParseException
84
     */
85 25
    public static function parse(string $name, string $zone, int $commentOptions = Comments::NONE): Zone
86
    {
87 25
        return (new self())->makeZone($name, $zone, $commentOptions);
88
    }
89
90
    /**
91
     * @param string $name
92
     * @param string $string
93
     * @param int    $commentOptions
94
     *
95
     * @return Zone
96
     *
97
     * @throws ParseException
98
     */
99 26
    public function makeZone(string $name, string $string, int $commentOptions = Comments::NONE): Zone
100
    {
101 26
        $this->zone = new Zone($name);
102 26
        $this->string = Normaliser::normalise($string, $commentOptions);
103
104 26
        foreach (explode(Tokens::LINE_FEED, $this->string) as $line) {
105 26
            $this->processLine($line);
106
        }
107
108 23
        return $this->zone;
109
    }
110
111
    /**
112
     * @param string $line
113
     *
114
     * @throws ParseException
115
     */
116 26
    private function processLine(string $line): void
117
    {
118 26
        list($entry, $comment) = $this->extractComment($line);
119
120 26
        $this->currentResourceRecord = new ResourceRecord();
121 26
        $this->currentResourceRecord->setComment($comment);
122
123 26
        if ('' === $entry) {
124 2
            $this->zone->addResourceRecord($this->currentResourceRecord);
125
126 2
            return;
127
        }
128
129 26
        $iterator = new ResourceRecordIterator($entry);
130
131 26
        if ($this->isControlEntry($iterator)) {
132 11
            $this->processControlEntry($iterator);
133
134 11
            return;
135
        }
136
137 26
        $this->processEntry($iterator);
138 23
        $this->zone->addResourceRecord($this->currentResourceRecord);
139 23
    }
140
141
    /**
142
     * @param ResourceRecordIterator $iterator
143
     *
144
     * @throws ParseException
145
     */
146 26
    private function processEntry(ResourceRecordIterator $iterator): void
147
    {
148 26
        if ($this->isTTL($iterator)) {
149 15
            $this->currentResourceRecord->setTtl(TimeFormat::toSeconds($iterator->current()));
150 15
            $iterator->next();
151 15
            $this->processEntry($iterator);
152
153 13
            return;
154
        }
155
156 26
        if ($this->isClass($iterator)) {
157 24
            $this->currentResourceRecord->setClass(strtoupper($iterator->current()));
158 24
            $iterator->next();
159 24
            $this->processEntry($iterator);
160
161 22
            return;
162
        }
163
164 26
        if ($this->isResourceName($iterator) && null === $this->currentResourceRecord->getName()) {
0 ignored issues
show
introduced by
The condition null === $this->currentResourceRecord->getName() is always false.
Loading history...
165 25
            $this->currentResourceRecord->setName($iterator->current());
166 25
            $iterator->next();
167 25
            $this->processEntry($iterator);
168
169 23
            return;
170
        }
171
172 26
        if ($this->isType($iterator)) {
173 25
            $this->currentResourceRecord->setRdata($this->extractRdata($iterator));
174 23
            $this->populateWithLastStated();
175
176 23
            return;
177
        }
178
179 1
        throw new ParseException(sprintf('Could not parse entry "%s".', implode(' ', $iterator->getArrayCopy())));
180
    }
181
182
    /**
183
     * If no domain-name, TTL, or class is set on the record, populate object with last stated value.
184
     *
185
     * @see https://www.ietf.org/rfc/rfc1035 Section 5.1
186
     */
187 23
    private function populateWithLastStated(): void
188
    {
189 23
        if (empty($this->currentResourceRecord->getName())) {
190 3
            $this->currentResourceRecord->setName($this->lastStatedDomain);
191
        } else {
192 23
            $this->lastStatedDomain = $this->currentResourceRecord->getName();
193
        }
194
195 23
        if (null === $this->currentResourceRecord->getTtl()) {
196 13
            $this->currentResourceRecord->setTtl($this->lastStatedTtl ?? $this->zone->getDefaultTTl());
197
        } else {
198 13
            $this->lastStatedTtl = $this->currentResourceRecord->getTtl();
199
        }
200
201 23
        if (null === $this->currentResourceRecord->getClass()) {
202
            $this->currentResourceRecord->setClass($this->lastStatedClass);
203
        } else {
204 23
            $this->lastStatedClass = $this->currentResourceRecord->getClass();
205
        }
206 23
    }
207
208
    /**
209
     * Processes control entries at the top of a BIND record, i.e. $ORIGIN, $TTL, $INCLUDE, etc.
210
     *
211
     * @param ResourceRecordIterator $iterator
212
     */
213 11
    private function processControlEntry(ResourceRecordIterator $iterator): void
214
    {
215 11
        if ('$TTL' === strtoupper($iterator->current())) {
216 10
            $iterator->next();
217 10
            $this->zone->setDefaultTtl(TimeFormat::toSeconds($iterator->current()));
218
        }
219
220 11
        if ('$ORIGIN' === strtoupper($iterator->current())) {
221 10
            $iterator->next();
222 10
            $this->zone->setName((string) $iterator->current());
223
        }
224 11
    }
225
226
    /**
227
     * Determine if iterant is a resource name.
228
     *
229
     * @param ResourceRecordIterator $iterator
230
     *
231
     * @return bool
232
     */
233 26
    private function isResourceName(ResourceRecordIterator $iterator): bool
234
    {
235
        // Look ahead and determine if the next token is a TTL, Class, or valid Type.
236 26
        $iterator->next();
237
238 26
        if (!$iterator->valid()) {
239
            return false;
240
        }
241
242 26
        $isName = $this->isTTL($iterator) ||
243 26
            $this->isClass($iterator, 'DOMAIN') ||
244 26
            $this->isType($iterator);
245 26
        $iterator->prev();
246
247 26
        if (!$isName) {
248 25
            return false;
249
        }
250
251 25
        if (0 === $iterator->key()) {
252 25
            return true;
253
        }
254
255 3
        return false;
256
    }
257
258
    /**
259
     * Determine if iterant is a class.
260
     *
261
     * @param ResourceRecordIterator $iterator
262
     * @param string|null            $origin   the previously assumed resource record parameter, either 'TTL' or NULL
263
     *
264
     * @return bool
265
     */
266 26
    private function isClass(ResourceRecordIterator $iterator, $origin = null): bool
267
    {
268 26
        if (!Classes::isValid($iterator->current())) {
269 26
            return false;
270
        }
271
272 25
        $iterator->next();
273 25
        if ('TTL' === $origin) {
274 12
            $isClass = $this->isType($iterator);
275
        } else {
276 24
            $isClass = $this->isTTL($iterator, 'CLASS') || $this->isType($iterator);
277
        }
278 25
        $iterator->prev();
279
280 25
        return $isClass;
281
    }
282
283
    /**
284
     * Determine if current iterant is an Rdata type string.
285
     *
286
     * @param ResourceRecordIterator $iterator
287
     *
288
     * @return bool
289
     */
290 26
    private function isType(ResourceRecordIterator $iterator): bool
291
    {
292 26
        return Types::isValid(strtoupper($iterator->current())) || array_key_exists($iterator->current(), $this->rdataHandlers);
293
    }
294
295
    /**
296
     * Determine if iterant is a control entry such as $TTL, $ORIGIN, $INCLUDE, etcetera.
297
     *
298
     * @param ResourceRecordIterator $iterator
299
     *
300
     * @return bool
301
     */
302 26
    private function isControlEntry(ResourceRecordIterator $iterator): bool
303
    {
304 26
        return 1 === preg_match('/^\$[A-Z0-9]+/i', $iterator->current());
305
    }
306
307
    /**
308
     * Determine if the iterant is a TTL (i.e. it is an integer after domain-name).
309
     *
310
     * @param ResourceRecordIterator $iterator
311
     * @param string                 $origin   the previously assumed resource record parameter, either 'CLASS' or NULL
312
     *
313
     * @return bool
314
     */
315 26
    private function isTTL(ResourceRecordIterator $iterator, $origin = null): bool
316
    {
317 26
        if (!TimeFormat::isTimeFormat($iterator->current())) {
318 26
            return false;
319
        }
320
321 24
        if ($iterator->key() < 1) {
322 5
            return false;
323
        }
324
        
325 24
        $iterator->next();
326 24
        if ('CLASS' === $origin) {
327 5
            $isTtl = $this->isType($iterator);
328
        } else {
329 24
            $isTtl = $this->isClass($iterator, 'TTL') || $this->isType($iterator);
330
        }
331 24
        $iterator->prev();
332
333 24
        return $isTtl;
334
    }
335
336
    /**
337
     * Split a DNS zone line into a resource record and a comment.
338
     *
339
     * @param string $rr
340
     *
341
     * @return array [$entry, $comment]
342
     */
343 26
    private function extractComment(string $rr): array
344
    {
345 26
        $string = new StringIterator($rr);
346 26
        $entry = '';
347 26
        $comment = null;
348
349 26
        while ($string->valid()) {
350
            //If a semicolon is within double quotes, it will not be treated as the beginning of a comment.
351 26
            $entry .= $this->extractDoubleQuotedText($string);
352
353 26
            if ($string->is(Tokens::SEMICOLON)) {
354 4
                $string->next();
355 4
                $comment = $string->getRemainingAsString();
356
357 4
                break;
358
            }
359
360 26
            $entry .= $string->current();
361 26
            $string->next();
362
        }
363
364 26
        return [$entry, $comment];
365
    }
366
367
    /**
368
     * Extract text within double quotation context.
369
     *
370
     * @param StringIterator $string
371
     *
372
     * @return string
373
     */
374 26
    private function extractDoubleQuotedText(StringIterator $string): string
375
    {
376 26
        if ($string->isNot(Tokens::DOUBLE_QUOTES)) {
377 26
            return '';
378
        }
379
380 12
        $entry = $string->current();
381 12
        $string->next();
382
383 12
        while ($string->isNot(Tokens::DOUBLE_QUOTES)) {
384
            //If the current char is a backslash, treat the next char as being escaped.
385 12
            if ($string->is(Tokens::BACKSLASH)) {
386 6
                $entry .= $string->current();
387 6
                $string->next();
388
            }
389 12
            $entry .= $string->current();
390 12
            $string->next();
391
        }
392
393 12
        return $entry;
394
    }
395
396
    /**
397
     * @param ResourceRecordIterator $iterator
398
     *
399
     * @return RdataInterface
400
     *
401
     * @throws ParseException
402
     */
403 25
    private function extractRdata(ResourceRecordIterator $iterator): RdataInterface
404
    {
405 25
        $type = strtoupper($iterator->current());
406 25
        $iterator->next();
407
408 25
        if (array_key_exists($type, $this->rdataHandlers)) {
409 4
            return call_user_func($this->rdataHandlers[$type], $iterator);
410
        }
411
412
        try {
413 23
            return Factory::textToRdataType($type, $iterator->getRemainingAsString());
414 2
        } catch (\Exception $exception) {
415 2
            throw new ParseException(sprintf('Could not extract Rdata from resource record "%s".', (string) $iterator), null, $exception);
416
        }
417
    }
418
419
    /**
420
     * This handler addresses the special case where an integer resource name could be confused for a TTL, for instance:
421
     * 50 IN PTR mx1.acme.com.
422
     *
423
     * In the above, if the integer is below 256 then it is assumed to represent an octet of an IPv4 address.
424
     *
425
     * @param ResourceRecordIterator $iterator
426
     *
427
     * @return PTR
428
     */
429 3
    private function ptrHandler(ResourceRecordIterator $iterator): PTR
430
    {
431 3
        if (null === $this->currentResourceRecord->getName() && null !== $this->currentResourceRecord->getTtl()) {
0 ignored issues
show
introduced by
The condition null === $this->currentResourceRecord->getName() is always false.
Loading history...
432
            if ($this->currentResourceRecord->getTtl() < 256) {
433
                $this->currentResourceRecord->setName((string) $this->currentResourceRecord->getTtl());
434
                $this->currentResourceRecord->setTtl(null);
435
            }
436
        }
437
438
        /** @var PTR $ptr */
439 3
        $ptr = PTR::fromText($iterator->getRemainingAsString());
440
441 3
        return $ptr;
442
    }
443
}
444