feat: add full Zonemaster stack with Docker and Spanish UI
- Clone all 5 Zonemaster component repos (LDNS, Engine, CLI, Backend, GUI) - Dockerfile.backend: 8-stage multi-stage build LDNS→Engine→CLI→Backend - Dockerfile.gui: Astro static build served via nginx - docker-compose.yml: backend (internal) + frontend (port 5353) - nginx.conf: root redirects to /es/, /api/ proxied to backend - zonemaster-gui/config.ts: defaultLanguage set to 'es' (Spanish) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
916
zonemaster-backend/lib/Zonemaster/Backend/RPCAPI.pm
Normal file
916
zonemaster-backend/lib/Zonemaster/Backend/RPCAPI.pm
Normal file
@@ -0,0 +1,916 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user