File Coverage

blib/lib/Zonemaster/Backend/RPCAPI.pm
Criterion Covered Total %
statement 173 242 71.4
branch 57 106 53.7
condition 51 87 58.6
subroutine 26 33 78.7
pod 0 14 0.0
total 307 482 63.6


line stmt bran cond sub pod time code
1             package Zonemaster::Backend::RPCAPI;
2              
3 2     2   2104890 use strict;
  2         5  
  2         55  
4 2     2   9 use warnings;
  2         4  
  2         45  
5 2     2   21 use 5.14.2;
  2         6  
6              
7             # Public Modules
8 2     2   449 use JSON::PP;
  2         9867  
  2         149  
9 2     2   2204 use DBI qw(:utils);
  2         25709  
  2         505  
10 2     2   20 use Digest::MD5 qw(md5_hex);
  2         4  
  2         98  
11 2     2   725 use String::ShellQuote;
  2         1302  
  2         109  
12 2     2   307 use File::Slurp qw(append_file);
  2         9230  
  2         108  
13 2     2   257 use Zonemaster::LDNS;
  2         25783  
  2         89  
14 2     2   596 use Net::IP::XS qw(:PROC);
  2         51907  
  2         635  
15 2     2   674 use HTML::Entities;
  2         9710  
  2         194  
16              
17             # Zonemaster Modules
18 2     2   290 use Zonemaster::Engine;
  2         1022813  
  2         69  
19 2     2   12 use Zonemaster::Engine::Nameserver;
  2         18  
  2         40  
20 2     2   10 use Zonemaster::Engine::DNSName;
  2         4  
  2         35  
21 2     2   9 use Zonemaster::Engine::Recursor;
  2         3  
  2         33  
22 2     2   791 use Zonemaster::Backend;
  2         4  
  2         49  
23 2     2   525 use Zonemaster::Backend::Config;
  2         6  
  2         65  
24 2     2   664 use Zonemaster::Backend::Translator;
  2         6  
  2         4196  
