Completed
Push — master ( abd889...497370 )
by Sam
05:43 queued 12s
created

Parser::isTTL()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 9
c 1
b 0
f 0
nc 4
nop 2
dl 0
loc 15
ccs 9
cts 9
cp 1
crap 4
rs 9.9666
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 25
    public function __construct(array $rdataHandlers = [])
69
    {
70 25
        $this->rdataHandlers = array_merge(
71 25
            [PTR::TYPE => [$this, 'ptrHandler']],
72 25
            $rdataHandlers
73
        );
74 25
    }
75
76
    /**
77
     * @param string $name
78
     * @param string $zone
79
     * @param int    $commentOptions
80
     *
81
     * @return Zone
82
     *
83
     * @throws ParseException
84
     */
85 24
    public static function parse(string $name, string $zone, int $commentOptions = Comments::NONE): Zone
86
    {
87 24
        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 25
    public function makeZone(string $name, string $string, int $commentOptions = Comments::NONE): Zone
100
    {
101 25
        $this->zone = new Zone($name);
102 25
        $this->string = Normaliser::normalise($string, $commentOptions);
103
104 25
        foreach (explode(Tokens::LINE_FEED, $this->string) as $line) {
105 25
            $this->processLine($line);
106
        }
107
108 22
        return $this->zone;
109
    }
110
111
    /**
112
     * @param string $line
113
     *
114
     * @throws ParseException
115
     */
116 25
    private function processLine(string $line): void
117
    {
118 25
        list($entry, $comment) = $this->extractComment($line);
119
120 25
        $this->currentResourceRecord = new ResourceRecord();
121 25
        $this->currentResourceRecord->setComment($comment);
122
123 25
        if ('' === $entry) {
124 2
            $this->zone->addResourceRecord($this->currentResourceRecord);
125
126 2
            return;
127
        }
128
129 25
        $iterator = new ResourceRecordIterator($entry);
130
131 25
        if ($this->isControlEntry($iterator)) {
132 10
            $this->processControlEntry($iterator);
133
134 10
            return;
135
        }
136
137 25
        $this->processEntry($iterator);
138 22
        $this->zone->addResourceRecord($this->currentResourceRecord);
139 22
    }
140
141
    /**
142
     * @param ResourceRecordIterator $iterator
143
     *
144
     * @throws ParseException
145
     */
146 25
    private function processEntry(ResourceRecordIterator $iterator): void
147
    {
148 25
        if ($this->isTTL($iterator)) {
149 14
            $this->currentResourceRecord->setTtl((int) $iterator->current());
150 14
            $iterator->next();
151 14
            $this->processEntry($iterator);
152
153 12
            return;
154
        }
155
156 25
        if ($this->isClass($iterator)) {
157 23
            $this->currentResourceRecord->setClass(strtoupper($iterator->current()));
158 23
            $iterator->next();
159 23
            $this->processEntry($iterator);
160
161 21
            return;
162
        }
163
164 25
        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 24
            $this->currentResourceRecord->setName($iterator->current());
166 24
            $iterator->next();
167 24
            $this->processEntry($iterator);
168
169 22
            return;
170
        }
171
172 25
        if ($this->isType($iterator)) {
173 24
            $this->currentResourceRecord->setRdata($this->extractRdata($iterator));
174 22
            $this->populateWithLastStated();
175
176 22
            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 22
    private function populateWithLastStated(): void
188
    {
189 22
        if (null === $this->currentResourceRecord->getName()) {
0 ignored issues
show
introduced by
The condition null === $this->currentResourceRecord->getName() is always false.
Loading history...
190 3
            $this->currentResourceRecord->setName($this->lastStatedDomain);
191
        } else {
192 22
            $this->lastStatedDomain = $this->currentResourceRecord->getName();
193
        }
194
195 22
        if (null === $this->currentResourceRecord->getTtl()) {
196 13
            $this->currentResourceRecord->setTtl($this->lastStatedTtl);
197
        } else {
198 12
            $this->lastStatedTtl = $this->currentResourceRecord->getTtl();
199
        }
200
201 22
        if (null === $this->currentResourceRecord->getClass()) {
202
            $this->currentResourceRecord->setClass($this->lastStatedClass);
203
        } else {
204 22
            $this->lastStatedClass = $this->currentResourceRecord->getClass();
205
        }
206 22
    }
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 10
    private function processControlEntry(ResourceRecordIterator $iterator): void
214
    {
215 10
        if ('$TTL' === strtoupper($iterator->current())) {
216 10
            $iterator->next();
217 10
            $this->zone->setDefaultTtl((int) $iterator->current());
218
        }
219
220 10
        if ('$ORIGIN' === strtoupper($iterator->current())) {
221 10
            $iterator->next();
222 10
            $this->zone->setName((string) $iterator->current());
223
        }
224 10
    }
225
226
    /**
227
     * Determine if iterant is a resource name.
228
     *
229
     * @param ResourceRecordIterator $iterator
230
     *
231
     * @return bool
232
     */
233 25
    private function isResourceName(ResourceRecordIterator $iterator): bool
234
    {
235
        // Look ahead and determine if the next token is a TTL, Class, or valid Type.
236 25
        $iterator->next();
237
238 25
        if (!$iterator->valid()) {
239
            return false;
240
        }
241
242 25
        $isName = $this->isTTL($iterator) ||
243 25
            $this->isClass($iterator, 'DOMAIN') ||
244 25
            $this->isType($iterator);
245 25
        $iterator->prev();
246
247 25
        if (!$isName) {
248 24
            return false;
249
        }
250
251 24
        if (0 === $iterator->key()) {
252 24
            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 25
    private function isClass(ResourceRecordIterator $iterator, $origin = null): bool
267
    {
268 25
        if (!Classes::isValid($iterator->current())) {
269 25
            return false;
270
        }
271
272 24
        $iterator->next();
273 24
        if ('TTL' === $origin) {
274 11
            $isClass = $this->isType($iterator);
275
        } else {
276 23
            $isClass = $this->isTTL($iterator, 'CLASS') || $this->isType($iterator);
277
        }
278 24
        $iterator->prev();
279
280 24
        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 25
    private function isType(ResourceRecordIterator $iterator): bool
291
    {
292 25
        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 25
    private function isControlEntry(ResourceRecordIterator $iterator): bool
303
    {
304 25
        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).
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 25
    private function isTTL(ResourceRecordIterator $iterator, $origin = null): bool
316
    {
317 25
        if (1 !== preg_match('/^\d+$/', $iterator->current())) {
318 25
            return false;
319
        }
320
321 23
        $iterator->next();
322 23
        if ('CLASS' === $origin) {
323 5
            $isTtl = $this->isType($iterator);
324
        } else {
325 23
            $isTtl = $this->isClass($iterator, 'TTL') || $this->isType($iterator);
326
        }
327 23
        $iterator->prev();
328
329 23
        return $isTtl;
330
    }
331
332
    /**
333
     * Split a DNS zone line into a resource record and a comment.
334
     *
335
     * @param string $rr
336
     *
337
     * @return array [$entry, $comment]
338
     */
339 25
    private function extractComment(string $rr): array
340
    {
341 25
        $string = new StringIterator($rr);
342 25
        $entry = '';
343 25
        $comment = '';
344
345 25
        while ($string->valid()) {
346
            //If a semicolon is within double quotes, it will not be treated as the beginning of a comment.
347 25
            if ($string->is(Tokens::DOUBLE_QUOTES)) {
348 12
                $entry .= $string->current();
349 12
                $string->next();
350
351 12
                while ($string->isNot(Tokens::DOUBLE_QUOTES)) {
352
                    //If the current char is a backslash, treat the next char as being escaped.
353 12
                    if ($string->is(Tokens::BACKSLASH)) {
354 6
                        $entry .= $string->current();
355 6
                        $string->next();
356
                    }
357 12
                    $entry .= $string->current();
358 12
                    $string->next();
359
                }
360
            }
361
362 25
            if ($string->is(Tokens::SEMICOLON)) {
363 3
                $string->next();
364 3
                while ($string->valid()) {
365 3
                    $comment .= $string->current();
366 3
                    $string->next();
367
                }
368
369 3
                break;
370
            }
371
372 25
            $entry .= $string->current();
373 25
            $string->next();
374
        }
375
376 25
        if ('' === $comment) {
377 24
            $comment = null;
378
        }
379
380 25
        return [$entry, $comment];
381
    }
382
383
    /**
384
     * @param ResourceRecordIterator $iterator
385
     *
386
     * @return RdataInterface
387
     *
388
     * @throws ParseException
389
     */
390 24
    private function extractRdata(ResourceRecordIterator $iterator): RdataInterface
391
    {
392 24
        $type = strtoupper($iterator->current());
393 24
        $iterator->next();
394
395 24
        if (array_key_exists($type, $this->rdataHandlers)) {
396 4
            return call_user_func($this->rdataHandlers[$type], $iterator);
397
        }
398
399
        try {
400 22
            return Factory::textToRdataType($type, $iterator->getRemainingAsString());
401 2
        } catch (\Exception $exception) {
402 2
            throw new ParseException(sprintf('Could not extract Rdata from resource record "%s".', (string) $iterator), null, $exception);
403
        }
404
    }
405
406
    /**
407
     * This handler addresses the special case where an integer resource name could be confused for a TTL, for instance:
408
     * 50 IN PTR mx1.acme.com.
409
     *
410
     * In the above, if the integer is below 256 then it is assumed to represent an octet of an IPv4 address.
411
     *
412
     * @param ResourceRecordIterator $iterator
413
     *
414
     * @return PTR
415
     */
416 3
    private function ptrHandler(ResourceRecordIterator $iterator): PTR
417
    {
418 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...
419 1
            if ($this->currentResourceRecord->getTtl() < 256) {
420 1
                $this->currentResourceRecord->setName((string) $this->currentResourceRecord->getTtl());
421 1
                $this->currentResourceRecord->setTtl(null);
422
            }
423
        }
424
425
        /** @var PTR $ptr */
426 3
        $ptr = PTR::fromText($iterator->getRemainingAsString());
427
428 3
        return $ptr;
429
    }
430
}
431