917 lines
25 KiB
Perl
917 lines
25 KiB
Perl
|
|
package Zonemaster::Backend::RPCAPI;
|
||
|
|
|
||
|
|
use strict;
|
||
|
|
use warnings;
|
||
|
|
use 5.14.2;
|
||
|
|
|
||
|
|
# Public Modules
|
||
|
|
use DBI qw(:utils);
|
||
|
|
use Digest::MD5 qw(md5_hex);
|
||
|
|
use File::Slurp qw(append_file);
|
||
|
|
use HTML::Entities;
|
||
|
|
use JSON::PP;
|
||
|
|
use JSON::Validator::Joi;
|
||
|
|
use Log::Any qw($log);
|
||
|
|
use Mojo::JSON::Pointer;
|
||
|
|
use Scalar::Util qw(blessed);
|
||
|
|
use JSON::Validator::Schema::Draft7;
|
||
|
|
use Locale::TextDomain qw[Zonemaster-Backend];
|
||
|
|
use Locale::Messages qw[LC_MESSAGES LC_ALL];
|
||
|
|
use POSIX qw (setlocale);
|
||
|
|
use Encode;
|
||
|
|
|
||
|
|
# Zonemaster Modules
|
||
|
|
use Zonemaster::Engine;
|
||
|
|
use Zonemaster::Engine::Normalization qw( normalize_name trim_space );
|
||
|
|
use Zonemaster::Engine::Profile;
|
||
|
|
use Zonemaster::Engine::Recursor;
|
||
|
|
use Zonemaster::Backend;
|
||
|
|
use Zonemaster::Backend::Config;
|
||
|
|
use Zonemaster::Backend::Translator;
|
||
|
|
use Zonemaster::Backend::Validator;
|
||
|
|
use Zonemaster::Backend::Errors;
|
||
|
|
|
||
|
|
my $zm_validator = Zonemaster::Backend::Validator->new;
|
||
|
|
our %json_schemas;
|
||
|
|
my $recursor = Zonemaster::Engine::Recursor->new;
|
||
|
|
|
||
|
|
sub joi {
|
||
|
|
return JSON::Validator::Joi->new;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub new {
|
||
|
|
my ( $type, $params ) = @_;
|
||
|
|
|
||
|
|
my $self = {};
|
||
|
|
bless( $self, $type );
|
||
|
|
|
||
|
|
if ( ! $params || ! $params->{config} ) {
|
||
|
|
handle_exception("Missing 'config' parameter");
|
||
|
|
}
|
||
|
|
|
||
|
|
$self->{config} = $params->{config};
|
||
|
|
|
||
|
|
my $dbtype;
|
||
|
|
if ( $params->{dbtype} ) {
|
||
|
|
$dbtype = $self->{config}->check_db($params->{dbtype});
|
||
|
|
} else {
|
||
|
|
$dbtype = $self->{config}->DB_engine;
|
||
|
|
}
|
||
|
|
|
||
|
|
$self->_init_db($dbtype);
|
||
|
|
|
||
|
|
$self->{_profiles} = Zonemaster::Backend::Config->load_profiles( #
|
||
|
|
$self->{config}->PUBLIC_PROFILES,
|
||
|
|
$self->{config}->PRIVATE_PROFILES,
|
||
|
|
);
|
||
|
|
|
||
|
|
return ( $self );
|
||
|
|
}
|
||
|
|
|
||
|
|
sub _init_db {
|
||
|
|
my ( $self, $dbtype ) = @_;
|
||
|
|
|
||
|
|
eval {
|
||
|
|
my $dbclass = Zonemaster::Backend::DB->get_db_class( $dbtype );
|
||
|
|
$self->{db} = $dbclass->from_config( $self->{config} );
|
||
|
|
};
|
||
|
|
|
||
|
|
if ($@) {
|
||
|
|
handle_exception("Failed to initialize the [$dbtype] database backend module: [$@]");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
sub handle_exception {
|
||
|
|
my ( $exception ) = @_;
|
||
|
|
|
||
|
|
if ( !$exception->isa('Zonemaster::Backend::Error') ) {
|
||
|
|
my $reason = $exception;
|
||
|
|
$exception = Zonemaster::Backend::Error::Internal->new( reason => $reason );
|
||
|
|
}
|
||
|
|
|
||
|
|
my $log_extra = $exception->as_hash;
|
||
|
|
delete $log_extra->{message};
|
||
|
|
|
||
|
|
if ( $exception->isa('Zonemaster::Backend::Error::Internal') ) {
|
||
|
|
$log->error($exception->as_string, $log_extra);
|
||
|
|
} else {
|
||
|
|
$log->info($exception->as_string, $log_extra);
|
||
|
|
}
|
||
|
|
|
||
|
|
die $exception->as_hash;
|
||
|
|
}
|
||
|
|
|
||
|
|
$json_schemas{version_info} = joi->object->strict;
|
||
|
|
sub version_info {
|
||
|
|
my ( $self ) = @_;
|
||
|
|
|
||
|
|
my %ver;
|
||
|
|
eval {
|
||
|
|
$ver{zonemaster_ldns} = Zonemaster::LDNS->VERSION;
|
||
|
|
$ver{zonemaster_engine} = Zonemaster::Engine->VERSION;
|
||
|
|
$ver{zonemaster_backend} = Zonemaster::Backend->VERSION;
|
||
|
|
};
|
||
|
|
if ($@) {
|
||
|
|
handle_exception( $@ );
|
||
|
|
}
|
||
|
|
|
||
|
|
return \%ver;
|
||
|
|
}
|
||
|
|
|
||
|
|
# Experimental
|
||
|
|
$json_schemas{system_versions} = $json_schemas{version_info};
|
||
|
|
sub system_versions {
|
||
|
|
return version_info( @_ );
|
||
|
|
}
|
||
|
|
|
||
|
|
$json_schemas{profile_names} = joi->object->strict;
|
||
|
|
sub profile_names {
|
||
|
|
my ( $self ) = @_;
|
||
|
|
|
||
|
|
my %profiles;
|
||
|
|
eval { %profiles = $self->{config}->PUBLIC_PROFILES };
|
||
|
|
if ( $@ ) {
|
||
|
|
handle_exception( $@ );
|
||
|
|
}
|
||
|
|
|
||
|
|
return [ keys %profiles ];
|
||
|
|
}
|
||
|
|
|
||
|
|
# Experimental
|
||
|
|
$json_schemas{conf_profiles} = $json_schemas{profile_names};
|
||
|
|
sub conf_profiles {
|
||
|
|
my $result = {
|
||
|
|
profiles => profile_names( @_ )
|
||
|
|
};
|
||
|
|
return $result;
|
||
|
|
}
|
||
|
|
|
||
|
|
# Return the list of language tags supported by get_test_results(). The tags are
|
||
|
|
# derived from the locale tags set in the configuration file.
|
||
|
|
$json_schemas{get_language_tags} = joi->object->strict;
|
||
|
|
sub get_language_tags {
|
||
|
|
my ( $self ) = @_;
|
||
|
|
|
||
|
|
my @lang_tags;
|
||
|
|
eval {
|
||
|
|
my %locales = $self->{config}->LANGUAGE_locale;
|
||
|
|
|
||
|
|
@lang_tags = sort keys %locales;
|
||
|
|
};
|
||
|
|
if ( $@ ) {
|
||
|
|
handle_exception( $@ );
|
||
|
|
}
|
||
|
|
|
||
|
|
return \@lang_tags;
|
||
|
|
}
|
||
|
|
|
||
|
|
# Experimental
|
||
|
|
$json_schemas{conf_languages} = $json_schemas{get_language_tags};
|
||
|
|
sub conf_languages {
|
||
|
|
my $result = {
|
||
|
|
languages => get_language_tags( @_ )
|
||
|
|
};
|
||
|
|
return $result;
|
||
|
|
}
|
||
|
|
|
||
|
|
$json_schemas{get_host_by_name} = {
|
||
|
|
type => 'object',
|
||
|
|
additionalProperties => 0,
|
||
|
|
required => [ 'hostname' ],
|
||
|
|
properties => {
|
||
|
|
hostname => $zm_validator->domain_name
|
||
|
|
}
|
||
|
|
};
|
||
|
|
sub get_host_by_name {
|
||
|
|
my ( $self, $params ) = @_;
|
||
|
|
my @adresses;
|
||
|
|
|
||
|
|
eval {
|
||
|
|
my $ns_name = $params->{hostname};
|
||
|
|
|
||
|
|
@adresses = map { {$ns_name => $_->short} } $recursor->get_addresses_for($ns_name);
|
||
|
|
@adresses = { $ns_name => '0.0.0.0' } if not @adresses;
|
||
|
|
|
||
|
|
};
|
||
|
|
if ($@) {
|
||
|
|
handle_exception( $@ );
|
||
|
|
}
|
||
|
|
|
||
|
|
return \@adresses;
|
||
|
|
}
|
||
|
|
|
||
|
|
# Experimental
|
||
|
|
$json_schemas{lookup_address_records} = $json_schemas{get_host_by_name};
|
||
|
|
sub lookup_address_records {
|
||
|
|
my $result = {
|
||
|
|
address_records => get_host_by_name( @_ )
|
||
|
|
};
|
||
|
|
return $result;
|
||
|
|
}
|
||
|
|
|
||
|
|
$json_schemas{get_data_from_parent_zone} = {
|
||
|
|
type => 'object',
|
||
|
|
additionalProperties => 0,
|
||
|
|
required => [ 'domain' ],
|
||
|
|
properties => {
|
||
|
|
domain => $zm_validator->domain_name,
|
||
|
|
language => $zm_validator->language_tag,
|
||
|
|
}
|
||
|
|
};
|
||
|
|
sub get_data_from_parent_zone {
|
||
|
|
my ( $self, $params ) = @_;
|
||
|
|
|
||
|
|
my $result = eval {
|
||
|
|
my %result;
|
||
|
|
my $domain = $params->{domain};
|
||
|
|
my ( $_errors, $normalized_domain ) = normalize_name( trim_space ( $domain ) );
|
||
|
|
|
||
|
|
my @ns_list;
|
||
|
|
my @ns_names;
|
||
|
|
|
||
|
|
my $zone = Zonemaster::Engine->zone( $normalized_domain );
|
||
|
|
push @ns_list, { ns => $_->name->string, ip => $_->address->short} for @{$zone->glue};
|
||
|
|
|
||
|
|
my @ds_list;
|
||
|
|
|
||
|
|
$zone = Zonemaster::Engine->zone($normalized_domain);
|
||
|
|
my $ds_p = $zone->parent->query_one( $zone->name, 'DS', { dnssec => 1, cd => 1, recurse => 1 } );
|
||
|
|
if ($ds_p) {
|
||
|
|
my @ds = $ds_p->get_records( 'DS', 'answer' );
|
||
|
|
|
||
|
|
foreach my $ds ( @ds ) {
|
||
|
|
next unless $ds->type eq 'DS';
|
||
|
|
push(@ds_list, { keytag => $ds->keytag, algorithm => $ds->algorithm, digtype => $ds->digtype, digest => $ds->hexdigest });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$result{ns_list} = \@ns_list;
|
||
|
|
$result{ds_list} = \@ds_list;
|
||
|
|
return \%result;
|
||
|
|
};
|
||
|
|
if ($@) {
|
||
|
|
handle_exception( $@ );
|
||
|
|
}
|
||
|
|
elsif ($result) {
|
||
|
|
return $result;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Experimental
|
||
|
|
$json_schemas{lookup_delegation_data} = $json_schemas{get_data_from_parent_zone};
|
||
|
|
sub lookup_delegation_data {
|
||
|
|
return get_data_from_parent_zone( @_ );
|
||
|
|
}
|
||
|
|
|
||
|
|
$json_schemas{start_domain_test} = {
|
||
|
|
type => 'object',
|
||
|
|
additionalProperties => 0,
|
||
|
|
required => [ 'domain' ],
|
||
|
|
properties => {
|
||
|
|
domain => $zm_validator->domain_name,
|
||
|
|
ipv4 => joi->boolean->compile,
|
||
|
|
ipv6 => joi->boolean->compile,
|
||
|
|
nameservers => {
|
||
|
|
type => 'array',
|
||
|
|
items => $zm_validator->nameserver
|
||
|
|
},
|
||
|
|
ds_info => {
|
||
|
|
type => 'array',
|
||
|
|
items => $zm_validator->ds_info
|
||
|
|
},
|
||
|
|
profile => $zm_validator->profile_name,
|
||
|
|
client_id => $zm_validator->client_id->compile,
|
||
|
|
client_version => $zm_validator->client_version->compile,
|
||
|
|
config => joi->string->compile,
|
||
|
|
priority => $zm_validator->priority->compile,
|
||
|
|
queue => $zm_validator->queue->compile,
|
||
|
|
language => $zm_validator->language_tag,
|
||
|
|
}
|
||
|
|
};
|
||
|
|
sub start_domain_test {
|
||
|
|
my ( $self, $params ) = @_;
|
||
|
|
|
||
|
|
my $result = 0;
|
||
|
|
eval {
|
||
|
|
$params->{profile} //= "default";
|
||
|
|
$params->{priority} //= 10;
|
||
|
|
$params->{queue} //= 0;
|
||
|
|
|
||
|
|
my $profile = $self->{_profiles}{ $params->{profile} };
|
||
|
|
$params->{ipv4} //= $profile->get( "net.ipv4" );
|
||
|
|
$params->{ipv6} //= $profile->get( "net.ipv6" );
|
||
|
|
|
||
|
|
$result = $self->{db}->create_new_test( $params->{domain}, $params, $self->{config}->ZONEMASTER_age_reuse_previous_test );
|
||
|
|
};
|
||
|
|
if ($@) {
|
||
|
|
handle_exception( $@ );
|
||
|
|
}
|
||
|
|
|
||
|
|
return $result;
|
||
|
|
}
|
||
|
|
|
||
|
|
# Experimental
|
||
|
|
$json_schemas{job_create} = $json_schemas{start_domain_test};
|
||
|
|
sub job_create {
|
||
|
|
my $result = {
|
||
|
|
job_id => start_domain_test( @_ )
|
||
|
|
};
|
||
|
|
return $result;
|
||
|
|
}
|
||
|
|
|
||
|
|
$json_schemas{test_progress} = joi->object->strict->props(
|
||
|
|
test_id => $zm_validator->test_id->required
|
||
|
|
);
|
||
|
|
sub test_progress {
|
||
|
|
my ( $self, $params ) = @_;
|
||
|
|
|
||
|
|
my $result = 0;
|
||
|
|
eval {
|
||
|
|
my $test_id = $params->{test_id};
|
||
|
|
$result = $self->{db}->test_progress( $test_id );
|
||
|
|
};
|
||
|
|
if ($@) {
|
||
|
|
handle_exception( $@ );
|
||
|
|
}
|
||
|
|
|
||
|
|
return $result;
|
||
|
|
}
|
||
|
|
|
||
|
|
# Experimental
|
||
|
|
$json_schemas{job_status} = joi->object->strict->props(
|
||
|
|
job_id => $zm_validator->test_id->required
|
||
|
|
);
|
||
|
|
sub job_status {
|
||
|
|
my ( $self, $params ) = @_;
|
||
|
|
|
||
|
|
$params->{test_id} = delete $params->{job_id};
|
||
|
|
|
||
|
|
my $result = {
|
||
|
|
progress => $self->test_progress( $params )
|
||
|
|
};
|
||
|
|
return $result;
|
||
|
|
}
|
||
|
|
|
||
|
|
$json_schemas{get_test_params} = joi->object->strict->props(
|
||
|
|
test_id => $zm_validator->test_id->required
|
||
|
|
);
|
||
|
|
sub get_test_params {
|
||
|
|
my ( $self, $params ) = @_;
|
||
|
|
|
||
|
|
my $result;
|
||
|
|
eval {
|
||
|
|
my $test_id = $params->{test_id};
|
||
|
|
|
||
|
|
$result = $self->{db}->get_test_params( $test_id );
|
||
|
|
};
|
||
|
|
if ($@) {
|
||
|
|
handle_exception( $@ );
|
||
|
|
}
|
||
|
|
|
||
|
|
return $result;
|
||
|
|
}
|
||
|
|
|
||
|
|
# Experimental
|
||
|
|
$json_schemas{job_params} = joi->object->strict->props(
|
||
|
|
job_id => $zm_validator->test_id->required
|
||
|
|
);
|
||
|
|
sub job_params {
|
||
|
|
my ( $self, $params ) = @_;
|
||
|
|
|
||
|
|
$params->{test_id} = delete $params->{job_id};
|
||
|
|
|
||
|
|
return $self->get_test_params( $params );
|
||
|
|
}
|
||
|
|
|
||
|
|
$json_schemas{get_test_results} = {
|
||
|
|
type => 'object',
|
||
|
|
additionalProperties => 0,
|
||
|
|
required => [ 'id', 'language' ],
|
||
|
|
properties => {
|
||
|
|
id => $zm_validator->test_id->required->compile,
|
||
|
|
language => $zm_validator->language_tag,
|
||
|
|
}
|
||
|
|
};
|
||
|
|
sub get_test_results {
|
||
|
|
my ( $self, $params ) = @_;
|
||
|
|
|
||
|
|
my $result;
|
||
|
|
eval{
|
||
|
|
|
||
|
|
my $locale = $self->_get_locale( $params );
|
||
|
|
|
||
|
|
my $translator;
|
||
|
|
$translator = Zonemaster::Backend::Translator->instance();
|
||
|
|
|
||
|
|
my $previous_locale = $translator->locale;
|
||
|
|
if ( !$translator->locale( $locale ) ) {
|
||
|
|
die "Failed to set locale: $locale";
|
||
|
|
}
|
||
|
|
|
||
|
|
eval { $translator->data } if $translator; # Provoke lazy loading of translation data
|
||
|
|
|
||
|
|
my @zm_results;
|
||
|
|
my %testcases;
|
||
|
|
|
||
|
|
my $test_info = $self->{db}->test_results( $params->{id} );
|
||
|
|
foreach my $test_res ( @{ $test_info->{results} } ) {
|
||
|
|
my $res;
|
||
|
|
if ( $test_res->{module} eq 'Nameserver' ) {
|
||
|
|
$res->{ns} = ( $test_res->{args}->{ns} ) ? ( $test_res->{args}->{ns} ) : ( 'All' );
|
||
|
|
}
|
||
|
|
elsif ($test_res->{module} eq 'SYSTEM'
|
||
|
|
&& $test_res->{tag} eq 'POLICY_DISABLED'
|
||
|
|
&& $test_res->{args}->{name} eq 'Example' )
|
||
|
|
{
|
||
|
|
next;
|
||
|
|
}
|
||
|
|
|
||
|
|
$res->{module} = $test_res->{module};
|
||
|
|
$res->{message} = $translator->translate_tag( $test_res ) . "\n";
|
||
|
|
$res->{message} =~ s/,/, /isg;
|
||
|
|
$res->{message} =~ s/;/; /isg;
|
||
|
|
$res->{level} = $test_res->{level};
|
||
|
|
$res->{testcase} = $test_res->{testcase} // 'UNSPECIFIED';
|
||
|
|
$testcases{$res->{testcase}} = $translator->test_case_description($res->{testcase});
|
||
|
|
|
||
|
|
if ( $test_res->{module} eq 'SYSTEM' ) {
|
||
|
|
if ( $res->{message} =~ /policy\.json/ ) {
|
||
|
|
my ( $policy ) = ( $res->{message} =~ /\s(\/.*)$/ );
|
||
|
|
if ( $policy ) {
|
||
|
|
my $policy_description = 'DEFAULT POLICY';
|
||
|
|
$policy_description = 'SOME OTHER POLICY' if ( $policy =~ /some\/other\/policy\/path/ );
|
||
|
|
$res->{message} =~ s/$policy/$policy_description/;
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
$res->{message} = 'UNKNOWN POLICY FORMAT';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
elsif ( $res->{message} =~ /config\.json/ ) {
|
||
|
|
my ( $config ) = ( $res->{message} =~ /\s(\/.*)$/ );
|
||
|
|
if ( $config ) {
|
||
|
|
my $config_description = 'DEFAULT CONFIGURATION';
|
||
|
|
$config_description = 'SOME OTHER CONFIGURATION' if ( $config =~ /some\/other\/configuration\/path/ );
|
||
|
|
$res->{message} =~ s/$config/$config_description/;
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
$res->{message} = 'UNKNOWN CONFIG FORMAT';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
push( @zm_results, $res );
|
||
|
|
}
|
||
|
|
|
||
|
|
$result = $test_info;
|
||
|
|
$result->{testcase_descriptions} = \%testcases;
|
||
|
|
$result->{results} = \@zm_results;
|
||
|
|
|
||
|
|
$translator->locale( $previous_locale );
|
||
|
|
|
||
|
|
$result = $test_info;
|
||
|
|
$result->{results} = \@zm_results;
|
||
|
|
};
|
||
|
|
if ($@) {
|
||
|
|
handle_exception( $@ );
|
||
|
|
}
|
||
|
|
|
||
|
|
return $result;
|
||
|
|
}
|
||
|
|
|
||
|
|
# Experimental
|
||
|
|
$json_schemas{job_results} = {
|
||
|
|
type => 'object',
|
||
|
|
additionalProperties => 0,
|
||
|
|
required => [ 'job_id', 'language' ],
|
||
|
|
properties => {
|
||
|
|
job_id => $zm_validator->test_id->required->compile,
|
||
|
|
language => $zm_validator->language_tag,
|
||
|
|
}
|
||
|
|
};
|
||
|
|
sub job_results {
|
||
|
|
my ( $self, $params ) = @_;
|
||
|
|
|
||
|
|
$params->{id} = delete $params->{job_id};
|
||
|
|
|
||
|
|
my $result = $self->get_test_results( $params );
|
||
|
|
|
||
|
|
return {
|
||
|
|
created_at => $result->{created_at},
|
||
|
|
job_id => $result->{hash_id},
|
||
|
|
results => $result->{results},
|
||
|
|
params => $result->{params},
|
||
|
|
testcase_descriptions => $result->{testcase_descriptionsd},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
$json_schemas{get_test_history} = {
|
||
|
|
type => 'object',
|
||
|
|
additionalProperties => 0,
|
||
|
|
required => [ 'frontend_params' ],
|
||
|
|
properties => {
|
||
|
|
offset => joi->integer->min(0)->compile,
|
||
|
|
limit => joi->integer->min(0)->compile,
|
||
|
|
filter => joi->string->regex('^(?:all|delegated|undelegated)$')->compile,
|
||
|
|
frontend_params => {
|
||
|
|
type => 'object',
|
||
|
|
additionalProperties => 0,
|
||
|
|
required => [ 'domain' ],
|
||
|
|
properties => {
|
||
|
|
domain => $zm_validator->domain_name
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
sub get_test_history {
|
||
|
|
my ( $self, $params ) = @_;
|
||
|
|
|
||
|
|
my $results;
|
||
|
|
|
||
|
|
eval {
|
||
|
|
$params->{offset} //= 0;
|
||
|
|
$params->{limit} //= 200;
|
||
|
|
$params->{filter} //= "all";
|
||
|
|
|
||
|
|
$results = $self->{db}->get_test_history( $params );
|
||
|
|
my @results = map { { %$_, undelegated => $_->{undelegated} ? JSON::PP::true : JSON::PP::false } } @$results;
|
||
|
|
$results = \@results;
|
||
|
|
|
||
|
|
};
|
||
|
|
if ($@) {
|
||
|
|
handle_exception( $@ );
|
||
|
|
}
|
||
|
|
|
||
|
|
return $results;
|
||
|
|
}
|
||
|
|
|
||
|
|
# Experimental
|
||
|
|
$json_schemas{domain_history} = {
|
||
|
|
type => 'object',
|
||
|
|
additionalProperties => 0,
|
||
|
|
required => [ 'params' ],
|
||
|
|
properties => {
|
||
|
|
offset => joi->integer->min(0)->compile,
|
||
|
|
limit => joi->integer->min(0)->compile,
|
||
|
|
filter => joi->string->regex('^(?:all|delegated|undelegated)$')->compile,
|
||
|
|
params => {
|
||
|
|
type => 'object',
|
||
|
|
additionalProperties => 0,
|
||
|
|
required => [ 'domain' ],
|
||
|
|
properties => {
|
||
|
|
domain => $zm_validator->domain_name
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
sub domain_history {
|
||
|
|
my ( $self, $params ) = @_;
|
||
|
|
|
||
|
|
$params->{frontend_params} = delete $params->{params};
|
||
|
|
|
||
|
|
my $results = $self->get_test_history( $params );
|
||
|
|
|
||
|
|
return {
|
||
|
|
history => [
|
||
|
|
map {
|
||
|
|
{
|
||
|
|
job_id => $_->{id},
|
||
|
|
created_at => $_->{created_at},
|
||
|
|
overall_result => $_->{overall_result},
|
||
|
|
undelegated => $_->{undelegated},
|
||
|
|
}
|
||
|
|
} @$results
|
||
|
|
],
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
$json_schemas{add_api_user} = joi->object->strict->props(
|
||
|
|
username => $zm_validator->username->required,
|
||
|
|
api_key => $zm_validator->api_key->required,
|
||
|
|
);
|
||
|
|
sub add_api_user {
|
||
|
|
my ( $self, $params, undef, $remote_ip ) = @_;
|
||
|
|
|
||
|
|
my $result = 0;
|
||
|
|
|
||
|
|
eval {
|
||
|
|
my $allow = 0;
|
||
|
|
if ( defined $remote_ip ) {
|
||
|
|
$allow = 1 if ( $remote_ip eq '::1' || $remote_ip eq '127.0.0.1' || $remote_ip eq '::ffff:127.0.0.1' );
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
$allow = 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( $allow ) {
|
||
|
|
$result = 1 if ( $self->{db}->add_api_user( $params->{username}, $params->{api_key} ) eq '1' );
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
die Zonemaster::Backend::Error::PermissionDenied->new(
|
||
|
|
message => 'Call to "add_api_user" method not permitted from a remote IP',
|
||
|
|
data => { remote_ip => $remote_ip }
|
||
|
|
);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
if ($@) {
|
||
|
|
handle_exception( $@ );
|
||
|
|
}
|
||
|
|
|
||
|
|
return $result;
|
||
|
|
}
|
||
|
|
|
||
|
|
# Experimental
|
||
|
|
$json_schemas{user_create} = $json_schemas{add_api_user};
|
||
|
|
sub user_create {
|
||
|
|
my $result = {
|
||
|
|
success => add_api_user( @_ )
|
||
|
|
};
|
||
|
|
return $result;
|
||
|
|
}
|
||
|
|
|
||
|
|
$json_schemas{add_batch_job} = {
|
||
|
|
type => 'object',
|
||
|
|
additionalProperties => 0,
|
||
|
|
required => [ 'username', 'api_key', 'domains' ],
|
||
|
|
properties => {
|
||
|
|
username => $zm_validator->username->required->compile,
|
||
|
|
api_key => $zm_validator->api_key->required->compile,
|
||
|
|
domains => {
|
||
|
|
type => "array",
|
||
|
|
additionalItems => 0,
|
||
|
|
items => $zm_validator->domain_name,
|
||
|
|
minItems => 1
|
||
|
|
},
|
||
|
|
test_params => {
|
||
|
|
type => 'object',
|
||
|
|
additionalProperties => 0,
|
||
|
|
properties => {
|
||
|
|
ipv4 => joi->boolean->compile,
|
||
|
|
ipv6 => joi->boolean->compile,
|
||
|
|
nameservers => {
|
||
|
|
type => 'array',
|
||
|
|
items => $zm_validator->nameserver
|
||
|
|
},
|
||
|
|
ds_info => {
|
||
|
|
type => 'array',
|
||
|
|
items => $zm_validator->ds_info
|
||
|
|
},
|
||
|
|
profile => $zm_validator->profile_name,
|
||
|
|
client_id => $zm_validator->client_id->compile,
|
||
|
|
client_version => $zm_validator->client_version->compile,
|
||
|
|
config => joi->string->compile,
|
||
|
|
priority => $zm_validator->priority->compile,
|
||
|
|
queue => $zm_validator->queue->compile,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
sub add_batch_job {
|
||
|
|
my ( $self, $params ) = @_;
|
||
|
|
|
||
|
|
my $results;
|
||
|
|
eval {
|
||
|
|
$params->{test_params}{profile} //= "default";
|
||
|
|
$params->{test_params}{priority} //= 5;
|
||
|
|
$params->{test_params}{queue} //= 0;
|
||
|
|
|
||
|
|
my $profile = $self->{_profiles}{ $params->{test_params}{profile} };
|
||
|
|
$params->{test_params}{ipv4} //= $profile->get( "net.ipv4" );
|
||
|
|
$params->{test_params}{ipv6} //= $profile->get( "net.ipv6" );
|
||
|
|
|
||
|
|
$results = $self->{db}->add_batch_job( $params );
|
||
|
|
};
|
||
|
|
if ($@) {
|
||
|
|
handle_exception( $@ );
|
||
|
|
}
|
||
|
|
|
||
|
|
return $results;
|
||
|
|
}
|
||
|
|
|
||
|
|
# Experimental
|
||
|
|
$json_schemas{batch_create} = {
|
||
|
|
type => 'object',
|
||
|
|
additionalProperties => 0,
|
||
|
|
required => [ 'username', 'api_key', 'domains' ],
|
||
|
|
properties => {
|
||
|
|
username => $zm_validator->username->required->compile,
|
||
|
|
api_key => $zm_validator->api_key->required->compile,
|
||
|
|
domains => {
|
||
|
|
type => "array",
|
||
|
|
additionalItems => 0,
|
||
|
|
items => $zm_validator->domain_name,
|
||
|
|
minItems => 1
|
||
|
|
},
|
||
|
|
job_params => {
|
||
|
|
type => 'object',
|
||
|
|
additionalProperties => 0,
|
||
|
|
properties => {
|
||
|
|
ipv4 => joi->boolean->compile,
|
||
|
|
ipv6 => joi->boolean->compile,
|
||
|
|
nameservers => {
|
||
|
|
type => 'array',
|
||
|
|
items => $zm_validator->nameserver
|
||
|
|
},
|
||
|
|
ds_info => {
|
||
|
|
type => 'array',
|
||
|
|
items => $zm_validator->ds_info
|
||
|
|
},
|
||
|
|
profile => $zm_validator->profile_name,
|
||
|
|
client_id => $zm_validator->client_id->compile,
|
||
|
|
client_version => $zm_validator->client_version->compile,
|
||
|
|
config => joi->string->compile,
|
||
|
|
priority => $zm_validator->priority->compile,
|
||
|
|
queue => $zm_validator->queue->compile,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
sub batch_create {
|
||
|
|
my ( $self, $params ) = @_;
|
||
|
|
|
||
|
|
$params->{test_params} = delete $params->{job_params};
|
||
|
|
|
||
|
|
my $result = {
|
||
|
|
batch_id => $self->add_batch_job( $params )
|
||
|
|
};
|
||
|
|
|
||
|
|
return $result;
|
||
|
|
}
|
||
|
|
|
||
|
|
$json_schemas{batch_status} = {
|
||
|
|
type => 'object',
|
||
|
|
additionalProperties => 0,
|
||
|
|
required => [ 'batch_id' ],
|
||
|
|
properties => {
|
||
|
|
batch_id => $zm_validator->batch_id->required,
|
||
|
|
list_waiting_tests => joi->boolean->compile,
|
||
|
|
list_running_tests => joi->boolean->compile,
|
||
|
|
list_finished_tests => joi->boolean->compile,
|
||
|
|
}
|
||
|
|
};
|
||
|
|
sub batch_status {
|
||
|
|
my ( $self, $params ) = @_;
|
||
|
|
|
||
|
|
my $result;
|
||
|
|
eval {
|
||
|
|
$result = $self->{db}->batch_status($params);
|
||
|
|
};
|
||
|
|
if ($@) {
|
||
|
|
handle_exception( $@ );
|
||
|
|
}
|
||
|
|
return $result;
|
||
|
|
}
|
||
|
|
|
||
|
|
sub _get_locale {
|
||
|
|
my ( $self, $params ) = @_;
|
||
|
|
my @error;
|
||
|
|
|
||
|
|
if ( ref $params ne 'HASH' ) {
|
||
|
|
return undef;
|
||
|
|
}
|
||
|
|
|
||
|
|
my $language = $params->{language};
|
||
|
|
if ( !defined $language ) {
|
||
|
|
return undef;
|
||
|
|
}
|
||
|
|
|
||
|
|
my %locales = $self->{config}->LANGUAGE_locale;
|
||
|
|
|
||
|
|
my $locale = $locales{$language};
|
||
|
|
if ( !defined $locale ) {
|
||
|
|
return undef;
|
||
|
|
}
|
||
|
|
|
||
|
|
return $locale . '.UTF-8';
|
||
|
|
}
|
||
|
|
|
||
|
|
sub _set_error_message_locale {
|
||
|
|
my ( $self, $params ) = @_;
|
||
|
|
|
||
|
|
my @error_response = ();
|
||
|
|
my $locale = $self->_get_locale( $params );
|
||
|
|
|
||
|
|
if (not defined $locale or $locale eq "") {
|
||
|
|
# Don't translate message if locale is not defined
|
||
|
|
$locale = "C";
|
||
|
|
}
|
||
|
|
|
||
|
|
# Use POSIX implementation instead of Locale::Messages wrapper
|
||
|
|
setlocale( LC_ALL, $locale );
|
||
|
|
return @error_response;
|
||
|
|
}
|
||
|
|
|
||
|
|
my $rpc_request = joi->object->props(
|
||
|
|
jsonrpc => joi->string->required,
|
||
|
|
method => $zm_validator->jsonrpc_method()->required,
|
||
|
|
id => joi->type([qw(null number string)]));
|
||
|
|
sub jsonrpc_validate {
|
||
|
|
my ( $self, $jsonrpc_request ) = @_;
|
||
|
|
|
||
|
|
my @error_rpc = $rpc_request->validate($jsonrpc_request);
|
||
|
|
if ((ref($jsonrpc_request) eq 'HASH' && !exists $jsonrpc_request->{id}) || @error_rpc) {
|
||
|
|
$self->_set_error_message_locale;
|
||
|
|
return {
|
||
|
|
jsonrpc => '2.0',
|
||
|
|
id => undef,
|
||
|
|
error => {
|
||
|
|
code => '-32600',
|
||
|
|
message => 'The JSON sent is not a valid request object.',
|
||
|
|
data => "@error_rpc"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
my $method_schema = $json_schemas{$jsonrpc_request->{method}};
|
||
|
|
if (blessed $method_schema) {
|
||
|
|
$method_schema = $method_schema->compile;
|
||
|
|
}
|
||
|
|
|
||
|
|
# The "params" key of the JSONRPC object is optional per the JSONRPC 2.0
|
||
|
|
# specification, but if the method being called requires at least one
|
||
|
|
# parameter, omitting it is an error.
|
||
|
|
|
||
|
|
if ( exists $method_schema->{required} and not exists $jsonrpc_request->{params} ) {
|
||
|
|
return {
|
||
|
|
jsonrpc => '2.0',
|
||
|
|
id => $jsonrpc_request->{id},
|
||
|
|
error => {
|
||
|
|
code => '-32602',
|
||
|
|
message => "Missing 'params' object",
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
elsif ( exists $jsonrpc_request->{params} ) {
|
||
|
|
my @error_response = $self->validate_params($method_schema, $jsonrpc_request->{params});
|
||
|
|
|
||
|
|
if ( scalar @error_response ) {
|
||
|
|
return {
|
||
|
|
jsonrpc => '2.0',
|
||
|
|
id => $jsonrpc_request->{id},
|
||
|
|
error => {
|
||
|
|
code => '-32602',
|
||
|
|
message => decode_utf8(__ 'Invalid method parameter(s).'),
|
||
|
|
data => \@error_response
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return '';
|
||
|
|
}
|
||
|
|
|
||
|
|
sub validate_params {
|
||
|
|
my ( $self, $method_schema, $params ) = @_;
|
||
|
|
my @error_response = ();
|
||
|
|
|
||
|
|
push @error_response, $self->_set_error_message_locale( $params );
|
||
|
|
|
||
|
|
if (blessed $method_schema) {
|
||
|
|
$method_schema = $method_schema->compile;
|
||
|
|
}
|
||
|
|
my $jv = JSON::Validator::Schema::Draft7->new->coerce('booleans,numbers,strings')->data($method_schema);
|
||
|
|
$jv->formats(Zonemaster::Backend::Validator::formats( $self->{config} ));
|
||
|
|
my @json_validation_error = $jv->validate( $params );
|
||
|
|
|
||
|
|
# Customize error message from json validation
|
||
|
|
foreach my $err ( @json_validation_error ) {
|
||
|
|
my $message = $err->message;
|
||
|
|
my @details = @{$err->details};
|
||
|
|
|
||
|
|
# Handle 'required' errors globally so it does not get overwritten
|
||
|
|
if ($details[1] eq 'required') {
|
||
|
|
$message = N__ 'Missing property';
|
||
|
|
} else {
|
||
|
|
my @path = split '/', $err->path, -1;
|
||
|
|
shift @path; # first item is an empty string
|
||
|
|
my $found = 1;
|
||
|
|
my $data = Mojo::JSON::Pointer->new($method_schema);
|
||
|
|
|
||
|
|
foreach my $p (@path) {
|
||
|
|
if ( $data->contains("/properties/$p") ) {
|
||
|
|
$data = $data->get("/properties/$p")
|
||
|
|
} elsif ( $p =~ /^\d+$/ and $data->contains("/items") ) {
|
||
|
|
$data = $data->get("/items")
|
||
|
|
} else {
|
||
|
|
$found = 0;
|
||
|
|
last;
|
||
|
|
}
|
||
|
|
$data = Mojo::JSON::Pointer->new($data);
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($found and exists $data->data->{'x-error-message'}) {
|
||
|
|
$message = $data->data->{'x-error-message'};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
push @error_response, { path => $err->path, message => $message };
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
# Translate messages
|
||
|
|
@error_response = map { { %$_, ( message => decode_utf8 __ $_->{message} ) } } @error_response;
|
||
|
|
|
||
|
|
return @error_response;
|
||
|
|
}
|
||
|
|
|
||
|
|
1;
|