25              
26             my $recursor = Zonemaster::Engine::Recursor->new;
27              
28             sub new {
29 2     2 0 1004 my ( $type, $params ) = @_;
30              
31 2         6 my $self = {};
32 2         5 bless( $self, $type );
33              
34 2 50 33     21 if ( $params && $params->{db} ) {
35 2         5 eval {
36 2         126 eval "require $params->{db}";
37 2 50       14 die $@ if $@;
38 2         65 $self->{db} = "$params->{db}"->new();
39             };
40 2 50       11 die $@ if $@;
41             }
42             else {
43 0         0 eval {
44 0         0 my $backend_module = "Zonemaster::Backend::DB::" . Zonemaster::Backend::Config->BackendDBType();
45 0         0 eval "require $backend_module";
46 0 0       0 die $@ if $@;
47 0         0 $self->{db} = $backend_module->new();
48             };
49 0 0       0 die $@ if $@;
50             }
51              
52 2         10 return ( $self );
53             }
54              
55             sub version_info {
56 0     0 0 0 my ( $self ) = @_;
57              
58 0         0 my %ver;
59 0         0 $ver{zonemaster_engine} = Zonemaster::Engine->VERSION;
60 0         0 $ver{zonemaster_backend} = Zonemaster::Backend->VERSION;
61              
62 0         0 return \%ver;
63             }
64              
65             sub get_ns_ips {
66 0     0 0 0 my ( $self, $ns_name ) = @_;
67              
68 0         0 my @adresses = map { {$ns_name => $_->short} } $recursor->get_addresses_for($ns_name);
  0         0  
69 0 0       0 @adresses = { $ns_name => '0.0.0.0' } if not @adresses;
70              
71 0         0 return \@adresses;
72             }
73              
74             sub get_data_from_parent_zone {
75 0     0 0 0 my ( $self, $domain ) = @_;
76              
77 0         0 my %result;
78              
79 0         0 my ( $dn, $dn_syntax ) = $self->_check_domain( $domain, 'Domain name' );
80 0 0       0 return $dn_syntax if ( $dn_syntax->{status} eq 'nok' );
81              
82 0         0 my @ns_list;
83             my @ns_names;
84              
85 0         0 my $zone = Zonemaster::Engine->zone( $domain );
86 0         0 push @ns_list, { ns => $_->name->string, ip => $_->address->short} for @{$zone->glue};
  0         0  
87              
88 0         0 my @ds_list;
89              
90 0         0 $zone = Zonemaster::Engine->zone($domain);
91 0         0 my $ds_p = $zone->parent->query_one( $zone->name, 'DS', { dnssec => 1, cd => 1, recurse => 1 } );
92 0 0       0 if ($ds_p) {
93 0         0 my @ds = $ds_p->get_records( 'DS', 'answer' );
94              
95 0         0 foreach my $ds ( @ds ) {
96 0 0       0 next unless $ds->type eq 'DS';
97 0         0 push(@ds_list, { keytag => $ds->keytag, algorithm => $ds->algorithm, digtype => $ds->digtype, digest => $ds->hexdigest });
98             }
99             }
100              
101 0         0 $result{ns_list} = \@ns_list;
102 0         0 $result{ds_list} = \@ds_list;
103              
104 0         0 return \%result;
105             }
106              
107             sub _check_domain {
108 65     65   114 my ( $self, $dn, $type ) = @_;
109              
110 65 50       129 if ( !defined( $dn ) ) {
111 0         0 return ( $dn, { status => 'nok', message => encode_entities( "$type required" ) } );
112             }
113              
114 65 100       194 if ( $dn =~ m/[^[:ascii:]]+/ ) {
115 8 50       25 if ( Zonemaster::LDNS::has_idn() ) {
116 0         0 eval { $dn = Zonemaster::LDNS::to_idn( $dn ); };
  0         0  
117 0 0       0 if ( $@ ) {
118             return (
119 0         0 $dn,
120             {
121             status => 'nok',
122             message => encode_entities( "The domain name cannot be converted to the IDN format" )
123             }
124             );
125             }
126             }
127             else {
128             return (
129 8         33 $dn,
130             {
131             status => 'nok',
132             message =>
133             encode_entities( "$type contains non-ascii characters and IDN conversion is not installed" )
134             }
135             );
136             }
137             }
138              
139 57         78 my @res;
140 57         184 @res = Zonemaster::Engine::Test::Basic->basic00($dn);
141 57 100       95356 if (@res != 0) {
142 4         19 return ( $dn, { status => 'nok', message => encode_entities( "$type name or label outside allowed length" ) } );
143             }
144              
145 53         198 return ( $dn, { status => 'ok', message => 'Syntax ok' } );
146             }
147              
148             sub validate_syntax {
149 31     31 0 9986 my ( $self, $syntax_input ) = @_;
150              
151 31         104 my @allowed_params_keys = (
152             'domain', 'ipv4', 'ipv6', 'ds_info', 'nameservers', 'profile',
153             'advanced', 'client_id', 'client_version', 'user_ip', 'user_location_info', 'config', 'priority', 'queue'
154             );
155              
156 31         94 foreach my $k ( keys %$syntax_input ) {
157             return { status => 'nok', message => encode_entities( "Unknown option [$k] in parameters" ) }
158 161 50       225 unless ( grep { $_ eq $k } @allowed_params_keys );
  2254         3113  
159             }
160              
161 31 100 66     81 if ( ( defined $syntax_input->{nameservers} && @{ $syntax_input->{nameservers} } ) ) {
  30         90  
162 30         36 foreach my $ns_ip ( @{ $syntax_input->{nameservers} } ) {
  30         63  
163 52         87 foreach my $k ( keys %$ns_ip ) {
164 102 50 66     248 delete( $ns_ip->{$k} ) unless ( $k eq 'ns' || $k eq 'ip' );
165             }
166             }
167             }
168              
169 31 100 100     68 if ( ( defined $syntax_input->{ds_info} && @{ $syntax_input->{ds_info} } ) ) {
  30         94  
170 6         12 foreach my $ds_digest ( @{ $syntax_input->{ds_info} } ) {
  6         14  
171 6         14 foreach my $k ( keys %$ds_digest ) {
172 16 50 100     78 delete( $ds_digest->{$k} ) unless ( $k eq 'algorithm' || $k eq 'digest' || $k eq 'digtype' || $k eq 'keytag' );
      100        
      66        
173             }
174             }
175             }
176              
177 31 100       64 if ( defined $syntax_input->{advanced} ) {
178             return { status => 'nok', message => encode_entities( "Invalid 'advanced' option format" ) }
179 2 50 33     17 unless ( $syntax_input->{advanced} eq JSON::PP::false || $syntax_input->{advanced} eq JSON::PP::true );
180             }
181              
182 31 50       175 if ( defined $syntax_input->{ipv4} ) {
183             return { status => 'nok', message => encode_entities( "Invalid IPv4 transport option format" ) }
184             unless ( $syntax_input->{ipv4} eq JSON::PP::false
185             || $syntax_input->{ipv4} eq JSON::PP::true
186             || $syntax_input->{ipv4} eq '1'
187 31 0 33     82 || $syntax_input->{ipv4} eq '0' );
      33        
      33        
188             }
189              
190 31 50       813 if ( defined $syntax_input->{ipv6} ) {
191             return { status => 'nok', message => encode_entities( "Invalid IPv6 transport option format" ) }
192             unless ( $syntax_input->{ipv6} eq JSON::PP::false
193             || $syntax_input->{ipv6} eq JSON::PP::true
194             || $syntax_input->{ipv6} eq '1'
195 31 0 66     55 || $syntax_input->{ipv6} eq '0' );
      33        
      33        
196             }
197              
198 31 100       618 if ( defined $syntax_input->{profile} ) {
199             return { status => 'nok', message => encode_entities( "Invalid profile option format" ) }
200             unless ( $syntax_input->{profile} eq 'default_profile'
201             || $syntax_input->{profile} eq 'test_profile_1'
202 2 0 33     10 || $syntax_input->{profile} eq 'test_profile_2' );
      33        
203             }
204              
205 31         74 my ( $dn, $dn_syntax ) = $self->_check_domain( $syntax_input->{domain}, 'Domain name' );
206              
207 31 100       196 return $dn_syntax if ( $dn_syntax->{status} eq 'nok' );
208              
209 25 100 66     58 if ( defined $syntax_input->{nameservers} && @{ $syntax_input->{nameservers} } ) {
  24         64  
210 24         31 foreach my $ns_ip ( @{ $syntax_input->{nameservers} } ) {
  24         48  
211 34         109 my ( $ns, $ns_syntax ) = $self->_check_domain( $ns_ip->{ns}, "NS [$ns_ip->{ns}]" );
212 34 100       271 return $ns_syntax if ( $ns_syntax->{status} eq 'nok' );
213             }
214              
215 18         27 foreach my $ns_ip ( @{ $syntax_input->{nameservers} } ) {
  18         40  
216             return { status => 'nok', message => encode_entities( "Invalid IP address: [$ns_ip->{ip}]" ) }
217 28 100 100     187 unless ( !$ns_ip->{ip} || ip_is_ipv4( $ns_ip->{ip} ) || ip_is_ipv6( $ns_ip->{ip} ) );
      100        
218             }
219              
220 16         24 foreach my $ds_digest ( @{ $syntax_input->{ds_info} } ) {
  16         55  
221             return {
222             status => 'nok',
223             message => encode_entities( "Invalid algorithm type: [$ds_digest->{algorithm}]" )
224             }
225 6 100       28 if ( $ds_digest->{algorithm} =~ /\D/ );
226             }
227              
228 15         23 foreach my $ds_digest ( @{ $syntax_input->{ds_info} } ) {
  15         25  
229             return {
230             status => 'nok',
231             message => encode_entities( "Invalid digest format: [$ds_digest->{digest}]" )
232             }
233             if (
234             ( length( $ds_digest->{digest} ) != 96 &&
235             length( $ds_digest->{digest} ) != 64 &&
236             length( $ds_digest->{digest} ) != 40 ) ||
237 5 100 66     55 $ds_digest->{digest} =~ /[^A-Fa-f0-9]/
      100        
      100        
238             );
239             }
240             }
241              
242 14         52 return { status => 'ok', message => encode_entities( 'Syntax ok' ) };
243             }
244              
245             sub add_user_ip_geolocation {
246 2     2 0 6 my ( $self, $params ) = @_;
247            
248 2 0 33     13 if ($params->{user_ip}
      33        
249             && Zonemaster::Backend::Config->Maxmind_ISP_DB_File()
250             && Zonemaster::Backend::Config->Maxmind_City_DB_File()
251             ) {
252 0         0 my $ip = new Net::IP::XS($params->{user_ip});
253 0 0       0 if ($ip->iptype() eq 'PUBLIC') {
254 0         0 require Geo::IP;
255 0         0 my $gi = Geo::IP->new(Zonemaster::Backend::Config->Maxmind_ISP_DB_File());
256 0         0 my $isp = $gi->isp_by_addr($params->{user_ip});
257            
258 0         0 require GeoIP2::Database::Reader;
259 0         0 my $reader = GeoIP2::Database::Reader->new(file => Zonemaster::Backend::Config->Maxmind_City_DB_File());
260            
261 0         0 my $city = $reader->city(ip => $params->{user_ip});
262              
263 0         0 $params->{user_location_info}->{isp} = $isp;
264 0         0 $params->{user_location_info}->{country} = $city->country()->name();
265 0         0 $params->{user_location_info}->{city} = $city->city()->name();
266 0         0 $params->{user_location_info}->{longitude} = $city->location()->longitude();
267 0         0 $params->{user_location_info}->{latitude} = $city->location()->latitude();
268             }
269             else {
270 0         0 $params->{user_location_info}->{isp} = "Private IP address";
271             }
272             }
273             }
274              
275             sub start_domain_test {
276 2     2 0 3246 my ( $self, $params ) = @_;
277 2         7 my $result = 0;
278              
279 2 50 33     30 $params->{domain} =~ s/^\.// unless ( !$params->{domain} || $params->{domain} eq '.' );
280 2         12 my $syntax_result = $self->validate_syntax( $params );
281 2 50 33     51 die $syntax_result->{message} unless ( $syntax_result && $syntax_result->{status} eq 'ok' );
282              
283 2 50       11 die "No domain in parameters\n" unless ( $params->{domain} );
284            
285 2 50       7 if ($params->{config}) {
286 0         0 $params->{config} =~ s/[^\w_]//isg;
287 0 0       0 die "Unknown test configuration: [$params->{config}]\n" unless ( Zonemaster::Backend::Config->GetCustomConfigParameter('ZONEMASTER', $params->{config}) );
288             }
289            
290 2         8 $self->add_user_ip_geolocation($params);
291              
292 2         130 $result = $self->{db}->create_new_test( $params->{domain}, $params, 10 );
293              
294 2         116 return $result;
295             }
296              
297             sub test_progress {
298 8     8 0 930 my ( $self, $test_id ) = @_;
299              
300 8         18 my $result = 0;
301              
302 8         31 $result = $self->{db}->test_progress( $test_id );
303              
304 8         48 return $result;
305             }
306              
307             sub get_test_params {
308 0     0 0 0 my ( $self, $test_id ) = @_;
309              
310 0         0 my $result = 0;
311              
312 0         0 $result = $self->{db}->get_test_params( $test_id );
313              
314 0         0 return $result;
315             }
316              
317             sub get_test_results {
318 2     2 0 10 my ( $self, $params ) = @_;
319 2         6 my $result;
320              
321             # my $syntax_result = $self->validate_syntax($params);
322             # die $syntax_result->{message} unless ($syntax_result && $syntax_result->{status} eq 'ok');
323              
324             my $translator;
325 2         108 $translator = Zonemaster::Backend::Translator->new;
326 2         2510 my ( $browser_lang ) = ( $params->{language} =~ /^(\w{2})/ );
327              
328 2 50       11 eval { $translator->data } if $translator; # Provoke lazy loading of translation data
  2         74  
329              
330 2         770 my $test_info = $self->{db}->test_results( $params->{id} );
331 2         8 my @zm_results;
332 2         5 foreach my $test_res ( @{ $test_info->{results} } ) {
  2         11  
333 293         418 my $res;
334 293 100 100     1009 if ( $test_res->{module} eq 'NAMESERVER' ) {
    100 66        
335 35 100       108 $res->{ns} = ( $test_res->{args}->{ns} ) ? ( $test_res->{args}->{ns} ) : ( 'All' );
336             }
337             elsif ($test_res->{module} eq 'SYSTEM'
338             && $test_res->{tag} eq 'POLICY_DISABLED'
339             && $test_res->{args}->{name} eq 'Example' )
340             {
341 3         7 next;
342             }
343              
344 290         535 $res->{module} = $test_res->{module};
345 290         714 $res->{message} = $translator->translate_tag( $test_res, $browser_lang ) . "\n";
346 290         924 $res->{message} =~ s/,/, /isg;
347 290         604 $res->{message} =~ s/;/; /isg;
348 290         598 $res->{level} = $test_res->{level};
349              
350 290 100       758 if ( $test_res->{module} eq 'SYSTEM' ) {
351 16 100       93 if ( $res->{message} =~ /policy\.json/ ) {
    100          
352 3         22 my ( $policy ) = ( $res->{message} =~ /\s(\/.*)$/ );
353 3         10 my $policy_description = 'DEFAULT POLICY';
354 3 50       14 $policy_description = 'SOME OTHER POLICY' if ( $policy =~ /some\/other\/policy\/path/ );
355 3         42 $res->{message} =~ s/$policy/$policy_description/;
356             }
357             elsif ( $res->{message} =~ /config\.json/ ) {
358 3         32 my ( $config ) = ( $res->{message} =~ /\s(\/.*)$/ );
359 3         10 my $config_description = 'DEFAULT CONFIGURATION';
360 3 50       21 $config_description = 'SOME OTHER CONFIGURATION' if ( $config =~ /some\/other\/configuration\/path/ );
361 3         52 $res->{message} =~ s/$config/$config_description/;
362             }
363             }
364              
365 290         722 push( @zm_results, $res );
366             }
367              
368 2         7 $result = $test_info;
369 2         8 $result->{results} = \@zm_results;
370              
371 2         67 return $result;
372             }
373              
374             sub get_test_history {
375 0     0 0 0 my ( $self, $p ) = @_;
376              
377 0         0 my $results = $self->{db}->get_test_history( $p );
378              
379 0         0 return $results;
380             }
381              
382             sub add_api_user {
383 1     1 0 4 my ( $self, $p, undef, $remote_ip ) = @_;
384 1         4 my $result = 0;
385              
386 1         3 my $allow = 0;
387 1 50       7 if ( defined $remote_ip ) {
388 0 0 0     0 $allow = 1 if ( $remote_ip eq '::1' || $remote_ip eq '127.0.0.1' );
389             }
390             else {
391 1         3 $allow = 1;
392             }
393              
394 1 50       4 if ( $allow ) {
395 1 50       10 $result = 1 if ( $self->{db}->add_api_user( $p->{username}, $p->{api_key} ) eq '1' );
396             }
397            
398 1         10 return $result;
399             }
400              
401             =coment
402             sub add_batch_job {
403             my ( $self, $params ) = @_;
404             my $batch_id;
405              
406             if ( $self->{db}->user_authorized( $params->{username}, $params->{api_key} ) ) {
407             $params->{test_params}->{client_id} = 'Zonemaster Batch Scheduler';
408             $params->{test_params}->{client_version} = '1.0';
409             $params->{test_params}->{priority} = 5 unless (defined $params->{test_params}->{priority});
410              
411             $batch_id = $self->{db}->create_new_batch_job( $params->{username} );
412              
413             my $minutes_between_tests_with_same_params = 5;
414             # $self->{db}->dbhandle->begin_work();
415             foreach my $domain ( @{$params->{domains}} ) {
416             $self->{db}
417             ->create_new_test( $domain, $params->{test_params}, 5, $batch_id );
418             }
419             # $self->{db}->dbhandle->commit();
420             }
421             else {
422             die "User $params->{username} not authorized to use batch mode\n";
423             }
424              
425             return $batch_id;
426             }
427             =cut
428              
429             sub add_batch_job {
430 0     0 0   my ( $self, $params ) = @_;
431              
432 0           my $results = $self->{db}->add_batch_job( $params );
433              
434 0           return $results;
435             }
436              
437              
438             sub get_batch_job_result {
439 0     0 0   my ( $self, $batch_id ) = @_;
440              
441 0           return $self->{db}->get_batch_job_result($batch_id);
442             }
443             1;