File Coverage

blib/lib/Zonemaster/Engine/Test/Syntax.pm
Criterion Covered Total %
statement 184 187 98.4
branch 65 74 87.8
condition 13 18 72.2
subroutine 35 35 100.0
pod 14 14 100.0
total 311 328 94.8


line stmt bran cond sub pod time code
1             package Zonemaster::Engine::Test::Syntax;
2              
3 26     26   166 use version; our $VERSION = version->declare("v1.0.3");
  26         60  
  26         188  
4              
5 26     26   2063 use strict;
  26         59  
  26         528  
6 26     26   121 use warnings;
  26         53  
  26         657  
7              
8 26     26   419 use 5.014002;
  26         88  
9              
10 26     26   131 use Zonemaster::Engine;
  26         59  
  26         521  
11 26     26   124 use Zonemaster::Engine::Util;
  26         54  
  26         1427  
12 26     26   159 use Zonemaster::Engine::Recursor;
  26         53  
  26         597  
13 26     26   131 use Zonemaster::Engine::DNSName;
  26         56  
  26         509  
14 26     26   137 use Zonemaster::Engine::TestMethods;
  26         96  
  26         590  
15 26     26   135 use Zonemaster::Engine::Constants qw[:name];
  26         56  
  26         2428  
16              
17 26     26   160 use Carp;
  26         73  
  26         1343  
18              
19 26     26   162 use List::MoreUtils qw[uniq none any];
  26         52  
  26         183  
20 26     26   24965 use Mail::RFC822::Address qw[valid];
  26         16765  
  26         1390  
21 26     26   7351 use Time::Local;
  26         32045  
  26         44125  
