File Coverage

blib/lib/Zonemaster/Engine/Config.pm
Criterion Covered Total %
statement 123 132 93.1
branch 27 36 75.0
condition 6 9 66.6
subroutine 27 27 100.0
pod 14 14 100.0
total 197 218 90.3


line stmt bran cond sub pod time code
1             package Zonemaster::Engine::Config;
2              
3 26     26   49906 use version; our $VERSION = version->declare("v1.0.6");
  26         1532  
  26         191  
4              
5 26     26   2746 use 5.014002;
  26         93  
6 26     26   120 use warnings;
  26         58  
  26         712  
7              
8 26     26   484 use Moose;
  26         370801  
  26         169  
9 26     26   163016 use JSON::PP;
  26         10204  
  26         1973  
10 26     26   2014 use File::ShareDir qw[dist_dir dist_file];
  26         25787  
  26         1493  
11 26     26   10152 use File::Slurp;
  26         160188  
  26         1715  
12 26     26   8372 use Hash::Merge;
  26         53510  
  26         1189  
13 26     26   193 use File::Spec;
  26         58  
  26         578  
14              
15 26     26   425 use Zonemaster::Engine;
  26         55  
  26         30397  
16              
17             has 'cfiles' => ( is => 'ro', isa => 'ArrayRef', default => sub { [] } );
18             has 'pfiles' => ( is => 'ro', isa => 'ArrayRef', default => sub { [] } );
19             has 'testcases' => ( is => 'ro', isa => 'HashRef', default => sub { {} } );
20              
21             my $merger = Hash::Merge->new;
22             $merger->specify_behavior(
23             {
24             'SCALAR' => {
25             'SCALAR' => sub { $_[1] },
26             'ARRAY' => sub { [ $_[0], @{ $_[1] } ] },
27             'HASH' => sub { $_[1] },
28             },
29             'ARRAY' => {
30             'SCALAR' => sub { $_[1] },
31             'ARRAY' => sub { [ @{ $_[1] } ] },
32             'HASH' => sub { $_[1] },
33             },
34             'HASH' => {
35             'SCALAR' => sub { $_[1] },
36             'ARRAY' => sub { [ values %{ $_[0] }, @{ $_[1] } ] },
37             'HASH' => sub { Hash::Merge::_merge_hashes( $_[0], $_[1] ) },
38             },
39             }
40             );
41              
42             our $config;
43             _load_base_config();
44              
45             our $policy = {};
46              
47             sub BUILD {
48 24     24 1 71 my ( $self ) = @_;
49              
50 24         108 foreach my $dir ( _config_directory_list() ) {
51 96         1370 my $cfile = File::Spec->catfile( $dir, 'config.json' );
52 96         278 my $new = eval { decode_json scalar read_file $cfile };
  96         461  
53 96 100       91840 if ( $new ) {
54 24         257 $config = $merger->merge( $config, $new );
55 24         536 push @{ $self->cfiles }, $cfile;
  24         997  
56             }
57              
58 96         1143 my $pfile = File::Spec->catfile( $dir, 'policy.json' );
59 96         265 $new = eval { decode_json scalar read_file $pfile };
  96         333  
60 96 100       1579014 if ( $new ) {
61 24         95 my $tc = $new->{__testcases__};
62 24         79 delete $new->{__testcases__};
63 24         82 foreach my $case ( keys %{$tc} ) {
  24         403  
64 1368         34930 $self->testcases->{$case} = $tc->{$case};
65             }
66 24         287 $policy = $merger->merge( $policy, $new );
67 24         1811 push @{ $self->pfiles }, $pfile;
  24         821  
68             }
69              
70             } ## end foreach my $dir ( _config_directory_list...)
71              
72 24         911 return $self;
73             } ## end sub BUILD
74              
75             sub get {
76 136306     136306 1 236826 my ( $class ) = @_;
77              
78 136306         617587 return $config;
79             }
80              
81             sub policy {
82 29     29 1 62 my ( $class ) = @_;
83              
84 29 50       93 if ( not $policy ) {
85 0         0 _load_base_policy();
86             }
87              
88 29         525 return $policy;
89             }
90              
91             sub _config_directory_list {
92 24     24   50 my @dirlist;
93 24         73 my $makefile_name = 'Zonemaster-Engine'; # This must be the same name as "name" in Makefile.PL
94 24         161 push @dirlist, dist_dir( $makefile_name );
95 24         2291 push @dirlist, '/etc/zonemaster';
96 24         65 push @dirlist, '/usr/local/etc/zonemaster';
97              
98 24         12496 my $dir = ( getpwuid( $> ) )[7];
99 24 50       184 if ( $dir ) {
100 24         107 push @dirlist, $dir . '/.zonemaster';
101             }
102              
103 24         233 return @dirlist;
104             }
105              
106             sub _load_base_config {
107 26     26   672 my $internal = decode_json( join( q{}, <DATA> ) );
108             # my $filename = dist_file( 'Zonemaster', 'config.json' );
109             # my $default = eval { decode_json scalar read_file $filename };
110             #
111             # $internal = $merger->merge( $internal, $default ) if $default;
112              
113 26         73812 $config = $internal;
114              
115 26         69 return;
116             }
117              
118             sub load_module_policy {
119 218     218 1 623 my ( $class, $mod ) = @_;
120              
121 218         706 my $m = 'Zonemaster::Engine::Test::' . $mod;
122 218 100 66     2546 if ( $m->can( 'policy' ) and $m->policy ) {
123 76         336 $policy = $merger->merge( $policy, { $mod => $m->policy } );
124             }
125              
126 218         8757 return;
127             }
128              
129             sub load_config_file {
130 2     2 1 7 my ( $self, $filename ) = @_;
131 2         12 my $new = decode_json scalar read_file $filename;
132              
133 2 50       7183 if ( $new ) {
134 2         20 $config = $merger->merge( $config, $new );
135 2 100 66     103 push @{ $self->cfiles }, $filename if ( ref( $self ) and $self->isa( __PACKAGE__ ) );
  1         61  
136             }
137              
138 2         20 return !!$new;
139             }
140              
141             sub load_policy_file {
142 31     31 1 100 my ( $self, $filename ) = @_;
143              
144 31 50       1022 if ( not -r $filename ) {
145 0         0 foreach my $dir ( _config_directory_list() ) {
146 0         0 my $name = File::Spec->catfile( $dir, $filename );
147 0 0       0 if ( -r $name ) {
148 0         0 $filename = $name;
149 0         0 last;
150             }
151             else {
152 0 0       0 if ( -r $name . '.json' ) {
153 0         0 $filename = $name . '.json';
154 0         0 last;
155             }
156             }
157             }
158             }
159              
160 31         192 my $new = decode_json scalar read_file $filename;
161 31 50       54616 if ( $new ) {
162 31         94 my $tc = $new->{__testcases__};
163 31         82 delete $new->{__testcases__};
164 31         72 foreach my $case ( keys %{$tc} ) {
  31         178  
165 229         5871 $self->testcases->{$case} = $tc->{$case};
166             }
167 31         233 $policy = $merger->merge( $policy, $new );
168 31 100 66     2372 push @{ $self->pfiles }, $filename if ( ref( $self ) and $self->isa( __PACKAGE__ ) );
  30         955  
169             }
170              
171 31         145 return !!$new;
172             } ## end sub load_policy_file
173              
174             sub no_network {
175 52     52 1 153 my ( $class, $value ) = @_;
176              
177 52 100       188 if ( defined( $value ) ) {
178 32         126 $class->get->{no_network} = $value;
179             }
180              
181 52         159 return $class->get->{no_network};
182             }
183              
184             sub ipv4_ok {
185 11058     11058 1 31590 my ( $class, $value ) = @_;
186              
187 11058 100       33016 if ( defined( $value ) ) {
188 14         47 $class->get->{net}{ipv4} = $value;
189             }
190              
191 11058         27630 return $class->get->{net}{ipv4};
192             }
193              
194             sub ipv6_ok {
195 14468     14468 1 41548 my ( $class, $value ) = @_;
196              
197 14468 100       40650 if ( defined( $value ) ) {
198 14         48 $class->get->{net}{ipv6} = $value;
199             }
200              
201 14468         36481 return $class->get->{net}{ipv6};
202             }
203              
204             sub resolver_defaults {
205 21418     21418 1 62446 my ( $class ) = @_;
206              
207 21418         62131 return $class->get->{resolver}{defaults};
208             }
209              
210             sub resolver_source {
211 7     7 1 22 my ( $class, $sourceaddr ) = @_;
212              
213 7 100       24 if ( defined( $sourceaddr ) ) {
214 2         10 $class->get->{resolver}{source} = $sourceaddr;
215             }
216              
217 7         21 return $class->get->{resolver}{source};
218             }
219              
220             sub logfilter {
221 89236     89236 1 170877 my ( $class ) = @_;
222              
223 89236         231554 return $class->get->{logfilter};
224             }
225              
226             sub asnroots {
227 3     3 1 12 my ( $class ) = @_;
228              
229 3         15 return $class->get->{asnroots};
230             }
231              
232             sub should_run {
233 409     409 1 1002 my ( $self, $name ) = @_;
234              
235 409 100       10726 if ( not defined $self->testcases->{$name} ) {
    100          
236 1         5 return 1; # Default to runnings test
237             }
238             elsif ( $self->testcases->{$name} ) {
239 244         2713 return 1;
240             }
241             else {
242 164         606 return;
243             }
244             }
245              
246 26     26   233 no Moose;
  26         50  
  26         224  
