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         62  
  26         149  
4              
5 26     26   2075 use strict;
  26         59  
  26         561  
6 26     26   124 use warnings;
  26         53  
  26         616  
7              
8 26     26   417 use 5.014002;
  26         89  
9              
10 26     26   142 use Zonemaster::Engine;
  26         51  
  26         510  
11 26     26   137 use Zonemaster::Engine::Util;
  26         59  
  26         1493  
12 26     26   208 use Zonemaster::Engine::Recursor;
  26         55  
  26         601  
13 26     26   170 use Zonemaster::Engine::DNSName;
  26         76  
  26         592  
14 26     26   150 use Zonemaster::Engine::TestMethods;
  26         60  
  26         621  
15 26     26   139 use Zonemaster::Engine::Constants qw[:name];
  26         58  
  26         2438  
16              
17 26     26   170 use Carp;
  26         58  
  26         1434  
18              
19 26     26   157 use List::MoreUtils qw[uniq none any];
  26         58  
  26         169  
20 26     26   25332 use Mail::RFC822::Address qw[valid];
  26         16159  
  26         1365  
21 26     26   7122 use Time::Local;
  26         31960  
  26         45722  
22              
23             ###
24             ### Entry points
25             ###
26              
27             sub all {
28 5     5 1 14 my ( $class, $zone ) = @_;
29 5         10 my @results;
30              
31 5 100       15 push @results, $class->syntax01( $zone->name ) if Zonemaster::Engine->config->should_run( 'syntax01' );
32 4 100       15 push @results, $class->syntax02( $zone->name ) if Zonemaster::Engine->config->should_run( 'syntax02' );
33 4 100       13 push @results, $class->syntax03( $zone->name ) if Zonemaster::Engine->config->should_run( 'syntax03' );
34              
35 4 100   4   22 if ( any { $_->tag eq q{ONLY_ALLOWED_CHARS} } @results ) {
  4         110  
36              
37 2         5 foreach my $local_nsname ( uniq map { $_->string } @{ Zonemaster::Engine::TestMethods->method2( $zone ) },
  12         26  
  2         13  
38 2         10 @{ Zonemaster::Engine::TestMethods->method3( $zone ) } )
39             {
40 6 100       19 push @results, $class->syntax04( $local_nsname ) if Zonemaster::Engine->config->should_run( 'syntax04' );
41             }
42              
43 2 100       51 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         194  
46 2 100       10 push @results, $class->syntax06( $zone ) if Zonemaster::Engine->config->should_run( 'syntax06' );
47 2 100       9 push @results, $class->syntax07( $zone ) if Zonemaster::Engine->config->should_run( 'syntax07' );
48             }
49              
50 2 100       12 push @results, $class->syntax08( $zone ) if Zonemaster::Engine->config->should_run( 'syntax08' );
51              
52             }
53              
54 4         23 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         754 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 24 '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 547 return "$Zonemaster::Engine::Test::Syntax::VERSION";
163             }
164              
165             ###
166             ### Tests
167             ###
168              
169             sub syntax01 {
170 8     8 1 23 my ( $class, $item ) = @_;
171 8         11 my @results;
172              
173 8         27 my $name = get_name( $item );
174              
175 8 100       26 if ( _name_has_only_legal_characters( $name ) ) {
176 6         28 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         30 return @results;
193             } ## end sub syntax01
194              
195             sub syntax02 {
196 9     9 1 21 my ( $class, $item ) = @_;
197 9         15 my @results;
198              
199 9         20 my $name = get_name( $item );
200              
201 9         12 foreach my $local_label ( @{ $name->labels } ) {
  9         219  
202 25 100       46 if ( _label_starts_with_hyphen( $local_label ) ) {
203 2         12 push @results,
204             info(
205             INITIAL_HYPHEN => {
206             label => $local_label,
207             name => $name,
208             }
209             );
210             }
211 25 100       45 if ( _label_ends_with_hyphen( $local_label ) ) {
212 2         11 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     14 if ( scalar @{ $name->labels } and not scalar @results ) {
  9         238  
223 5         21 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 23 my ( $class, $item ) = @_;
236 9         15 my @results;
237              
238 9         25 my $name = get_name( $item );
239              
240 9         17 foreach my $local_label ( @{ $name->labels } ) {
  9         228  
241 23 100       48 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     16 if ( scalar @{ $name->labels } and not scalar @results ) {
  9         213  
253 7         32 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 16 my ( $class, $item ) = @_;
266 7         10 my @results;
267              
268 7         18 my $name = get_name( $item );
269              
270 7         19 push @results, check_name_syntax( q{NAMESERVER}, $name );
271              
272 7         88 return @results;
273             }
274              
275             sub syntax05 {
276 6     6 1 20 my ( $class, $zone ) = @_;
277 6         13 my @results;
278              
279 6         156 my $p = $zone->query_one( $zone->name, q{SOA} );
280              
281 6 100 66     54 if ( $p and my ( $soa ) = $p->get_records( q{SOA}, q{answer} ) ) {
282 5         58 my $rname = $soa->rname;
283 5         16 $rname =~ s/\\./\./smgx;
284 5 100       19 if ( index( $rname, q{@} ) != -1 ) {
285 2         22 push @results,
286             info(
287             RNAME_MISUSED_AT_SIGN => {
288             rname => $soa->rname,
289             }
290             );
291             }
292             else {
293 3         29 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         7 push @results, info( NO_RESPONSE_SOA_QUERY => {} );
303             }
304              
305 6         47 return @results;
306             } ## end sub syntax05
307              
308             sub syntax06 {
309 9     9 1 28 my ( $class, $zone ) = @_;
310 9         17 my @results;
311              
312 9         275 my $p = $zone->query_one( $zone->name, q{SOA} );
313              
314 9 100 66     76 if ( $p and my ( $soa ) = $p->get_records( q{SOA}, q{answer} ) ) {
315 8         90 my $rname = $soa->rname;
316 8         84 $rname =~ s/([^\\])[.]/$1@/smx; # Replace first non-escaped dot with an at-sign
317 8         23 $rname =~ s/[\\][.]/./smgx; # Un-escape dots
318 8         42 $rname =~ s/[.]\z//smgx; # Validator does not like final dots
319 8 100       42 if ( not valid( $rname ) ) {
320 4         140 push @results,
321             info(
322             RNAME_RFC822_INVALID => {
323             rname => $rname,
324             }
325             );
326             }
327             else {
328 4         1964 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         5 push @results, info( NO_RESPONSE_SOA_QUERY => {} );
338             }
339 9         85 return @results;
340             } ## end sub syntax06
341              
342             sub syntax07 {
343 7     7 1 23 my ( $class, $zone ) = @_;
344 7         12 my @results;
345              
346 7         184 my $p = $zone->query_one( $zone->name, q{SOA} );
347              
348 7 100 66     55 if ( $p and my ( $soa ) = $p->get_records( q{SOA}, q{answer} ) ) {
349 6         63 my $mname = $soa->mname;
350              
351 6         25 push @results, check_name_syntax( q{MNAME}, $mname );
352             }
353             else {
354 1         4 push @results, info( NO_RESPONSE_SOA_QUERY => {} );
355             }
356              
357 7         44 return @results;
358             }
359              
360             sub syntax08 {
361 7     7 1 21 my ( $class, $zone ) = @_;
362 7         13 my @results;
363              
364 7         179 my $p = $zone->query_one( $zone->name, q{MX} );
365              
366 7 50       29 if ( $p ) {
367 7         33 my %mx = map { $_->exchange => 1 } $p->get_records( q{MX}, q{answer} );
  11         111  
368 7         64 foreach my $mx ( sort keys %mx ) {
369 11         39 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         31 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   68 my ( $name ) = @_;
385              
386 32 100   93   111 if ( List::MoreUtils::all { m/\A[-A-Za-z0-9]+\z/smx } @{ $name->labels } ) {
  93         315  
  32         856  
387 30         137 return 1;
388             }
389             else {
390 2         7 return 0;
391             }
392             }
393              
394             sub _label_starts_with_hyphen {
395 25     25   42 my ( $label ) = @_;
396              
397 25 50       54 return 0 if not $label;
398              
399 25 100       55 if ( $label =~ /\A-/smgx ) {
400 2         6 return 1;
401             }
402             else {
403 23         49 return 0;
404             }
405             }
406              
407             sub _label_ends_with_hyphen {
408 25     25   38 my ( $label ) = @_;
409              
410 25 50       45 return 0 if not $label;
411              
412 25 100       52 if ( $label =~ /-\z/smgx ) {
413 2         5 return 1;
414             }
415             else {
416 23         59 return 0;
417             }
418             }
419              
420             sub _label_not_ace_has_double_hyphen_in_position_3_and_4 {
421 98     98   154 my ( $label ) = @_;
422              
423 98 50       182 return 0 if not $label;
424              
425 98 100 100     265 if ( $label =~ /\A..--/smx and $label !~ /\Axn/ismx ) {
426 8         20 return 1;
427             }
428             else {
429 90         208 return 0;
430             }
431             }
432              
433             ###
434             ### Common part for syntax04, syntax07 and syntax08
435             ###
436              
437             sub get_name {
438 57     57 1 117 my ( $item ) = @_;
439 57         73 my $name;
440              
441 57 100       217 if ( not ref $item ) {
    50          
    50          
442 22         80 $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         50 $name = $item;
449             }
450              
451 57         112 return $name;
452             }
453              
454             sub check_name_syntax {
455 24     24 1 59 my ( $info_label_prefix, $name ) = @_;
456 24         46 my @results;
457              
458 24         71 $name = get_name( $name );
459              
460 24 50       76 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       127 if ( $name ne q{.} ) {
471              
472 24         41 foreach my $local_label ( @{ $name->labels } ) {
  24         565  
473 75 100       152 if ( _label_not_ace_has_double_hyphen_in_position_3_and_4( $local_label ) ) {
474 6         25 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         42 my $tld = @{ $name->labels }[-1];
  24         556  
486 24 100       94 if ( $tld =~ /\A\d+\z/smgx ) {
487 6         27 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       68 if ( not scalar @results ) {
500 17         74 push @results,
501             info(
502             $info_label_prefix
503             . q{_SYNTAX_OK} => {
504             name => "$name",
505             }
506             );
507             }
508              
509 24         485 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