22              
23             ###
24             ### Entry points
25             ###
26              
27             sub all {
28 5     5 1 13 my ( $class, $zone ) = @_;
29 5         11 my @results;
30              
31 5 100       19 push @results, $class->syntax01( $zone->name ) if Zonemaster::Engine->config->should_run( 'syntax01' );
32 4 100       19 push @results, $class->syntax02( $zone->name ) if Zonemaster::Engine->config->should_run( 'syntax02' );
33 4 100       21 push @results, $class->syntax03( $zone->name ) if Zonemaster::Engine->config->should_run( 'syntax03' );
34              
35 4 100   4   30 if ( any { $_->tag eq q{ONLY_ALLOWED_CHARS} } @results ) {
  4         113  
36              
37 2         8 foreach my $local_nsname ( uniq map { $_->string } @{ Zonemaster::Engine::TestMethods->method2( $zone ) },
  12         37  
  2         19  
38 2         12 @{ Zonemaster::Engine::TestMethods->method3( $zone ) } )
39             {
40 6 100       23 push @results, $class->syntax04( $local_nsname ) if Zonemaster::Engine->config->should_run( 'syntax04' );
41             }
42              
43 2 100       52 push @results, $class->syntax05( $zone ) if Zonemaster::Engine->config->should_run( 'syntax05' );
44              
45 2 50   8   218 if ( none { $_->tag eq q{NO_RESPONSE_SOA_QUERY} } @results ) {
  8         227  
46 2 100       10 push @results, $class->syntax06( $zone ) if Zonemaster::Engine->config->should_run( 'syntax06' );
47 2 100       11 push @results, $class->syntax07( $zone ) if Zonemaster::Engine->config->should_run( 'syntax07' );
48             }
49              
50 2 100       14 push @results, $class->syntax08( $zone ) if Zonemaster::Engine->config->should_run( 'syntax08' );
51              
52             }
53              
54 4         26 return @results;
55             } ## end sub all
56              
57             ###
58             ### Metadata Exposure
59             ###
60              
61             sub metadata {
62 50     50 1 102 my ( $class ) = @_;
63              
64             return {
65 50         720 syntax01 => [
66             qw(
67             ONLY_ALLOWED_CHARS
68             NON_ALLOWED_CHARS
69             )
70             ],
71             syntax02 => [
72             qw(
73             INITIAL_HYPHEN
74             TERMINAL_HYPHEN
75             NO_ENDING_HYPHENS
76             )
77             ],
78             syntax03 => [
79             qw(
80             DISCOURAGED_DOUBLE_DASH
81             NO_DOUBLE_DASH
82             )
83             ],
84             syntax04 => [
85             qw(
86             NAMESERVER_DISCOURAGED_DOUBLE_DASH
87             NAMESERVER_NON_ALLOWED_CHARS
88             NAMESERVER_NUMERIC_TLD
89             NAMESERVER_SYNTAX_OK
90             )
91             ],
92             syntax05 => [
93             qw(
94             RNAME_MISUSED_AT_SIGN
95             RNAME_NO_AT_SIGN
96             NO_RESPONSE_SOA_QUERY
97             )
98             ],
99             syntax06 => [
100             qw(
101             RNAME_RFC822_INVALID
102             NO_RESPONSE_SOA_QUERY
103             )
104             ],
105             syntax07 => [
106             qw(
107             MNAME_DISCOURAGED_DOUBLE_DASH
108             MNAME_NON_ALLOWED_CHARS
109             MNAME_NUMERIC_TLD
110             MNAME_SYNTAX_OK
111             NO_RESPONSE_SOA_QUERY
112             )
113             ],
114             syntax08 => [
115             qw(
116             MX_DISCOURAGED_DOUBLE_DASH
117             MX_NON_ALLOWED_CHARS
118             MX_NUMERIC_TLD
119             MX_SYNTAX_OK
120             NO_RESPONSE_MX_QUERY
121             )
122             ],
123             };
124             } ## end sub metadata
125              
126             sub translation {
127             return {
128 1     1 1 21 'NAMESERVER_DISCOURAGED_DOUBLE_DASH' =>
129             'Nameserver ({name}) has a label ({label}) with a double hyphen (\'--\') in position 3 and 4 (with a prefix which is not \'xn--\').',
130             'NAMESERVER_NON_ALLOWED_CHARS' => 'Found illegal characters in the nameserver ({name}).',
131             'NAMESERVER_NUMERIC_TLD' => 'Nameserver ({name}) within a \'numeric only\' TLD ({tld}).',
132             'NAMESERVER_SYNTAX_OK' => 'Nameserver ({name}) syntax is valid.',
133             'MNAME_DISCOURAGED_DOUBLE_DASH' =>
134             'SOA MNAME ({name}) has a label ({label}) with a double hyphen (\'--\') in position 3 and 4 (with a prefix which is not \'xn--\').',
135             'MNAME_NON_ALLOWED_CHARS' => 'Found illegal characters in SOA MNAME ({name}).',
136             'MNAME_NUMERIC_TLD' => 'SOA MNAME ({name}) within a \'numeric only\' TLD ({tld}).',
137             'MNAME_SYNTAX_OK' => 'SOA MNAME ({name}) syntax is valid.',
138             'MX_DISCOURAGED_DOUBLE_DASH' =>
139             'Domain name MX ({name}) has a label ({label}) with a double hyphen (\'--\') in position 3 and 4 (with a prefix which is not \'xn--\').',
140             'MX_NON_ALLOWED_CHARS' => 'Found illegal characters in MX ({name}).',
141             'MX_NUMERIC_TLD' => 'Domain name MX ({name}) within a \'numeric only\' TLD ({tld}).',
142             'MX_SYNTAX_OK' => 'Domain name MX ({name}) syntax is valid.',
143             'DISCOURAGED_DOUBLE_DASH' =>
144             'Domain name ({name}) has a label ({label}) with a double hyphen (\'--\') in position 3 and 4 (with a prefix which is not \'xn--\').',
145             'INITIAL_HYPHEN' => 'Domain name ({name}) has a label ({label}) starting with an hyphen (\'-\').',
146             'TERMINAL_HYPHEN' => 'Domain name ({name}) has a label ({label}) ending with an hyphen (\'-\').',
147             'NON_ALLOWED_CHARS' => 'Found illegal characters in the domain name ({name}).',
148             'ONLY_ALLOWED_CHARS' => 'No illegal characters in the domain name ({name}).',
149             'RNAME_MISUSED_AT_SIGN' => 'There must be no misused \'@\' character in the SOA RNAME field ({rname}).',
150             'RNAME_RFC822_INVALID' => 'There must be no illegal characters in the SOA RNAME field ({rname}).',
151             'RNAME_RFC822_VALID' => 'The SOA RNAME field ({rname}) is compliant with RFC2822.',
152             'NO_ENDING_HYPHENS' => 'Both ends of all labels of the domain name ({name}) have no hyphens.',
153             'NO_DOUBLE_DASH' =>
154             'Domain name ({name}) has no label with a double hyphen (\'--\') in position 3 and 4 (with a prefix which is not \'xn--\').',
155             'RNAME_NO_AT_SIGN' => 'There is no misused \'@\' character in the SOA RNAME field ({rname}).',
156             'NO_RESPONSE_SOA_QUERY' => 'No response from nameserver(s) on SOA queries.',
157             'NO_RESPONSE_MX_QUERY' => 'No response from nameserver(s) on MX queries.',
158             };
159             } ## end sub translation
160              
161             sub version {
162 53     53 1 569 return "$Zonemaster::Engine::Test::Syntax::VERSION";
163             }
164              
165             ###
166             ### Tests
167             ###
168              
169             sub syntax01 {
170 8     8 1 25 my ( $class, $item ) = @_;
171 8         14 my @results;
172              
173 8         30 my $name = get_name( $item );
174              
175 8 100       28 if ( _name_has_only_legal_characters( $name ) ) {
176 6         31 push @results,
177             info(
178             ONLY_ALLOWED_CHARS => {
179             name => $name,
180             }
181             );
182             }
183             else {
184 2         8 push @results,
185             info(
186             NON_ALLOWED_CHARS => {
187             name => $name,
188             }
189             );
190             }
191              
192 6         28 return @results;
193             } ## end sub syntax01
194              
195             sub syntax02 {
196 9     9 1 29 my ( $class, $item ) = @_;
197 9         18 my @results;
198              
199 9         99 my $name = get_name( $item );
200              
201 9         18 foreach my $local_label ( @{ $name->labels } ) {
  9         229  
202 25 100       52 if ( _label_starts_with_hyphen( $local_label ) ) {
203 2         11 push @results,
204             info(
205             INITIAL_HYPHEN => {
206             label => $local_label,
207             name => $name,
208             }
209             );
210             }
211 25 100       46 if ( _label_ends_with_hyphen( $local_label ) ) {
212 2         10 push @results,
213             info(
214             TERMINAL_HYPHEN => {
215             label => $local_label,
216             name => $name,
217             }
218             );
219             }
220             } ## end foreach my $local_label ( @...)
221              
222 9 100 66     16 if ( scalar @{ $name->labels } and not scalar @results ) {
  9         215  
223 5         23 push @results,
224             info(
225             NO_ENDING_HYPHENS => {
226             name => $name,
227             }
228             );
229             }
230              
231 9         28 return @results;
232             } ## end sub syntax02
233              
234             sub syntax03 {
235 9     9 1 24 my ( $class, $item ) = @_;
236 9         14 my @results;
237              
238 9         22 my $name = get_name( $item );
239              
240 9         16 foreach my $local_label ( @{ $name->labels } ) {
  9         231  
241 23 100       47 if ( _label_not_ace_has_double_hyphen_in_position_3_and_4( $local_label ) ) {
242 2         10 push @results,
243             info(
244             DISCOURAGED_DOUBLE_DASH => {
245             label => $local_label,
246             name => $name,
247             }
248             );
249             }
250             }
251              
252 9 100 66     14 if ( scalar @{ $name->labels } and not scalar @results ) {
  9         212  
253 7         33 push @results,
254             info(
255             NO_DOUBLE_DASH => {
256             name => $name,
257             }
258             );
259             }
260              
261 9         29 return @results;
262             } ## end sub syntax03
263              
264             sub syntax04 {
265 7     7 1 18 my ( $class, $item ) = @_;
266 7         11 my @results;
267              
268 7         20 my $name = get_name( $item );
269              
270 7         72 push @results, check_name_syntax( q{NAMESERVER}, $name );
271              
272 7         92 return @results;
273             }
274              
275             sub syntax05 {
276 6     6 1 23 my ( $class, $zone ) = @_;
277 6         14 my @results;
278              
279 6         155 my $p = $zone->query_one( $zone->name, q{SOA} );
280              
281 6 100 66     43 if ( $p and my ( $soa ) = $p->get_records( q{SOA}, q{answer} ) ) {
282 5         62 my $rname = $soa->rname;
283 5         18 $rname =~ s/\\./\./smgx;
284 5 100       18 if ( index( $rname, q{@} ) != -1 ) {
285 2         21 push @results,
286             info(
287             RNAME_MISUSED_AT_SIGN => {
288             rname => $soa->rname,
289             }
290             );
291             }
292             else {
293 3         26 push @results,
294             info(
295             RNAME_NO_AT_SIGN => {
296             rname => $soa->rname,
297             }
298             );
299             }
300             } ## end if ( $p and my ( $soa ...))
301             else {
302 1         6 push @results, info( NO_RESPONSE_SOA_QUERY => {} );
303             }
304              
305 6         51 return @results;
306             } ## end sub syntax05
307              
308             sub syntax06 {
309 9     9 1 31 my ( $class, $zone ) = @_;
310 9         15 my @results;
311              
312 9         232 my $p = $zone->query_one( $zone->name, q{SOA} );
313              
314 9 100 66     73 if ( $p and my ( $soa ) = $p->get_records( q{SOA}, q{answer} ) ) {
315 8         86 my $rname = $soa->rname;
316 8         92 $rname =~ s/([^\\])[.]/$1@/smx; # Replace first non-escaped dot with an at-sign
317 8         22 $rname =~ s/[\\][.]/./smgx; # Un-escape dots
318 8         37 $rname =~ s/[.]\z//smgx; # Validator does not like final dots
319 8 100       45 if ( not valid( $rname ) ) {
320 4         158 push @results,
321             info(
322             RNAME_RFC822_INVALID => {
323             rname => $rname,
324             }
325             );
326             }
327             else {
328 4         2131 push @results,
329             info(
330             RNAME_RFC822_VALID => {
331             rname => $rname,
332             }
333             );
334             }
335             } ## end if ( $p and my ( $soa ...))
336             else {
337 1         4 push @results, info( NO_RESPONSE_SOA_QUERY => {} );
338             }
339 9         90 return @results;
340             } ## end sub syntax06
341              
342             sub syntax07 {
343 7     7 1 21 my ( $class, $zone ) = @_;
344 7         14 my @results;
345              
346 7         178 my $p = $zone->query_one( $zone->name, q{SOA} );
347              
348 7 100 66     51 if ( $p and my ( $soa ) = $p->get_records( q{SOA}, q{answer} ) ) {
349 6         66 my $mname = $soa->mname;
350              
351 6         26 push @results, check_name_syntax( q{MNAME}, $mname );
352             }
353             else {
354 1         6 push @results, info( NO_RESPONSE_SOA_QUERY => {} );
355             }
356              
357 7         43 return @results;
358             }
359              
360             sub syntax08 {
361 7     7 1 56 my ( $class, $zone ) = @_;
362 7         16 my @results;
363              
364 7         194 my $p = $zone->query_one( $zone->name, q{MX} );
365              
366 7 50       31 if ( $p ) {
367 7         37 my %mx = map { $_->exchange => 1 } $p->get_records( q{MX}, q{answer} );
  11         95  
368 7         71 foreach my $mx ( sort keys %mx ) {
369 11         36 push @results, check_name_syntax( q{MX}, $mx );
370             }
371             }
372             else {
373 0         0 push @results, info( NO_RESPONSE_MX_QUERY => {} );
374             }
375              
376 7         28 return @results;
377             }
378              
379             ###
380             ### Internal Tests with Boolean (0|1) return value.
381             ###
382              
383             sub _name_has_only_legal_characters {
384 32     32   71 my ( $name ) = @_;
385              
386 32 100   93   108 if ( List::MoreUtils::all { m/\A[-A-Za-z0-9]+\z/smx } @{ $name->labels } ) {
  93         291  
  32         816  
387 30         106 return 1;
388             }
389             else {
390 2         7 return 0;
391             }
392             }
393              
394             sub _label_starts_with_hyphen {
395 25     25   47 my ( $label ) = @_;
396              
397 25 50       41 return 0 if not $label;
398              
399 25 100       55 if ( $label =~ /\A-/smgx ) {
400 2         5 return 1;
401             }
402             else {
403 23         49 return 0;
404             }
405             }
406              
407             sub _label_ends_with_hyphen {
408 25     25   37 my ( $label ) = @_;
409              
410 25 50       48 return 0 if not $label;
411              
412 25 100       51 if ( $label =~ /-\z/smgx ) {
413 2         6 return 1;
414             }
415             else {
416 23         52 return 0;
417             }
418             }
419              
420             sub _label_not_ace_has_double_hyphen_in_position_3_and_4 {
421 98     98   159 my ( $label ) = @_;
422              
423 98 50       168 return 0 if not $label;
424              
425 98 100 100     241 if ( $label =~ /\A..--/smx and $label !~ /\Axn/ismx ) {
426 8         18 return 1;
427             }
428             else {
429 90         192 return 0;
430             }
431             }
432              
433             ###
434             ### Common part for syntax04, syntax07 and syntax08
435             ###
436              
437             sub get_name {
438 57     57 1 107 my ( $item ) = @_;
439 57         97 my $name;
440              
441 57 100       217 if ( not ref $item ) {
    50          
    50          
442 22         78 $name = name( $item );
443             }
444             elsif ( ref( $item ) eq q{Zonemaster::Engine::Zone} ) {
445 0         0 $name = $item->name;
446             }
447             elsif ( ref( $item ) eq q{Zonemaster::Engine::DNSName} ) {
448 35         53 $name = $item;
449             }
450              
451 57         114 return $name;
452             }
453              
454             sub check_name_syntax {
455 24     24 1 68 my ( $info_label_prefix, $name ) = @_;
456 24         39 my @results;
457              
458 24         65 $name = get_name( $name );
459              
460 24 50       70 if ( not _name_has_only_legal_characters( $name ) ) {
461 0         0 push @results,
462             info(
463             $info_label_prefix
464             . q{_NON_ALLOWED_CHARS} => {
465             name => $name,
466             }
467             );
468             }
469              
470 24 50       128 if ( $name ne q{.} ) {
471              
472 24         40 foreach my $local_label ( @{ $name->labels } ) {
  24         590  
473 75 100       131 if ( _label_not_ace_has_double_hyphen_in_position_3_and_4( $local_label ) ) {
474 6         34 push @results,
475             info(
476             $info_label_prefix
477             . q{_DISCOURAGED_DOUBLE_DASH} => {
478             label => $local_label,
479             name => "$name",
480             }
481             );
482             }
483             }
484              
485 24         37 my $tld = @{ $name->labels }[-1];
  24         568  
486 24 100       104 if ( $tld =~ /\A\d+\z/smgx ) {
487 6         29 push @results,
488             info(
489             $info_label_prefix
490             . q{_NUMERIC_TLD} => {
491             name => "$name",
492             tld => $tld,
493             }
494             );
495             }
496              
497             }
498              
499 24 100       74 if ( not scalar @results ) {
500 17         53 push @results,
501             info(
502             $info_label_prefix
503             . q{_SYNTAX_OK} => {
504             name => "$name",
505             }
506             );
507             }
508              
509 24         480 return @results;
510             } ## end sub check_name_syntax
511              
512             1;
513              
514             =head1 NAME
515              
516             Zonemaster::Engine::Test::Syntax - test validating the syntax of host names and other data
517              
518             =head1 SYNOPSIS
519              
520             my @results = Zonemaster::Engine::Test::Syntax->all($zone);
521              
522             =head1 METHODS
523              
524             =over
525              
526             =item all($zone)
527              
528             Runs the default set of tests and returns a list of log entries made by the tests.
529              
530             =item translation()
531              
532             Returns a refernce to a hash with translation data. Used by the builtin translation system.
533              
534             =item metadata()
535              
536             Returns a reference to a hash, the keys of which are the names of all test methods in the module, and the corresponding values are references to
537             lists with all the tags that the method can use in log entries.
538              
539             =item version()
540              
541             Returns a version string for the module.
542              
543             =back
544              
545             =head1 TESTS
546              
547             =over
548              
549             =item syntax01($name)
550              
551             Verifies that the name (Zonemaster::Engine::DNSName) given contains only allowed characters.
552              
553             =item syntax02($name)
554              
555             Verifies that the name (Zonemaster::Engine::DNSName) given does not start or end with a hyphen ('-').
556              
557             =item syntax03($name)
558              
559             Verifies that the name (Zonemaster::Engine::DNSName) given does not contain a hyphen in 3rd and 4th position (in the exception of 'xn--').
560              
561             =item syntax04($name)
562              
563             Verify that a nameserver (Zonemaster::Engine::DNSName) given is conform to previous syntax rules. It also verify name total length as well as labels.
564              
565             =item syntax05($zone)
566              
567             Verify that a SOA rname (Zonemaster::Engine::DNSName) given has a conform usage of at sign (@).
568              
569             =item syntax06($zone)
570              
571             Verify that a SOA rname (Zonemaster::Engine::DNSName) given is RFC822 compliant.
572              
573             =item syntax07($zone)
574              
575             Verify that SOA mname of zone given is conform to previous syntax rules (syntax01, syntax02, syntax03). It also verify name total length as well as labels.
576              
577             =item syntax08(@mx_names)
578              
579             Verify that MX name (Zonemaster::Engine::DNSName) given is conform to previous syntax rules (syntax01, syntax02, syntax03). It also verify name total length as well as labels.
580              
581             =back
582              
583             =head1 INTERNAL METHODS
584              
585             =over
586              
587             =item get_name($item)
588              
589             Converts argument to a L<Zonemaster::Engine::DNSName> object.
590              
591             =item check_name_syntax
592              
593             Implementation of some tests that are used on several kinds of input.
594              
595             =back
596              
597             =cut