247             __PACKAGE__->meta->make_immutable;
248              
249             1;
250              
251             =head1 NAME
252              
253             Zonemaster::Engine::Config - configuration access module for Zonemaster::Engine
254              
255             =head1 SYNOPSIS
256              
257             Zonemaster::Engine->config->no_network(1); # Forbid network traffic
258              
259             my $value = Zonemaster::Engine::Config->get->{key}{subkey}; # Not really recommended way to access config data
260              
261             =head1 LOADING CONFIGURATION
262              
263             Configuration data is loaded in several stages, each one overlaying the result
264             from the previous one (that is, the later in the list take priority over the
265             earlier). The first stage is hardcoded into the source code and loaded while it
266             is being compiled, to make sure that there will always be some basic
267             information available. Later, when the configuration object is first used, the
268             system will look for a file named F<config.json> in each of a list of
269             directories. If the file exists, is readable and contains proper JSON data, it
270             will be loaded and overlaid on the current internal config. The directories
271             are, in order from first checked to last:
272              
273             =over
274              
275             =item The L<Zonemaster::Engine> perl module installation directory
276              
277             This is where the installation process puts the default configuration. It is
278             not meant to be modified by the user, and it will be overwritten when the
279             module is upgraded (or reinstalled for any other reason). If you really need to
280             know where it is, you can either check the log message left when loading it or
281             run this command to find the path:
282              
283             perl -MFile::ShareDir=dist_dir -E 'say dist_dir( "Zonemaster" )'
284              
285             =item /etc/zonemaster
286              
287             Intended to hold system-global configuration changes.
288              
289             =item /usr/local/etc/zonemaster
290              
291             Basically the same as the previous one, but for those who like to keep their
292             locally installed software inside F</usr/local>.
293              
294             =item ~/.zonemaster
295              
296             That is, a F<.zonemaster> directory in the home directory of the current user.
297             Intended, obviously, for configuration changes local to one particular user.
298              
299             =back
300              
301             The possible contents of the JSON data is described further down in this manual
302             page.
303              
304             =head1 METHODS FOR CONFIGURATION ITEMS
305              
306             =over
307              
308             =item no_network([$value])
309              
310             Returns the value of the C<no_network> flag. If given a defined value, sets the value to that value.
311              
312             =item ipv4_ok([$value])
313              
314             Returns the value of the C<ipv4> flag. If given a defined value, sets the value to that value.
315              
316             =item ipv6_ok([$value])
317              
318             Returns the value of the C<ipv6> flag. If given a defined value, sets the value to that value.
319              
320             =item resolver_defaults()
321              
322             Returns a reference to the resolver_defaults hash.
323              
324             =item resolver_source([$addr])
325              
326             Returns the source address all resolver objects should use when sending
327             queries, if one is set. If given an argument, sets the source address to the
328             argument.
329              
330             =item logfilter()
331              
332             Returns a reference to the logfilter hash.
333              
334             =item asnroots()
335              
336             Returns a reference to the list of ASN lookup domains.
337              
338             =back
339              
340             =head1 METHODS
341              
342             =over
343              
344             =item get()
345              
346             Returns a reference to a hash with configuration values.
347              
348             =item policy()
349              
350             Returns a reference to the current policy data. The format of that data is described further down in this document.
351              
352             =item load_policy_file($filename)
353              
354             Load policy information from the given file and merge it into the pre-loaded
355             policy. Information from the loaded file overrides the pre-loaded information
356             when the same keys exist in both places.
357              
358             If the given name does not lead directly to a readable file, each of the usual
359             directories will be checked if the name is there. If the plain name isn't, the
360             suffix C<.json> will be appended and another try will be done. For example, a
361             file F<$HOME/.zonemaster/Example.json> may be loaded by calling this method
362             with the string C<"Example">.
363              
364             =item load_config_file($filename)
365              
366             Load configuration information from the given file and merge it into the pre-loaded config. Information from the loaded file overrides the pre-loaded information when the same keys exist in both places.
367              
368             =item load_module_policy($module)
369              
370             Loads policy data included in a test module. The argument must be the short
371             form (without the initial C<Zonemaster::Engine::Test::>) and correctly capitalized.
372              
373             =item BUILD
374              
375             Internal method only mentioned here to please L<Pod::Coverage>.
376              
377             =item should_run($name)
378              
379             Given a test case name, it returns true if that test case should be included in
380             a test run according to the currently active policy or false if not.
381              
382             =back
383              
384             =head1 CONFIGURATION DATA
385              
386             The configuration data is stored internally in a nested hash (possibly with arrays as values in places). As of this writing, the file format used is JSON.
387              
388             The interesting keys are as follows.
389              
390             =head2 resolver
391              
392             =head3 defaults
393              
394             These are the default flag and timing values used for the resolver objects used to actually send DNS queries.
395              
396             =over
397              
398             =item usevc
399              
400             If set, only use TCP. Default not set.
401              
402             =item retrans
403              
404             The number of seconds between retries. Default 3.
405              
406             =item dnssec
407              
408             If set, sets the DO flag in queries. Default not set.
409              
410             =item recurse
411              
412             If set, sets the RD flag in queries. Default not set (and almost certainly should remain that way).
413              
414             =item retry
415              
416             The number of times a query is sent before we give up. Can be set to zero, although that's not very useful (since no queries will be sent at all). Defaults to 2.
417              
418             =item igntc
419              
420             If set, queries that get truncated UDP responses will be automatically retried over TCP. Default not set.
421              
422             =back
423              
424             =head2 net
425              
426             =over
427              
428             =item ipv4
429              
430             If set, resolver objects are allowed to send queries over IPv4. Default set.
431              
432             =item ipv6
433              
434             If set, resolver objects are allowed to send queries over IPv6. Default set.
435              
436             =back
437              
438             =head2 no_network
439              
440             If set to a true value, network traffic is forbidden. Use when you want to be sure that any data is only taken from a preloaded cache.
441              
442             =head2 asnroots
443              
444             This key must be a list of domain names. The domains will be assumed to be
445             Cymru-style AS lookup zones. Normally only the first name in the list will be
446             used, the rest are backups in case the earlier ones don't work.
447              
448             =head2 logfilter
449              
450             By using this key, the log level of messages can be set in a much more fine-grained way than by the policy file. The intended use is to remove known erroneous results. If you, for example, know that a certain name server is recursive and for some reason should be, you can use this functionality to lower the severity of the complaint about it to a lower level than normal.
451              
452             The data under the C<logfilter> key should be structured like this:
453              
454             Module
455             Tag
456             Array of exceptions
457             "when"
458             Hash with conditions
459             "set"
460             Level to set if all conditions match
461              
462             The hash with conditions should have keys matching the attributes of the log entry that's being filtered (check the translation files to see what they are). The values for the keys should be either a single value that the attribute should be, or an array of values any one of which the attribute should be.
463              
464             A complete entry might could look like this:
465              
466             "SYSTEM": {
467             "FILTER_THIS": [
468             {
469             "when": {
470             "count": 1,
471             "type": ["this", "or"]
472             },
473             "set": "INFO"
474             },
475             {
476             "when": {
477             "count": 128,
478             "type": ["that"]
479             },
480             "set": "INFO"
481             },
482             {
483             "when": {
484             "count": 0
485             },
486             "set": "WARNING"
487             }
488             ]
489             }
490              
491             This would set the level to C<INFO> for any C<SYSTEM:FILTER_THIS> messages that had a C<count> attribute set to 1 and a C<type> attribute set to either C<this> or C<or>.
492             This also would set the level to C<INFO> for any C<SYSTEM:FILTER_THIS> messages that had a C<count> attribute set to 128 and a C<type> attribute set to C<that>.
493             And this would set the level to C<WARNING> for any C<SYSTEM:FILTER_THIS> messages that had a C<count> attribute set to 0.
494              
495             =head1 POLICY DATA
496              
497             Like the configuration data, policy data is stored in JSON format. Structurally, it's a bit less complex. All the keys on the top level, with one exception, are names of test implementation modules (without the C<Zonemaster::Engine::Test::> prefix). Each of those keys hold another hash, where the keys are the tags that the module in question can emit and the values are the the severity levels that should apply to the tags. Any tags that are not found in the policy data will default to level C<DEBUG>.
498              
499             The one exception is a top-level key C<__testcases__>. The value of that must be a hash where the keys are names of test cases from the test specifications, and the corresponding values are booleans specifying if the test case in question should be executed or not. Any missing test cases are treated as if they had the value C<true> set. The test cases C<basic00>, C<basic01> and C<basic02> will be executed even if their values are set to C<false>, since part of their function is to verify that the given name can be tested at all. The values here only apply when test modules are asked to run all their tests. A test case that is set to C<false> here will still run if asked for specifically.
500              
501             The easiest way to create a modified policy is to copy the default one and change the relevant values.
502              
503             =cut
504              
505             __DATA__
506             {
507             "asnroots" : [ "asnlookup.zonemaster.net", "asnlookup.iis.se"],
508             "net" : {
509             "ipv4" : 1,
510             "ipv6" : 1
511             },
512             "no_network" : 0,
513             "resolver" : {
514             "defaults" : {
515             "debug" : 0,
516             "dnssec" : 0,
517             "edns_size" : 0,
518             "igntc" : 0,
519             "recurse" : 0,
520             "retrans" : 3,
521             "retry" : 2,
522             "usevc" : 0
523             }
524             }
525             }