├── share ├── cleanup-postgres.sql ├── cleanup-mysql.sql ├── tmpfiles.conf ├── patch │ ├── README.txt │ ├── patch_mysql_db_zonemaster_backend_ver_5.0.0.pl │ ├── patch_postgresql_db_zonemaster_backend_ver_5.0.0.pl │ ├── patch_mysql_db_zonemaster_backend_ver_5.0.2.pl │ ├── patch_postgresql_db_zonemaster_backend_ver_1.0.3.pl │ ├── patch_db_zonemaster_backend_ver_11.2.0.pl │ ├── patch_mysql_db_zonemaster_backend_ver_1.0.3.pl │ ├── patch_mysql_db_zonemaster_backend_ver_8.0.0.pl │ ├── patch_sqlite_db_zonemaster_backend_ver_8.0.0.pl │ ├── patch_postgresql_db_zonemaster_backend_ver_8.0.0.pl │ └── patch_db_zonemaster_backend_ver_9.0.0.pl ├── update-po ├── freebsd-pwd.conf ├── Makefile ├── backend_config.ci_sqlite.ini ├── create_db.pl ├── backend_config.ci_mysql.ini ├── backend_config.ci_postgresql.ini ├── zm-rpcapi.service ├── zm-testagent.service ├── zm_rpcapi-bsd ├── zm_testagent-bsd ├── backend_config.ini ├── zm-testagent.lsb ├── zm-rpcapi.lsb ├── nb.po ├── GNUmakefile ├── fr.po ├── da.po ├── sl.po ├── sv.po ├── fi.po └── es.po ├── .github ├── pull_request_template.md └── workflows │ └── ci.yml ├── t ├── 00-load.t ├── manifest.t ├── po-files.t ├── test_profile.json ├── test_profile_no_network.json ├── test_profile_network_true.json ├── queue.t ├── translator.t ├── idn.t ├── TestUtil.pm ├── db_ddl.t ├── parameters_validation.t ├── rpc_validation.t ├── idn.data └── db.t ├── .gitignore ├── lib └── Zonemaster │ ├── Backend.pm │ └── Backend │ ├── Metrics.pm │ ├── Translator.pm │ ├── Errors.pm │ ├── Log.pm │ ├── Config │ └── DCPlugin.pm │ ├── TestAgent.pm │ └── DB │ └── SQLite.pm ├── zonemaster_launch ├── docs ├── TypographicConventions.md ├── files-description.md ├── internal-documentation │ └── maintenance │ │ └── Garbage-Collection-Testing.md └── Architecture.md ├── script ├── add-batch-job.pl ├── zmtest ├── zonemaster_backend_rpcapi.psgi └── zonemaster_backend_testagent ├── CONTRIBUTING.md ├── MANIFEST.SKIP ├── LICENSE ├── README.md ├── MANIFEST ├── Makefile.PL └── Dockerfile /share/cleanup-postgres.sql: -------------------------------------------------------------------------------- 1 | -- Remove Zonemaster data from database 2 | DROP DATABASE zonemaster; 3 | DROP USER zonemaster; 4 | -------------------------------------------------------------------------------- /share/cleanup-mysql.sql: -------------------------------------------------------------------------------- 1 | -- Remove Zonemaster data from database 2 | DROP DATABASE zonemaster; 3 | DROP USER 'zonemaster'@'localhost'; 4 | -------------------------------------------------------------------------------- /share/tmpfiles.conf: -------------------------------------------------------------------------------- 1 | #Type Path Mode UID GID Age Argument 2 | d /run/zonemaster 0755 zonemaster zonemaster - - 3 | -------------------------------------------------------------------------------- /share/patch/README.txt: -------------------------------------------------------------------------------- 1 | Find instructions on patching (upgrading) the Zonemaster database 2 | on https://github.com/zonemaster/zonemaster/blob/master/docs/public/upgrading/backend.md 3 | -------------------------------------------------------------------------------- /share/update-po: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -z "$1" ] ; then 4 | echo "error: No PO file specified." >&2 5 | exit 2 6 | fi 7 | po_file="$1" ; shift 8 | 9 | make update-po POFILES="$po_file" 10 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | This PR... 4 | 5 | ## Context 6 | 7 | (e.g. Fixes #9999, Follow-up to #9999, etc.) 8 | 9 | ## Changes 10 | 11 | ... 12 | 13 | ## How to test this PR 14 | 15 | ... 16 | -------------------------------------------------------------------------------- /share/freebsd-pwd.conf: -------------------------------------------------------------------------------- 1 | # Range of free system UIDs that can be used for Zonemaster user 2 | minuid = 736 3 | maxuid = 769 4 | 5 | # Range of free system GIDs that can be used for Zonemaster group 6 | mingid = 736 7 | maxgid = 769 8 | -------------------------------------------------------------------------------- /t/00-load.t: -------------------------------------------------------------------------------- 1 | use 5.014002; 2 | use strict; 3 | use warnings FATAL => 'all'; 4 | use Test::More; 5 | 6 | plan tests => 1; 7 | 8 | BEGIN { 9 | use_ok( 'Zonemaster::Backend::Config' ) || print "Bail out!\n"; 10 | } 11 | 12 | done_testing; 13 | -------------------------------------------------------------------------------- /share/Makefile: -------------------------------------------------------------------------------- 1 | # This is a wrapper for BSD Make (FreeBSD) to execute 2 | # GNU Make (gmake) and the primary makefile GNUmakefile. 3 | 4 | GNUMAKE ?= gmake 5 | FILES != ls * 6 | 7 | # File targets should be evaluated by gmake. 8 | .PHONY: all $(FILES) 9 | 10 | all: 11 | @${GNUMAKE} $@ 12 | 13 | .DEFAULT: 14 | @${GNUMAKE} $@ 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /Makefile 2 | Makefile.old 3 | Build 4 | Build.bat 5 | META.* 6 | MYMETA.* 7 | .build/ 8 | _build/ 9 | cover_db/ 10 | blib/ 11 | .lwpcookies 12 | .last_cover_stats 13 | nytprof.out 14 | pod2htm*.tmp 15 | pm_to_blib 16 | Zonemaster-* 17 | Zonemaster-*.tar.gz 18 | .orig 19 | inc 20 | *.mo 21 | 22 | # Ignore Emacs and other backup files 23 | *.bak 24 | *~ 25 | .*.swp 26 | -------------------------------------------------------------------------------- /share/backend_config.ci_sqlite.ini: -------------------------------------------------------------------------------- 1 | [DB] 2 | engine=SQLite 3 | polling_interval=0.5 4 | #seconds 5 | 6 | [SQLITE] 7 | database_file=/tmp/zonemaster.sqlite 8 | 9 | [ZONEMASTER] 10 | max_zonemaster_execution_time=300 11 | number_of_processes_for_frontend_testing=20 12 | number_of_processes_for_batch_testing=20 13 | #seconds 14 | 15 | [LANGUAGE] 16 | locale = da_DK en_US es_ES fi_FI fr_FR nb_NO sv_SE 17 | 18 | -------------------------------------------------------------------------------- /share/create_db.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Zonemaster::Backend::Config; 7 | use Zonemaster::Backend::DB; 8 | 9 | my $config = Zonemaster::Backend::Config->load_config(); 10 | my $db_engine = $config->DB_engine; 11 | 12 | my $db_class = Zonemaster::Backend::DB->get_db_class( $db_engine ); 13 | 14 | my $db = $db_class->from_config( $config ); 15 | $db->create_schema(); 16 | -------------------------------------------------------------------------------- /share/backend_config.ci_mysql.ini: -------------------------------------------------------------------------------- 1 | [DB] 2 | engine=MySQL 3 | polling_interval=0.5 4 | #seconds 5 | 6 | [MYSQL] 7 | host=localhost 8 | database=zonemaster 9 | user=ci 10 | password=password 11 | 12 | [ZONEMASTER] 13 | max_zonemaster_execution_time=300 14 | number_of_processes_for_frontend_testing=20 15 | number_of_processes_for_batch_testing=20 16 | #seconds 17 | 18 | [LANGUAGE] 19 | locale = da_DK en_US es_ES fi_FI fr_FR nb_NO sv_SE 20 | 21 | -------------------------------------------------------------------------------- /share/backend_config.ci_postgresql.ini: -------------------------------------------------------------------------------- 1 | [DB] 2 | engine=PostgreSQL 3 | polling_interval=0.5 4 | #seconds 5 | 6 | [POSTGRESQL] 7 | host=localhost 8 | database=zonemaster 9 | user=ci 10 | password=password 11 | 12 | [ZONEMASTER] 13 | max_zonemaster_execution_time=300 14 | number_of_processes_for_frontend_testing=20 15 | number_of_processes_for_batch_testing=20 16 | #seconds 17 | 18 | [LANGUAGE] 19 | locale = da_DK en_US es_ES fi_FI fr_FR nb_NO sv_SE 20 | 21 | -------------------------------------------------------------------------------- /lib/Zonemaster/Backend.pm: -------------------------------------------------------------------------------- 1 | package Zonemaster::Backend; 2 | 3 | our $VERSION = '12.0.0'; 4 | 5 | use strict; 6 | use warnings; 7 | use 5.14.2; 8 | 9 | =head1 NAME 10 | 11 | Zonemaster::Backend - A system for running Zonemaster tests asynchronously through an RPC-API 12 | 13 | =head1 AUTHOR 14 | 15 | Michal Toma 16 | 17 | =head1 LICENSE 18 | 19 | This is free software under a 2-clause BSD license. The full text of the license can 20 | be found in the F file included with this distribution. 21 | 22 | =cut 23 | 24 | 1; 25 | -------------------------------------------------------------------------------- /share/zm-rpcapi.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=RPC server for Zonemaster Backend 3 | After=network.target mariadb.service postgresql.service 4 | Wants=mariadb.service postgresql.service 5 | 6 | [Service] 7 | Type=simple 8 | ExecStart=/usr/local/bin/starman --listen=127.0.0.1:5000 --preload-app --user=zonemaster --group=zonemaster --pid=/run/zonemaster/zm-rpcapi.pid --error-log=/var/log/zonemaster/zm-rpcapi.log --daemonize /usr/local/bin/zonemaster_backend_rpcapi.psgi 9 | KillSignal=SIGQUIT 10 | PIDFile=/run/zonemaster/zm-rpcapi.pid 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /share/zm-testagent.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=test agent for Zonemaster Backend 3 | After=network.target mariadb.service postgresql.service 4 | Wants=mariadb.service postgresql.service 5 | 6 | [Service] 7 | Type=simple 8 | ExecStart=/usr/local/bin/zonemaster_backend_testagent --logfile=/var/log/zonemaster/zm-testagent.log --outfile=/var/log/zonemaster/zm-testagent.out --pidfile=/run/zonemaster/zm-testagent.pid --user=zonemaster --group=zonemaster start 9 | ExecStop=/usr/local/bin/zonemaster_backend_testagent --logfile=/var/log/zonemaster/zm-testagent.log --outfile=/var/log/zonemaster/zm-testagent.out --pidfile=/run/zonemaster/zm-testagent.pid --user=zonemaster --group=zonemaster stop 10 | PIDFile=/run/zonemaster/zm-testagent.pid 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /share/patch/patch_mysql_db_zonemaster_backend_ver_5.0.0.pl: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use DBI qw(:utils); 5 | 6 | use Zonemaster::Backend::Config; 7 | use Zonemaster::Backend::DB::MySQL; 8 | 9 | my $config = Zonemaster::Backend::Config->load_config(); 10 | if ( $config->DB_engine ne 'MySQL' ) { 11 | die "The configuration file does not contain the MySQL backend"; 12 | } 13 | my $dbh = Zonemaster::Backend::DB::MySQL->from_config( $config )->dbh; 14 | 15 | sub patch_db { 16 | #################################################################### 17 | # TEST RESULTS 18 | #################################################################### 19 | $dbh->do( 'ALTER TABLE test_results ADD COLUMN nb_retries INTEGER NOT NULL DEFAULT 0' ); 20 | } 21 | 22 | patch_db(); 23 | -------------------------------------------------------------------------------- /share/patch/patch_postgresql_db_zonemaster_backend_ver_5.0.0.pl: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use DBI qw(:utils); 5 | 6 | use Zonemaster::Backend::Config; 7 | use Zonemaster::Backend::DB::PostgreSQL; 8 | 9 | my $config = Zonemaster::Backend::Config->load_config(); 10 | if ( $config->DB_engine ne 'PostgreSQL' ) { 11 | die "The configuration file does not contain the PostgreSQL backend"; 12 | } 13 | my $dbh = Zonemaster::Backend::DB::PostgreSQL->from_config( $config )->dbh; 14 | 15 | sub patch_db { 16 | 17 | #################################################################### 18 | # TEST RESULTS 19 | #################################################################### 20 | $dbh->do( 'ALTER TABLE test_results ADD COLUMN nb_retries INTEGER NOT NULL DEFAULT 0' ); 21 | } 22 | 23 | patch_db(); 24 | -------------------------------------------------------------------------------- /share/patch/patch_mysql_db_zonemaster_backend_ver_5.0.2.pl: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use DBI qw(:utils); 5 | 6 | use Zonemaster::Backend::Config; 7 | use Zonemaster::Backend::DB::MySQL; 8 | 9 | my $config = Zonemaster::Backend::Config->load_config(); 10 | if ( $config->DB_engine ne 'MySQL' ) { 11 | die "The configuration file does not contain the MySQL backend"; 12 | } 13 | my $dbh = Zonemaster::Backend::DB::MySQL->from_config( $config )->dbh; 14 | 15 | sub patch_db { 16 | ############################################################################ 17 | # Convert column "results" to MEDIUMBLOB so that it can hold larger results 18 | ############################################################################ 19 | $dbh->do( 'ALTER TABLE test_results MODIFY results mediumblob' ); 20 | } 21 | 22 | patch_db(); 23 | -------------------------------------------------------------------------------- /share/patch/patch_postgresql_db_zonemaster_backend_ver_1.0.3.pl: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use DBI qw(:utils); 5 | 6 | use Zonemaster::Backend::Config; 7 | use Zonemaster::Backend::DB::MySQL; 8 | 9 | my $config = Zonemaster::Backend::Config->load_config(); 10 | if ( $config->DB_engine ne 'MySQL' ) { 11 | die "The configuration file does not contain the MySQL backend"; 12 | } 13 | my $dbh = Zonemaster::Backend::DB::MySQL->from_config( $config )->dbh; 14 | 15 | sub patch_db { 16 | 17 | #################################################################### 18 | # TEST RESULTS 19 | #################################################################### 20 | $dbh->do( 'ALTER TABLE test_results ADD COLUMN hash_id VARCHAR(16) DEFAULT substring(md5(random()::text || clock_timestamp()::text) from 1 for 16) NOT NULL' ); 21 | } 22 | 23 | patch_db(); 24 | -------------------------------------------------------------------------------- /zonemaster_launch: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | 4 | case $1 in 5 | 6 | cli) 7 | shift 1 8 | zonemaster-cli $@ 9 | ;; 10 | 11 | zmb) 12 | shift 1 13 | zmb $@ 14 | ;; 15 | 16 | zmtest) 17 | shift 1 18 | zmtest $@ 19 | ;; 20 | 21 | rpcapi) 22 | /usr/local/bin/starman --listen=0.0.0.0:5000 --preload-app --user=zonemaster --group=zonemaster /usr/local/bin/zonemaster_backend_rpcapi.psgi 23 | 24 | ;; 25 | 26 | testagent) 27 | /usr/local/bin/zonemaster_backend_testagent -user=zonemaster --group=zonemaster foreground 28 | ;; 29 | 30 | full) 31 | exec /init 32 | ;; 33 | *) 34 | echo "'$1' is not a valid option. 35 | Available options: 36 | - cli : pass argument to zonemaster-cli then quit 37 | - full : start both rpcapi & testagent 38 | - rpcapi 39 | - testagent 40 | - zmb 41 | - zmtest 42 | " 43 | ;; 44 | esac; 45 | -------------------------------------------------------------------------------- /docs/TypographicConventions.md: -------------------------------------------------------------------------------- 1 | # Typographic conventions 2 | 3 | The Zonemaster Backend documentation uses the following typographic conventions: 4 | 5 | *Italic* text is used for: 6 | 7 | * technical terms defined in the [Architecture] document 8 | * data types defined in the [API] document 9 | 10 | `Monospace` text is used for: 11 | 12 | * snippets of JSON or sh 13 | * JSON-RPC method names 14 | * JSON values 15 | * single or strings of characters 16 | * internet addresses (e.g. domain names and IP addresses) 17 | * file names with or without paths (e.g. configuration files and command line 18 | tools) 19 | * config section names 20 | 21 | > 22 | > Block quotes are used for: 23 | > 24 | > * notes and commentary 25 | > 26 | 27 | [API]: https://github.com/zonemaster/zonemaster/blob/master/docs/public/using/backend/rpcapi-reference.md 28 | [Architecture]: Architecture.md 29 | -------------------------------------------------------------------------------- /share/patch/patch_db_zonemaster_backend_ver_11.2.0.pl: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use Zonemaster::Backend::Config; 5 | use Zonemaster::Engine; 6 | 7 | my $config = Zonemaster::Backend::Config->load_config(); 8 | 9 | my $db_engine = $config->DB_engine; 10 | print "Configured database engine: $db_engine\n"; 11 | 12 | if ( $db_engine =~ /^(MySQL|PostgreSQL|SQLite)$/ ) { 13 | print( "Starting database migration\n" ); 14 | 15 | _update_result_entries( $config->new_DB()->dbh() ); 16 | 17 | print( "\nMigration done\n" ); 18 | } 19 | else { 20 | die "Unknown database engine configured: $db_engine\n"; 21 | } 22 | 23 | 24 | sub _update_result_entries { 25 | my ( $dbh ) = @_; 26 | 27 | $dbh->do(< 2; 7 | use Test::NoWarnings; 8 | 9 | use File::Basename qw( dirname ); 10 | 11 | chdir dirname( dirname( __FILE__ ) ) or BAIL_OUT( "chdir: $!" ); 12 | 13 | my $makebin = 'make'; 14 | 15 | sub make { 16 | my @make_args = @_; 17 | 18 | undef $ENV{MAKEFLAGS}; 19 | 20 | my $command = join( ' ', $makebin, '-s', @make_args ); 21 | my $output = `$command 2>&1`; 22 | 23 | if ( $? == -1 ) { 24 | BAIL_OUT( "failed to execute: $!" ); 25 | } 26 | elsif ( $? & 127 ) { 27 | BAIL_OUT( "child died with signal %d, %s coredump\n", ( $? & 127 ), ( $? & 128 ) ? 'with' : 'without' ); 28 | } 29 | 30 | return $output, $? >> 8; 31 | } 32 | 33 | subtest "distcheck" => sub { 34 | my ( $output, $status ) = make "distcheck"; 35 | is $status, 0, $makebin . ' distcheck exits with value 0'; 36 | is $output, "", $makebin . ' distcheck gives empty output'; 37 | }; 38 | -------------------------------------------------------------------------------- /share/zm_rpcapi-bsd: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # PROVIDE: zm_rpcapi 4 | # REQUIRE: NETWORKING mysql postgresql 5 | # KEYWORD: shutdown 6 | 7 | . /etc/rc.subr 8 | 9 | name="zm_rpcapi" 10 | rcvar="${name}_enable" 11 | 12 | load_rc_config $name 13 | : ${zm_rpcapi_enable="NO"} 14 | : ${zm_rpcapi_user="zonemaster"} 15 | : ${zm_rpcapi_group="zonemaster"} 16 | : ${zm_rpcapi_pidfile="/var/run/zonemaster/${name}.pid"} 17 | : ${zm_rpcapi_logfile="/var/log/zonemaster/${name}.log"} 18 | : ${zm_rpcapi_listen="127.0.0.1:5000"} 19 | 20 | export ZONEMASTER_BACKEND_CONFIG_FILE="/usr/local/etc/zonemaster/backend_config.ini" 21 | #export ZM_BACKEND_RPCAPI_LOGLEVEL='warning' # Set this variable to override the default log level 22 | 23 | command="/usr/local/bin/starman" 24 | command_args="--listen=${zm_rpcapi_listen} --preload-app --user=${zm_rpcapi_user} --group=${zm_rpcapi_group} --pid=${zm_rpcapi_pidfile} --error-log=${zm_rpcapi_logfile} --daemonize /usr/local/bin/zonemaster_backend_rpcapi.psgi" 25 | pidfile="${zm_rpcapi_pidfile}" 26 | required_files="/usr/local/etc/zonemaster/backend_config.ini /usr/local/bin/zonemaster_backend_rpcapi.psgi" 27 | 28 | run_rc_command "$1" 29 | -------------------------------------------------------------------------------- /script/add-batch-job.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | # This script is for testing purpose only. 4 | 5 | use 5.14.2; 6 | use warnings; 7 | 8 | use Data::Dumper; 9 | use Encode qw[decode_utf8]; 10 | use Zonemaster::Backend::RPCAPI; 11 | use Digest::MD5 qw(md5_hex); 12 | 13 | binmode STDOUT, ':utf8'; 14 | 15 | my $e = Zonemaster::Backend::RPCAPI->new; 16 | 17 | say "Starting add_batch_job"; 18 | my @domains; 19 | for (my $i = 0; $i < 100; $i++) { 20 | push(@domains, substr(md5_hex(rand(10000)), 0, 5).".fr"); 21 | } 22 | 23 | #die Dumper(\@domains); 24 | 25 | $e->add_api_user({ username => 'test_user', api_key => 'API_KEY_01'}); 26 | 27 | $e->add_batch_job( 28 | { 29 | client_id => 'Add Script', 30 | client_version => '1.0', 31 | username => 'test_user', 32 | api_key => 'API_KEY_01', 33 | test_params => { 34 | client_id => 'Add Script', 35 | client_version => '1.0', 36 | ipv4 => 1, # 0 or 1, is the ipv4 checkbox checked 37 | ipv6 => 1, # 0 or 1, is the ipv6 checkbox checked 38 | profile => 'default', # the id if the Test profile listbox (unused) 39 | }, 40 | domains => \@domains, 41 | } 42 | ); 43 | -------------------------------------------------------------------------------- /lib/Zonemaster/Backend/Metrics.pm: -------------------------------------------------------------------------------- 1 | package Zonemaster::Backend::Metrics; 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Log::Any qw($log); 7 | 8 | eval("use Net::Statsd"); 9 | 10 | my $enable_metrics = 0; 11 | 12 | if (!$@) { 13 | $enable_metrics = 1; 14 | } 15 | 16 | my %CODE_STATUS_HASH = ( 17 | -32700 => 'RPC_PARSE_ERROR', 18 | -32600 => 'RPC_INVALID_REQUEST', 19 | -32601 => 'RPC_METHOD_NOT_FOUND', 20 | -32602 => 'RPC_INVALID_PARAMS', 21 | -32603 => 'RPC_INTERNAL_ERROR' 22 | ); 23 | 24 | sub setup { 25 | my ( $cls, $host, $port ) = @_; 26 | if (!defined $host) { 27 | $enable_metrics = 0; 28 | } elsif ( $enable_metrics ) { 29 | $log->info('Enabling metrics module', { host => $host, port => $port }); 30 | $Net::Statsd::HOST = $host; 31 | $Net::Statsd::PORT = $port; 32 | } 33 | } 34 | 35 | sub code_to_status { 36 | my ($cls, $code) = @_; 37 | if (defined $code) { 38 | return $CODE_STATUS_HASH{$code}; 39 | } else { 40 | return 'RPC_SUCCESS'; 41 | } 42 | } 43 | 44 | sub increment { 45 | if ( $enable_metrics ) { 46 | Net::Statsd::increment(@_); 47 | } 48 | } 49 | 50 | sub gauge { 51 | if ( $enable_metrics ) { 52 | Net::Statsd::gauge(@_); 53 | } 54 | } 55 | 56 | sub timing { 57 | if ( $enable_metrics ) { 58 | Net::Statsd::timing(@_); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /share/patch/patch_mysql_db_zonemaster_backend_ver_1.0.3.pl: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use DBI qw(:utils); 5 | 6 | use Zonemaster::Backend::Config; 7 | use Zonemaster::Backend::DB::MySQL; 8 | 9 | my $config = Zonemaster::Backend::Config->load_config(); 10 | if ( $config->DB_engine ne 'MySQL' ) { 11 | die "The configuration file does not contain the MySQL backend"; 12 | } 13 | my $dbh = Zonemaster::Backend::DB::MySQL->from_config( $config )->dbh; 14 | 15 | sub patch_db { 16 | 17 | #################################################################### 18 | # TEST RESULTS 19 | #################################################################### 20 | $dbh->do( 'ALTER TABLE test_results ADD COLUMN hash_id VARCHAR(16) NULL' ); 21 | 22 | $dbh->do( 'UPDATE test_results SET hash_id = (SELECT SUBSTRING(MD5(CONCAT(RAND(), UUID())) from 1 for 16))' ); 23 | 24 | $dbh->do( 'ALTER TABLE test_results MODIFY hash_id VARCHAR(16) DEFAULT NULL NOT NULL' ); 25 | 26 | $dbh->do( 27 | 'CREATE TRIGGER before_insert_test_results 28 | BEFORE INSERT ON test_results 29 | FOR EACH ROW 30 | BEGIN 31 | IF new.hash_id IS NULL OR new.hash_id=\'\' 32 | THEN 33 | SET new.hash_id = SUBSTRING(MD5(CONCAT(RAND(), UUID())) from 1 for 16); 34 | END IF; 35 | END; 36 | ' 37 | ); 38 | } 39 | 40 | patch_db(); 41 | -------------------------------------------------------------------------------- /share/zm_testagent-bsd: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # PROVIDE: zm_testagent 4 | # REQUIRE: NETWORKING mysql postgresql 5 | # KEYWORD: shutdown 6 | 7 | . /etc/rc.subr 8 | 9 | name="zm_testagent" 10 | rcvar="${name}_enable" 11 | 12 | load_rc_config $name 13 | : ${zm_testagent_enable="NO"} 14 | : ${zm_testagent_user="zonemaster"} 15 | : ${zm_testagent_group="zonemaster"} 16 | : ${zm_testagent_pidfile="/var/run/zonemaster/${name}.pid"} 17 | 18 | export ZONEMASTER_BACKEND_CONFIG_FILE="/usr/local/etc/zonemaster/backend_config.ini" 19 | #ZM_BACKEND_TESTAGENT_LOGLEVEL='info' # Set this variable to override the default log level 20 | 21 | # Make Perl available for service() when executed via env() in script 22 | export PATH="$PATH:/usr/local/bin" 23 | 24 | command="/usr/local/bin/zonemaster_backend_testagent" 25 | command_args="--user=${zm_testagent_user} --group=${zm_testagent_group} --pidfile=${zm_testagent_pidfile}" 26 | if [ -n "$ZM_BACKEND_TESTAGENT_LOGLEVEL" ] ; then 27 | command_args="$testagent_args --loglevel=$ZM_BACKEND_TESTAGENT_LOGLEVEL" 28 | fi 29 | pidfile="${zm_testagent_pidfile}" 30 | procname="/usr/local/bin/perl" 31 | required_files="/usr/local/etc/zonemaster/backend_config.ini" 32 | 33 | start_precmd="${name}_prestart" 34 | stop_precmd="${name}_prestop" 35 | 36 | zm_testagent_prestart() 37 | { 38 | rc_flags="${rc_flags} start" 39 | } 40 | 41 | zm_testagent_prestop() 42 | { 43 | rc_flags="${rc_flags} stop" 44 | } 45 | 46 | run_rc_command "$1" 47 | -------------------------------------------------------------------------------- /share/backend_config.ini: -------------------------------------------------------------------------------- 1 | # For documentation of the backend_config.ini file see 2 | # https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md 3 | 4 | [DB] 5 | engine = SQLite 6 | polling_interval = 0.5 7 | 8 | [MYSQL] 9 | host = localhost 10 | user = zonemaster 11 | password = zonemaster 12 | database = zonemaster 13 | 14 | [POSTGRESQL] 15 | host = localhost 16 | user = zonemaster 17 | password = zonemaster 18 | database = zonemaster 19 | 20 | [SQLITE] 21 | database_file = /var/lib/zonemaster/db.sqlite 22 | 23 | [ZONEMASTER] 24 | #max_zonemaster_execution_time = 600 25 | #number_of_processes_for_frontend_testing = 20 26 | #number_of_processes_for_batch_testing = 20 27 | #lock_on_queue = 0 28 | #age_reuse_previous_test = 600 29 | 30 | [RPCAPI] 31 | 32 | # Uncomment to enable API method "add_api_user" 33 | #enable_add_api_user = yes 34 | # Uncomment to disable API method "add_batch_job" 35 | #enable_add_batch_job = no 36 | 37 | [LANGUAGE] 38 | locale = da_DK en_US es_ES fi_FI fr_FR nb_NO sl_SI sv_SE 39 | 40 | [PUBLIC PROFILES] 41 | #example_profile_1=/example/directory/test1_profile.json 42 | #default=/example/directory/default_profile.json 43 | 44 | [PRIVATE PROFILES] 45 | #example_profile_2=/example/directory/test2_profile.json 46 | 47 | [METRICS] 48 | # Uncoment the following option to enable the metrics feature 49 | #statsd_host = localhost 50 | #statsd_port = 8125 51 | -------------------------------------------------------------------------------- /docs/files-description.md: -------------------------------------------------------------------------------- 1 | # Files Description 2 | 3 | ./lib/Zonemaster/Backend/RPCAPI.pm 4 | The main module 5 | 6 | ./script/zonemaster_backend_rpcapi.psgi 7 | The Plack/PSGI module. The main entry module for a Plack/PSGI server (like Starman) 8 | 9 | ./lib/Zonemaster/Backend/Config.pm 10 | The Configuration file abstraction layer 11 | 12 | ./share/backend_config.ini 13 | A sample configuration file 14 | 15 | ./CodeSnippets/Client.pm 16 | ./CodeSnippets/client.pl 17 | A sample script and library to communicate with the backend. 18 | 19 | ./lib/Zonemaster/Backend/DB.pm 20 | The Database abstraction layer. 21 | 22 | ./lib/Zonemaster/Backend/DB/MySQL.pm 23 | The Database abstraction layer MySQL sample backend. 24 | 25 | ./lib/Zonemaster/Backend/DB/SQLite.pm 26 | The Database abstraction layer SQLite sample backend. 27 | 28 | ./lib/Zonemaster/Backend/DB/PostgreSQL.pm 29 | The Database abstraction layer PostgreSQL backend. 30 | 31 | ./lib/Zonemaster/Backend/Translator.pm 32 | The translation module. 33 | 34 | ./lib/Zonemaster/Backend/TestAgent.pm 35 | The TestAgent main module. 36 | 37 | ./script/execute_zonemaster_P10.pl 38 | ./script/execute_zonemaster_P5.pl 39 | The scripts to execute tests with differents priorities (application level priorities). 40 | 41 | ./script/execute_tests.pl 42 | The main Test Agent entry point to execute from crontab. 43 | 44 | ./t/test01.t 45 | ./t/test_mysql_backend.pl 46 | ./t/test_postgresql_backend.pl 47 | ./t/test_validate_syntax.t 48 | Test files. 49 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Zonemaster::Backend 2 | 3 | Contribution to this repository is welcome. Contribution can be either an issue 4 | report or a code or a documentation update. Also see the information in the 5 | [main README][Zonemaster/Zonemaster README] in the main Zonemaster respository. 6 | 7 | ## Issue 8 | 9 | First search for a similar issue in the [issues list]. If a relevant issue is 10 | found, add your information as a comment. If no relevant issue is found, create 11 | [a new issue][create issue]. Give as many details as you have and describe, if 12 | possible, how the issue can be reproduced. 13 | 14 | ## Pull request 15 | 16 | If you would like to contribute an update, first please look for issues and open 17 | [pull requests] that are about the same thing. If nothing relevant is found or 18 | you have a different solution, create [a new pull request][create pull request]. 19 | Creating a pull request assumes that you have your proposal in a fork repository. 20 | 21 | When you create a pull request, please always start with the `develop` branch 22 | and create the pull request against the same branch. 23 | 24 | 25 | [issues list]: https://github.com/zonemaster/zonemaster-backend/issues 26 | [create issue]: https://github.com/zonemaster/zonemaster-backend/issues/new 27 | [pull requests]: https://github.com/zonemaster/zonemaster-backend/pulls 28 | [create pull request]: https://github.com/zonemaster/zonemaster-backend/compare 29 | [Zonemaster/Zonemaster README]: https://github.com/zonemaster/zonemaster#readme 30 | 31 | -------------------------------------------------------------------------------- /lib/Zonemaster/Backend/Translator.pm: -------------------------------------------------------------------------------- 1 | package Zonemaster::Backend::Translator; 2 | 3 | our $VERSION = '1.1.0'; 4 | 5 | use 5.14.2; 6 | 7 | use Moose; 8 | use Encode; 9 | use Readonly; 10 | use POSIX qw[setlocale LC_MESSAGES LC_CTYPE]; 11 | use Locale::TextDomain qw[Zonemaster-Backend]; 12 | use Zonemaster::Backend::Config; 13 | 14 | # Zonemaster Modules 15 | require Zonemaster::Engine::Translator; 16 | require Zonemaster::Engine::Logger::Entry; 17 | 18 | extends 'Zonemaster::Engine::Translator'; 19 | 20 | Readonly my %TAG_DESCRIPTIONS => ( 21 | TEST_DIED => sub { 22 | __x # BACKEND_TEST_AGENT:TEST_DIED 23 | 'An error occured and Zonemaster could not start or finish the test.', @_; 24 | }, 25 | UNABLE_TO_FINISH_TEST => sub { 26 | __x # BACKEND_TEST_AGENT:UNABLE_TO_FINISH_TEST 27 | 'The test took too long to run (the current limit is {max_execution_time} seconds). ' 28 | . 'Maybe there are too many name servers or the name servers are either unreachable or not responsive enough.', @_; 29 | }, 30 | ); 31 | 32 | sub _build_all_tag_descriptions { 33 | my ( $class ) = @_; 34 | 35 | my $all_tag_descriptions = Zonemaster::Engine::Translator::_build_all_tag_descriptions(); 36 | $all_tag_descriptions->{Backend} = \%TAG_DESCRIPTIONS; 37 | return $all_tag_descriptions; 38 | } 39 | 40 | sub translate_tag { 41 | my ( $self, $hashref ) = @_; 42 | 43 | my $entry = Zonemaster::Engine::Logger::Entry->new( { %{ $hashref } } ); 44 | 45 | return decode_utf8( $self->SUPER::translate_tag( $entry ) ); 46 | } 47 | 48 | sub test_case_description { 49 | my ( $self, $test_name ) = @_; 50 | 51 | return decode_utf8( $self->SUPER::test_case_description( $test_name ) ); 52 | } 53 | 54 | 1; 55 | -------------------------------------------------------------------------------- /MANIFEST.SKIP: -------------------------------------------------------------------------------- 1 | ^maint/ 2 | ^tags$ 3 | ^\.last_cover_stats$ 4 | ^t.*sessions 5 | ^.*\.log 6 | ^.*\.swp$ 7 | ^MANIFEST\.SKIP$ 8 | ^Dockerfile$ 9 | ^zonemaster_launch$ 10 | ^\.github/ 11 | ^docs/internal-documentation/ 12 | \.po$ 13 | ^share/[^/]*\.mo$ 14 | ^share/Zonemaster-Backend.pot$ 15 | ^share/backend_config\.ci_mysql\.ini$ 16 | ^share/backend_config\.ci_postgresql\.ini$ 17 | ^share/backend_config\.ci_sqlite\.ini$ 18 | ^share/update-po$ 19 | ^Zonemaster-Backend-[0-9.]*\.tar\.gz 20 | 21 | # PO files are not present in the distribution package, tests of those are irrelevant there. 22 | ^t/po-files.t 23 | 24 | #!start included /usr/share/perl/5.20/ExtUtils/MANIFEST.SKIP 25 | # Avoid version control files. 26 | \bRCS\b 27 | \bCVS\b 28 | \bSCCS\b 29 | ,v$ 30 | \B\.svn\b 31 | \B\.git\b 32 | \B\.gitignore\b 33 | \b_darcs\b 34 | \B\.cvsignore$ 35 | 36 | # Avoid VMS specific MakeMaker generated files 37 | \bDescrip.MMS$ 38 | \bDESCRIP.MMS$ 39 | \bdescrip.mms$ 40 | 41 | # Avoid Makemaker generated and utility files. 42 | \bMANIFEST\.bak 43 | ^Makefile$ 44 | \bblib/ 45 | \bMakeMaker-\d 46 | \bpm_to_blib\.ts$ 47 | \bpm_to_blib$ 48 | \bblibdirs\.ts$ # 6.18 through 6.25 generated this 49 | 50 | # Avoid Module::Build generated and utility files. 51 | \bBuild$ 52 | \b_build/ 53 | \bBuild.bat$ 54 | \bBuild.COM$ 55 | \bBUILD.COM$ 56 | \bbuild.com$ 57 | 58 | # Avoid temp and backup files. 59 | ~$ 60 | \.old$ 61 | \#$ 62 | \b\.# 63 | \.bak$ 64 | \.tmp$ 65 | \.# 66 | \.rej$ 67 | 68 | # Avoid OS-specific files/dirs 69 | # Mac OSX metadata 70 | \B\.DS_Store 71 | # Mac OSX SMB mount metadata files 72 | \B\._ 73 | 74 | # Avoid Devel::Cover and Devel::CoverX::Covered files. 75 | \bcover_db\b 76 | \bcovered\b 77 | 78 | # Avoid MYMETA files 79 | ^MYMETA\. 80 | 81 | # Avoid MANIFEST test 82 | t/manifest.t 83 | 84 | #!end included /usr/share/perl/5.20/ExtUtils/MANIFEST.SKIP 85 | -------------------------------------------------------------------------------- /share/zm-testagent.lsb: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | ### BEGIN INIT INFO 4 | # Provides: zm-testagent 5 | # Required-Start: $network $local_fs 6 | # Required-Stop: $network $local_fs 7 | # Should-Start: mysql postgresql 8 | # Should-Stop: mysql postgresql 9 | # Default-Start: 2 3 4 5 10 | # Default-Stop: 0 1 6 11 | # Short-Description: An asynchronous execution backend for Zonemaster Backend 12 | # Description: zm-testagent checks the Zonemaster Backend database for new 13 | # tests, executes them and writes back progress and results. 14 | ### END INIT INFO 15 | 16 | BINDIR=${ZM_BACKEND_BINDIR:-/usr/local/bin} 17 | LOGFILE=${ZM_BACKEND_LOGFILE:-/var/log/zonemaster/zm-testagent.log} 18 | OUTFILE=${ZM_BACKEND_OUTFILE:-/var/log/zonemaster/zm-testagent.out} 19 | PIDFILE=${ZM_BACKEND_PIDFILE:-/var/run/zonemaster/zm-testagent.pid} 20 | USER=${ZM_BACKEND_USER:-zonemaster} 21 | GROUP=${ZM_BACKEND_GROUP:-zonemaster} 22 | 23 | #ZM_BACKEND_TESTAGENT_LOGLEVEL='info' # Set this variable to override the default log level 24 | 25 | testagent_args="--logfile=$LOGFILE --outfile=$OUTFILE --pidfile=$PIDFILE --user=$USER --group=$GROUP" 26 | if [ -n "$ZM_BACKEND_TESTAGENT_LOGLEVEL" ] ; then 27 | testagent_args="$testagent_args --loglevel=$ZM_BACKEND_TESTAGENT_LOGLEVEL" 28 | fi 29 | 30 | start () { 31 | $BINDIR/zonemaster_backend_testagent $testagent_args start || exit 1 32 | } 33 | 34 | stop () { 35 | $BINDIR/zonemaster_backend_testagent $testagent_args stop 36 | } 37 | 38 | status () { 39 | $BINDIR/zonemaster_backend_testagent $testagent_args status 40 | } 41 | 42 | case "$1" in 43 | start) 44 | start 45 | ;; 46 | stop) 47 | stop 48 | ;; 49 | restart|force-reload) 50 | stop 51 | start 52 | ;; 53 | status) 54 | status 55 | ;; 56 | *) 57 | echo "usage: $0 [start|stop|restart|status]" 58 | exit 1 59 | esac 60 | exit 0 61 | -------------------------------------------------------------------------------- /docs/internal-documentation/maintenance/Garbage-Collection-Testing.md: -------------------------------------------------------------------------------- 1 | # Testing instructions for the Garbage Collection feature of the Zonemaster Backend module 2 | 3 | ## Introduction 4 | The purpose of this instruction is to serve as a notice for manual testing of the new garbage collection feature at release time. 5 | 6 | ## Testing the unfinished tests garbage collection feature 7 | 8 | 9 | 1. Start a test and wait for it to be finished 10 | 11 | ``` 12 | SELECT hash_id, progress FROM test_results LIMIT 1; 13 | ``` 14 | Should return: 15 | 16 | ``` 17 | hash_id | progress 18 | ------------------+---------- 19 | 3f7a604683efaf93 | 100 20 | (1 row) 21 | ``` 22 | 23 | 2. Simulate a crashed test 24 | ``` 25 | UPDATE test_results SET progress = 50, test_start_time = '2020-01-01' WHERE hash_id = '3f7a604683efaf93'; 26 | ``` 27 | 28 | 3. Check that the backend finishes the test with a result stating it was unfinished 29 | 30 | ``` 31 | SELECT hash_id, progress FROM test_results WHERE hash_id = '3f7a604683efaf93'; 32 | ``` 33 | Should return a finished result: 34 | ``` 35 | hash_id | progress 36 | ------------------+---------- 37 | 3f7a604683efaf93 | 100 38 | (1 row) 39 | ``` 40 | 41 | 4. Ensure the test result contains the backend generated critical message: 42 | ``` 43 | {"tag":"UNABLE_TO_FINISH_TEST","level":"CRITICAL","timestamp":"300","module":"BACKEND_TEST_AGENT"} 44 | ``` 45 | 46 | ``` 47 | SELECT hash_id, progress FROM test_results WHERE hash_id = '3f7a604683efaf93' AND results::text like '%UNABLE_TO_FINISH_TEST%'; 48 | ``` 49 | _Remark: for MySQL queries remove the `::text` from all queries_ 50 | 51 | Should return: 52 | ``` 53 | hash_id | progress 54 | ------------------+---------- 55 | 3f7a604683efaf93 | 100 56 | (1 row) 57 | 58 | ``` 59 | 60 | -------------------------------------------------------------------------------- /t/po-files.t: -------------------------------------------------------------------------------- 1 | #!perl 2 | 3 | # This file is not included in the distribution package and not run 4 | # at installation with cpanm(). 5 | 6 | use v5.14.2; 7 | use strict; 8 | use warnings; 9 | use utf8; 10 | use Test::More; # see done_testing() 11 | 12 | use File::Basename qw( dirname ); 13 | 14 | chdir dirname( dirname( __FILE__ ) ) or BAIL_OUT( "chdir: $!" ); 15 | chdir 'share' or BAIL_OUT( "chdir: $!" ); 16 | 17 | my $makebin = 'make'; 18 | 19 | sub make { 20 | my @make_args = @_; 21 | 22 | undef $ENV{MAKEFLAGS}; 23 | 24 | my $command = join( ' ', $makebin, '--silent', '--no-print-directory', @make_args ); 25 | my $output = `$command`; 26 | 27 | if ( $? == -1 ) { 28 | BAIL_OUT( "failed to execute: $!" ); 29 | } 30 | elsif ( $? & 127 ) { 31 | BAIL_OUT( "child died with signal %d, %s coredump\n", ( $? & 127 ), ( $? & 128 ) ? 'with' : 'without' ); 32 | } 33 | 34 | return $output, $? >> 8; 35 | } 36 | 37 | subtest "no fuzzy marks" => sub { 38 | my ( $output, $status ) = make "show-fuzzy"; 39 | is $status, 0, $makebin . ' show-fuzzy exits with value 0'; 40 | is $output, "", $makebin . ' show-fuzzy gives empty output'; 41 | }; 42 | 43 | subtest "check po files" => sub { 44 | my ( $output, $status ) = make "check-po"; 45 | is $status, 0, $makebin . ' check-po exits with value 0'; 46 | is $output, "", $makebin . ' check-po gives empty output'; 47 | }; 48 | 49 | subtest "tidy po files" => sub { 50 | SKIP: { 51 | my ( $output, $status ); 52 | 53 | $output = `git diff --numstat`; 54 | 55 | skip 'git repo should be clean to run this test', 3 if $output ne ''; 56 | 57 | ( $output, $status ) = make "tidy-po"; 58 | is $status, 0, $makebin . ' tidy-po exits with value 0'; 59 | is $output, "", $makebin . ' tidy-po gives empty output'; 60 | 61 | $output = `git diff --numstat`; 62 | is $output, "", 'all files are tidied (if not run "make tidy-po")'; 63 | } 64 | }; 65 | 66 | done_testing(); 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ### Code license 2 | 3 | Copyright (c) The Swedish Internet Foundation () 4 | Copyright (c) AFNIC () 5 | All rights reserved. 6 | 7 | Copyright belongs to external contributor where applicable. 8 | 9 | Redistribution and use in source and binary forms, with or without modification, 10 | are permitted provided that the following conditions are met: 11 | 12 | Redistributions of source code must retain the above copyright notice, this 13 | list of conditions and the following disclaimer. 14 | 15 | Redistributions in binary form must reproduce the above copyright notice, this 16 | list of conditions and the following disclaimer in the documentation and/or 17 | other materials provided with the distribution. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 23 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | 31 | ### Documentation license 32 | 33 | Copyright (c) The Swedish Internet Foundation () 34 | Copyright (c) AFNIC () 35 | All rights reserved. 36 | 37 | Copyright belongs to external contributor where applicable. 38 | 39 | Creative Commons Attribution 4.0 International License 40 | 41 | You should have received a copy of the license along with this 42 | work. If not, see . 43 | -------------------------------------------------------------------------------- /t/test_profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "logfilter": { 3 | "BASIC":{ 4 | "IPV6_DISABLED" : [ 5 | { 6 | "when": { 7 | "rrtype": [ "SOA", "NS" ] 8 | }, 9 | "set": "INFO" 10 | } 11 | ] 12 | } 13 | }, 14 | "test_cases": [ 15 | "address01", 16 | "address02", 17 | "address03", 18 | "basic00", 19 | "basic01", 20 | "basic02", 21 | "basic03", 22 | "connectivity01", 23 | "connectivity02", 24 | "connectivity03", 25 | "consistency01", 26 | "consistency02", 27 | "consistency03", 28 | "consistency04", 29 | "consistency05", 30 | "consistency06", 31 | "dnssec01", 32 | "dnssec02", 33 | "dnssec03", 34 | "dnssec04", 35 | "dnssec05", 36 | "dnssec07", 37 | "dnssec06", 38 | "dnssec08", 39 | "dnssec09", 40 | "dnssec10", 41 | "dnssec11", 42 | "dnssec13", 43 | "dnssec14", 44 | "dnssec15", 45 | "dnssec16", 46 | "dnssec17", 47 | "dnssec18", 48 | "delegation01", 49 | "delegation02", 50 | "delegation03", 51 | "delegation04", 52 | "delegation05", 53 | "delegation06", 54 | "delegation07", 55 | "nameserver01", 56 | "nameserver02", 57 | "nameserver04", 58 | "nameserver05", 59 | "nameserver06", 60 | "nameserver07", 61 | "nameserver10", 62 | "nameserver11", 63 | "nameserver12", 64 | "nameserver13", 65 | "syntax01", 66 | "syntax02", 67 | "syntax03", 68 | "syntax04", 69 | "syntax05", 70 | "syntax06", 71 | "syntax07", 72 | "syntax08", 73 | "zone01", 74 | "zone02", 75 | "zone03", 76 | "zone04", 77 | "zone05", 78 | "zone06", 79 | "zone07", 80 | "zone08", 81 | "zone09", 82 | "zone10" 83 | ] 84 | } 85 | -------------------------------------------------------------------------------- /t/test_profile_no_network.json: -------------------------------------------------------------------------------- 1 | { 2 | "no_network" : true, 3 | "logfilter": { 4 | "BASIC":{ 5 | "IPV6_DISABLED" : [ 6 | { 7 | "when": { 8 | "rrtype": [ "SOA", "NS" ] 9 | }, 10 | "set": "INFO" 11 | } 12 | ] 13 | } 14 | }, 15 | "test_cases": [ 16 | "address01", 17 | "address02", 18 | "address03", 19 | "basic00", 20 | "basic01", 21 | "basic02", 22 | "basic03", 23 | "connectivity01", 24 | "connectivity02", 25 | "connectivity03", 26 | "consistency01", 27 | "consistency02", 28 | "consistency03", 29 | "consistency04", 30 | "consistency05", 31 | "consistency06", 32 | "dnssec01", 33 | "dnssec02", 34 | "dnssec03", 35 | "dnssec04", 36 | "dnssec05", 37 | "dnssec07", 38 | "dnssec06", 39 | "dnssec08", 40 | "dnssec09", 41 | "dnssec10", 42 | "dnssec11", 43 | "dnssec13", 44 | "dnssec14", 45 | "dnssec15", 46 | "dnssec16", 47 | "dnssec17", 48 | "dnssec18", 49 | "delegation01", 50 | "delegation02", 51 | "delegation03", 52 | "delegation04", 53 | "delegation05", 54 | "delegation06", 55 | "delegation07", 56 | "nameserver01", 57 | "nameserver02", 58 | "nameserver04", 59 | "nameserver05", 60 | "nameserver06", 61 | "nameserver07", 62 | "nameserver10", 63 | "nameserver11", 64 | "nameserver12", 65 | "nameserver13", 66 | "syntax01", 67 | "syntax02", 68 | "syntax03", 69 | "syntax04", 70 | "syntax05", 71 | "syntax06", 72 | "syntax07", 73 | "syntax08", 74 | "zone01", 75 | "zone02", 76 | "zone03", 77 | "zone04", 78 | "zone05", 79 | "zone06", 80 | "zone07", 81 | "zone08", 82 | "zone09", 83 | "zone10" 84 | ] 85 | } 86 | -------------------------------------------------------------------------------- /t/test_profile_network_true.json: -------------------------------------------------------------------------------- 1 | { 2 | "no_network" : false, 3 | "logfilter": { 4 | "BASIC":{ 5 | "IPV6_DISABLED" : [ 6 | { 7 | "when": { 8 | "rrtype": [ "SOA", "NS" ] 9 | }, 10 | "set": "INFO" 11 | } 12 | ] 13 | } 14 | }, 15 | "test_cases": [ 16 | "address01", 17 | "address02", 18 | "address03", 19 | "basic00", 20 | "basic01", 21 | "basic02", 22 | "basic03", 23 | "connectivity01", 24 | "connectivity02", 25 | "connectivity03", 26 | "consistency01", 27 | "consistency02", 28 | "consistency03", 29 | "consistency04", 30 | "consistency05", 31 | "consistency06", 32 | "dnssec01", 33 | "dnssec02", 34 | "dnssec03", 35 | "dnssec04", 36 | "dnssec05", 37 | "dnssec07", 38 | "dnssec06", 39 | "dnssec08", 40 | "dnssec09", 41 | "dnssec10", 42 | "dnssec11", 43 | "dnssec13", 44 | "dnssec14", 45 | "dnssec15", 46 | "dnssec16", 47 | "dnssec17", 48 | "dnssec18", 49 | "delegation01", 50 | "delegation02", 51 | "delegation03", 52 | "delegation04", 53 | "delegation05", 54 | "delegation06", 55 | "delegation07", 56 | "nameserver01", 57 | "nameserver02", 58 | "nameserver04", 59 | "nameserver05", 60 | "nameserver06", 61 | "nameserver07", 62 | "nameserver10", 63 | "nameserver11", 64 | "nameserver12", 65 | "nameserver13", 66 | "syntax01", 67 | "syntax02", 68 | "syntax03", 69 | "syntax04", 70 | "syntax05", 71 | "syntax06", 72 | "syntax07", 73 | "syntax08", 74 | "zone01", 75 | "zone02", 76 | "zone03", 77 | "zone04", 78 | "zone05", 79 | "zone06", 80 | "zone07", 81 | "zone08", 82 | "zone09", 83 | "zone10" 84 | ] 85 | } 86 | -------------------------------------------------------------------------------- /share/zm-rpcapi.lsb: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | ### BEGIN INIT INFO 4 | # Provides: zm-rpcapi 5 | # Required-Start: $network $local_fs 6 | # Required-Stop: $network $local_fs 7 | # Should-Start: mysql postgresql 8 | # Should-Stop: mysql postgresql 9 | # Default-Start: 2 3 4 5 10 | # Default-Stop: 0 1 6 11 | # Short-Description: A JSON-RPC frontend for Zonemaster Backend 12 | # Description: zm-rpcapi lets you add new tests and check for results in 13 | # the the Zonemaster Backend database 14 | ### END INIT INFO 15 | 16 | BINDIR=${ZM_BACKEND_BINDIR:-/usr/local/bin} 17 | LOGFILE=${ZM_BACKEND_LOGFILE:-/var/log/zonemaster/zm-rpcapi.log} 18 | PIDFILE=${ZM_BACKEND_PIDFILE:-/var/run/zonemaster/zm-rpcapi.pid} 19 | LISTENIP=${ZM_BACKEND_LISTENIP:-127.0.0.1} 20 | LISTENPORT=${ZM_BACKEND_LISTENPORT:-5000} 21 | USER=${ZM_BACKEND_USER:-zonemaster} 22 | GROUP=${ZM_BACKEND_GROUP:-zonemaster} 23 | 24 | STARMAN=`PATH="$PATH:/usr/local/bin" /usr/bin/which starman` 25 | #export ZM_BACKEND_RPCAPI_LOGLEVEL='warning' # Set this variable to override the default log level 26 | 27 | . /lib/lsb/init-functions 28 | 29 | start () { 30 | $STARMAN --listen=$LISTENIP:$LISTENPORT --preload-app --user=$USER --group=$GROUP --pid=$PIDFILE --error-log=$LOGFILE --daemonize $BINDIR/zonemaster_backend_rpcapi.psgi || exit 1 31 | } 32 | 33 | stop () { 34 | if [ -f $PIDFILE ] 35 | then 36 | kill `cat $PIDFILE` 37 | fi 38 | } 39 | 40 | status () { 41 | status="0" 42 | pidofproc -p "$PIDFILE" starman >/dev/null || status="$?" 43 | if [ "$status" = 0 ]; then 44 | log_success_msg "zm-rpcapi is running" 45 | return 0 46 | elif [ "$status" = 4 ]; then 47 | log_failure_msg "could not access PID file for zm-rpcapi" 48 | return $status 49 | else 50 | log_failure_msg "zm-rpcapi is not running" 51 | return $status 52 | fi 53 | } 54 | 55 | case "$1" in 56 | start) 57 | start 58 | ;; 59 | stop) 60 | stop 61 | ;; 62 | restart|force-reload) 63 | stop 64 | start 65 | ;; 66 | status) 67 | status 68 | ;; 69 | *) 70 | echo "usage: $0 [start|stop|restart|force-reload|status]" 71 | exit 1 72 | esac 73 | exit 0 74 | -------------------------------------------------------------------------------- /share/nb.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: 1.0.0\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2023-05-24 09:36+0000\n" 6 | "PO-Revision-Date: 2025-04-25 08:30+0200\n" 7 | "Last-Translator: richard.persson@norid.no\n" 8 | "Language-Team: Zonemaster project\n" 9 | "Language: nb\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | 14 | msgid "Invalid method parameter(s)." 15 | msgstr "Ugyldig metodeparameter." 16 | 17 | msgid "Missing property" 18 | msgstr "Mangler verdi" 19 | 20 | msgid "" 21 | "Warning: Zonemaster::LDNS not compiled with IDN support, cannot handle non-" 22 | "ASCII names correctly." 23 | msgstr "" 24 | "Advarsel: Zonemaster::LDNS er ikke kompilert med IDN-støtte. Kan bare " 25 | "håndtere ASCII-navn." 26 | 27 | #. BACKEND_TEST_AGENT:TEST_DIED 28 | msgid "An error occured and Zonemaster could not start or finish the test." 29 | msgstr "" 30 | "Det oppstod en feil og Zonemaster kunne ikke starte eller fullføre testen." 31 | 32 | #. BACKEND_TEST_AGENT:UNABLE_TO_FINISH_TEST 33 | #, perl-brace-format 34 | msgid "" 35 | "The test took too long to run (the current limit is {max_execution_time} " 36 | "seconds). Maybe there are too many name servers or the name servers are " 37 | "either unreachable or not responsive enough." 38 | msgstr "" 39 | "Testen tok for lang tid å kjøre (gjeldende grense er {max_execution_time} " 40 | "sekunder). Kanskje er det for mange navnetjenere eller navnetjenerne er " 41 | "enten utilgjengelige eller ikke responsive nok." 42 | 43 | msgid "Invalid digest format" 44 | msgstr "Ugyldig format på digest" 45 | 46 | msgid "Algorithm must be a positive integer" 47 | msgstr "Algoritme må være et positivt tall" 48 | 49 | msgid "Digest type must be a positive integer" 50 | msgstr "Algoritme-type må være et positivt tall" 51 | 52 | msgid "Keytag must be a positive integer" 53 | msgstr "Keytag må være et positivt tall" 54 | 55 | msgid "Domain name required" 56 | msgstr "Domenenavn påkrevd" 57 | 58 | msgid "Invalid language tag format" 59 | msgstr "Ugyldig format på språk-tag" 60 | 61 | msgid "Unkown language string" 62 | msgstr "Ukjent språk-tag" 63 | 64 | msgid "Invalid IP address" 65 | msgstr "Ugylding IP-adresse" 66 | 67 | msgid "Invalid profile format" 68 | msgstr "Ugyldig format på profil" 69 | 70 | msgid "Unknown profile" 71 | msgstr "Ukjent profil" 72 | -------------------------------------------------------------------------------- /share/GNUmakefile: -------------------------------------------------------------------------------- 1 | .POSIX: 2 | .SUFFIXES: .po .mo 3 | .PHONY: all check-msg-args dist extract-pot tidy-po show-fuzzy update-po new-po check-po 4 | 5 | POFILES := $(shell find . -maxdepth 1 -type f -name '*.po' -exec basename {} \;) 6 | MOFILES := $(POFILES:%.po=%.mo) 7 | POTFILE = Zonemaster-Backend.pot 8 | PMFILES := $(shell find ../lib -type f -name '*.pm' | sort) 9 | 10 | all: $(MOFILES) 11 | @echo 12 | @echo Remember to make sure all of the above names are in the 13 | @echo MANIFEST file, or they will not be installed. 14 | @echo 15 | 16 | # Tidy the formatting of all PO files 17 | tidy-po: 18 | @tmpdir="`mktemp -d tidy-po.XXXXXXXX`" ;\ 19 | trap 'rm -rf "$$tmpdir"' EXIT ;\ 20 | for f in $(POFILES) ; do msgcat $$f -o $$tmpdir/$$f && mv -f $$tmpdir/$$f $$f ; done 21 | 22 | update-po: extract-pot 23 | @for f in $(POFILES) ; do msgmerge --update --backup=none --quiet --no-location $(MSGMERGE_OPTS) $$f $(POTFILE) ; done 24 | 25 | # Create a new empty PO file with basename provided with the POLANG variable 26 | # Update the Language field in the header 27 | new-po: extract-pot 28 | @[ -n "$(POLANG)" ] || ( echo "Usage: make POLANG=xx new-po" && exit 1 ) 29 | @cp $(POTFILE) $(POLANG).po 30 | @perl -pi -e 's/^("Project-Id-Version:) .+(\\n)/$$1 1.0.0$$2/;' \ 31 | -e 's/^("Language-Team:) .+(\\n)/$$1 Zonemaster Team$$2/;' \ 32 | -e 's/^"Language: /$$&$(POLANG)/;' \ 33 | -e 's/^("Content-Type:.+charset=)CHARSET/$${1}UTF-8/;' $(POLANG).po 34 | @perl -ni -e 'print unless /^#( |$$)/' $(POLANG).po 35 | 36 | # Check the msgid/msgstr pair for some inconsistencies between them in the 37 | # selected PO file and report on standard error any errors found. The PO file 38 | # is not updated. 39 | check-po: 40 | @for f in $(POFILES) ; do msgfmt -c $$f ; done 41 | 42 | extract-pot: 43 | @xgettext --output $(POTFILE) --sort-by-file --add-comments --language=Perl --from-code=UTF-8 -k__ -k\$$__ -k%__ -k__x -k__n:1,2 -k__nx:1,2 -k__xn:1,2 -kN__ -kN__n:1,2 -k__p:1c,2 -k__np:1c,2,3 -kN__p:1c,2 -kN__np:1c,2,3 $(PMFILES) 44 | 45 | $(POTFILE): extract-pot 46 | 47 | .po.mo: 48 | @msgfmt -o $@ $< 49 | @mkdir -p locale/`basename $@ .mo`/LC_MESSAGES 50 | @ln -vf $@ locale/`basename $@ .mo`/LC_MESSAGES/Zonemaster-Backend.mo 51 | 52 | show-fuzzy: 53 | @for f in $(POFILES) ; do msgattrib --only-fuzzy $$f ; done 54 | 55 | check-msg-args: 56 | @for f in $(POFILES) ; do ../util/check-msg-args $$f ; done 57 | -------------------------------------------------------------------------------- /share/fr.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: 1.0.0\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2025-02-18 11:20+0100\n" 6 | "PO-Revision-Date: 2023-05-22 07:17+0200\n" 7 | "Last-Translator: thomas.green@afnic.fr\n" 8 | "Language-Team: Zonemaster project\n" 9 | "Language: fr\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | 14 | msgid "Invalid method parameter(s)." 15 | msgstr "Paramètre(s) incorrect(s)." 16 | 17 | msgid "Missing property" 18 | msgstr "Propriété manquante" 19 | 20 | msgid "" 21 | "Warning: Zonemaster::LDNS not compiled with IDN support, cannot handle non-" 22 | "ASCII names correctly." 23 | msgstr "" 24 | "Attention : Zonemaster::LDNS n'est pas compilé avec le support IDN, " 25 | "impossible de traiter correctement les noms non ASCII." 26 | 27 | #. BACKEND_TEST_AGENT:TEST_DIED 28 | msgid "An error occured and Zonemaster could not start or finish the test." 29 | msgstr "" 30 | "Une erreur est survenue et Zonemaster n’a pas pu commencer ou finir le test." 31 | 32 | #. BACKEND_TEST_AGENT:UNABLE_TO_FINISH_TEST 33 | #, perl-brace-format 34 | msgid "" 35 | "The test took too long to run (the current limit is {max_execution_time} " 36 | "seconds). Maybe there are too many name servers or the name servers are " 37 | "either unreachable or not responsive enough." 38 | msgstr "" 39 | "Le test a mis trop de temps à s’exécuter (la limite actuelle est de " 40 | "{max_execution_time} secondes). Il y a peut-être trop de serveurs de noms, " 41 | "ou les serveurs de noms sont injoignables ou trop peu réactifs." 42 | 43 | msgid "Invalid digest format" 44 | msgstr "Format du condensat non valide" 45 | 46 | msgid "Algorithm must be a positive integer" 47 | msgstr "L'algorithme doit être un entier positif" 48 | 49 | msgid "Digest type must be a positive integer" 50 | msgstr "Le type d'empreinte doit être un entier positif" 51 | 52 | msgid "Keytag must be a positive integer" 53 | msgstr "L'identifiant de clef doit être un entier positif" 54 | 55 | msgid "Domain name required" 56 | msgstr "Nom de domaine requis" 57 | 58 | msgid "Invalid language tag format" 59 | msgstr "Format de l'étiquette d'identification de langue incorrect" 60 | 61 | msgid "Unkown language string" 62 | msgstr "Étiquette d'identification de langue inconnue" 63 | 64 | msgid "Invalid IP address" 65 | msgstr "Adresse IP non valide" 66 | 67 | msgid "Invalid profile format" 68 | msgstr "Format du profil non valide" 69 | 70 | msgid "Unknown profile" 71 | msgstr "Profil inconnu" 72 | -------------------------------------------------------------------------------- /t/queue.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use 5.14.2; 4 | 5 | use Test::More tests => 2; 6 | use Test::NoWarnings; 7 | use Log::Any::Test; 8 | 9 | use File::Basename qw( dirname ); 10 | use File::Spec::Functions qw( rel2abs ); 11 | use File::Temp qw( tempdir ); 12 | use Log::Any qw( $log ); 13 | use Test::Differences; 14 | use Test::Exception; 15 | use Zonemaster::Backend::Config; 16 | use Zonemaster::Backend::DB qw( $TEST_RUNNING ); 17 | use Zonemaster::Engine; 18 | 19 | my $t_path; 20 | BEGIN { 21 | $t_path = dirname( rel2abs( $0 ) ); 22 | } 23 | use lib $t_path; 24 | use TestUtil; 25 | 26 | my $db_backend = TestUtil::db_backend(); 27 | my $tempdir = tempdir( CLEANUP => 1 ); 28 | my $config = Zonemaster::Backend::Config->parse( < sub { 52 | lives_ok { # Make sure we get to print log messages in case of errors. 53 | my $db = TestUtil::init_db( $config ); 54 | 55 | subtest 'Claiming waiting tests for processing' => sub { 56 | eq_or_diff 57 | [ $db->get_test_request( undef ) ], 58 | [ undef, undef ], 59 | "An empty list is returned when queue is empty"; 60 | 61 | my $testid1 = $db->create_new_test( "1.claim.test", {}, 10 ); 62 | eq_or_diff 63 | [ $db->get_test_request( undef ) ], 64 | [ $testid1, undef ], 65 | "A waiting test is returned if one is available"; 66 | eq_or_diff 67 | [ $db->get_test_request( undef ) ], 68 | [ undef, undef ], 69 | "Claimed test is removed from queue"; 70 | is 71 | $db->test_state( $testid1 ), 72 | $TEST_RUNNING, 73 | "Claimed test is in 'running' state"; 74 | }; 75 | }; 76 | }; 77 | 78 | for my $msg ( @{ $log->msgs } ) { 79 | my $text = sprintf( "%s: %s", $msg->{level}, $msg->{message} ); 80 | if ( $msg->{level} =~ /trace|debug|info|notice/ ) { 81 | note $text; 82 | } 83 | else { 84 | diag $text; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zonemaster Backend 2 | 3 | ### Purpose 4 | This repository is one of the components of the Zonemaster software. For an 5 | overview of the Zonemaster software, please see the 6 | [Zonemaster repository]. 7 | 8 | This module is the Backend JSON/RPC weservice for the Web Interface part of 9 | the Zonemaster project. It offers a JSON/RPC api to run tests one by one 10 | (as the zonemaster-gui web frontend module does, or by using a batch API to 11 | run the Zonemaster engine on many domains) 12 | 13 | A Zonemaster user needs to install the backend only in the case where there is a 14 | need of logging the Zonemaster test runs in one's own respective database for 15 | analysing. 16 | 17 | 18 | ### Prerequisites 19 | 20 | Before you install the Zonemaster Backend, you need the 21 | Zonemaster Engine installed. Please see the 22 | [Zonemaster Engine installation instructions][Zonemaster-Engine installation]. 23 | 24 | 25 | ### Upgrade 26 | 27 | See the [upgrade document]. 28 | 29 | 30 | ### Installation 31 | 32 | Follow the detailed [installation instructions]. 33 | 34 | 35 | ### Configuration 36 | 37 | See the [configuration documentation]. 38 | 39 | 40 | ### Documentation 41 | 42 | The Zonemaster Backend documentation is split up into several documents: 43 | 44 | * A number of [Typographic Conventions] are used throughout this documentation. 45 | * The [Architecture] document describes each of the Zonemaster Backend 46 | components and how they operate. It also discusses all central concepts 47 | needed to understand the Zonemaster backend, and contains a glossary over 48 | domain specific technical terms. 49 | * The [Getting Started] guide walks you through creating a *test* and following 50 | it through its life cycle, all using JSON-RPC calls to the *RPC API daemon*. 51 | * The [API] documentation describes the *RPC API daemon* inteface in detail. 52 | 53 | 54 | ## License 55 | 56 | This is free software under a 2-clause BSD license. The full text of the license can 57 | be found in the [LICENSE](LICENSE) file included in this respository. 58 | 59 | 60 | [API]: https://github.com/zonemaster/zonemaster/blob/master/docs/public/using/backend/rpcapi-reference.md 61 | [Architecture]: docs/Architecture.md 62 | [Configuration documentation]: https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md 63 | [Getting Started]: https://github.com/zonemaster/zonemaster/blob/master/docs/public/using/backend/getting-started.md 64 | [Installation instructions]: https://github.com/zonemaster/zonemaster/blob/master/docs/public/installation/zonemaster-backend.md 65 | [Typographic Conventions]: docs/TypographicConventions.md 66 | [Upgrade document]: https://github.com/zonemaster/zonemaster/blob/master/docs/public/upgrading/backend.md 67 | [Zonemaster-Engine installation]: https://github.com/zonemaster/zonemaster/blob/master/docs/public/installation/zonemaster-engine.md 68 | [Zonemaster repository]: https://github.com/zonemaster/zonemaster 69 | -------------------------------------------------------------------------------- /share/da.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: 1.0.0\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2023-05-26 14:36+0000\n" 6 | "PO-Revision-Date: 2023-05-26 14:33+0000\n" 7 | "Last-Translator: haarbo@dk-hostmaster.dk\n" 8 | "Language-Team: Zonemaster project\n" 9 | "Language: da\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | 14 | msgid "Invalid method parameter(s)." 15 | msgstr "Invalid metodeparametre" 16 | 17 | msgid "Missing property" 18 | msgstr "Manglende egenskab" 19 | 20 | msgid "" 21 | "Warning: Zonemaster::LDNS not compiled with IDN support, cannot handle non-" 22 | "ASCII names correctly." 23 | msgstr "" 24 | "Advarsel: Zonemaster::LDNS ikke kompileret med IDN-support, og kan derfor " 25 | "ikke håndtere ikke-ascii-navne korrekt" 26 | 27 | #. BACKEND_TEST_AGENT:TEST_DIED 28 | msgid "An error occured and Zonemaster could not start or finish the test." 29 | msgstr "" 30 | "Der opstod en fejl, og Zonemaster kunne ikke starte eller afslutte testen." 31 | 32 | #. BACKEND_TEST_AGENT:UNABLE_TO_FINISH_TEST 33 | #, perl-brace-format 34 | msgid "" 35 | "The test took too long to run (the current limit is {max_execution_time} " 36 | "seconds). Maybe there are too many name servers or the name servers are " 37 | "either unreachable or not responsive enough." 38 | msgstr "" 39 | "Testen tog for lang tid at afvikle (den aktuelle grænse er " 40 | "{max_execution_time} sekunder). Måske er der for mange navneservere, eller " 41 | "også er navneserverne enten ikke tilgængelige eller ikke responsive nok." 42 | 43 | msgid "Invalid digest format" 44 | msgstr "Invalid digest-format" 45 | 46 | msgid "Algorithm must be a positive integer" 47 | msgstr "Algoritmen skal være et positivt heltal" 48 | 49 | msgid "Digest type must be a positive integer" 50 | msgstr "Digest type skal være et positivt heltal" 51 | 52 | msgid "Keytag must be a positive integer" 53 | msgstr "Keytag skal være et positivt heltal" 54 | 55 | msgid "Domain name required" 56 | msgstr "Domænenavn påkrævet" 57 | 58 | msgid "The domain name is IDNA invalid" 59 | msgstr "Domænenavnet er IDNA ugyldigt" 60 | 61 | msgid "" 62 | "The domain name contains non-ascii characters and IDNA support is not " 63 | "installed" 64 | msgstr "" 65 | "Domænenavnet indeholder ikke-ascii-tegn og IDNA-support er ikke installeret" 66 | 67 | msgid "The domain name character(s) are not supported" 68 | msgstr "Domænenavnets tegn er ikke understøttet" 69 | 70 | msgid "The domain name contains consecutive dots" 71 | msgstr "Domænenavnet indeholder flere dots (.) efter hinanden" 72 | 73 | msgid "The domain name or label is too long" 74 | msgstr "Domænenavnet eller labelen er for lang" 75 | 76 | msgid "Invalid language tag format" 77 | msgstr "Invalidt sprogkode-format" 78 | 79 | msgid "Unkown language string" 80 | msgstr "Ukendt sprogstreng" 81 | 82 | msgid "Invalid IP address" 83 | msgstr "Invalid IP-adresse" 84 | 85 | msgid "Invalid profile format" 86 | msgstr "Invalidt profil-format" 87 | 88 | msgid "Unknown profile" 89 | msgstr "Ukendt profil" 90 | -------------------------------------------------------------------------------- /share/sl.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: 1.0.0\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2024-09-18 08:43+0200\n" 6 | "PO-Revision-Date: 2024-09-18 10:05+0200\n" 7 | "Last-Translator: milijan@arnes.si\n" 8 | "Language-Team: Zonemaster project\n" 9 | "Language: sl\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "X-Generator: Poedit 3.5\n" 14 | 15 | #: ../lib/Zonemaster/Backend/RPCAPI.pm:858 16 | msgid "Invalid method parameter(s)." 17 | msgstr "Nepravilni parametri." 18 | 19 | #: ../lib/Zonemaster/Backend/RPCAPI.pm:888 20 | msgid "Missing property" 21 | msgstr "Manjkajoče polje" 22 | 23 | #: ../lib/Zonemaster/Backend/TestAgent.pm:213 24 | msgid "" 25 | "Warning: Zonemaster::LDNS not compiled with IDN support, cannot handle non-" 26 | "ASCII names correctly." 27 | msgstr "" 28 | "Opozorilo: Zonemaster::LDNS ne podpira IDN, ni mogoče obdelati ne-ASCII " 29 | "imena." 30 | 31 | #. BACKEND_TEST_AGENT:TEST_DIED 32 | #: ../lib/Zonemaster/Backend/Translator.pm:23 33 | msgid "An error occured and Zonemaster could not start or finish the test." 34 | msgstr "Zgodila se je napaka, Zonemaster ne more začeti ali končati testa." 35 | 36 | #. BACKEND_TEST_AGENT:UNABLE_TO_FINISH_TEST 37 | #: ../lib/Zonemaster/Backend/Translator.pm:27 38 | #, perl-brace-format 39 | msgid "" 40 | "The test took too long to run (the current limit is {max_execution_time} " 41 | "seconds). Maybe there are too many name servers or the name servers are " 42 | "either unreachable or not responsive enough." 43 | msgstr "" 44 | "Test traja predolgo (trenutna meja je {max_execution_time} sekund). Mogoče " 45 | "je preveč strežnikov za preveriti, ali so neodzivni ali pa počasni." 46 | 47 | #: ../lib/Zonemaster/Backend/Validator.pm:162 48 | msgid "Invalid digest format" 49 | msgstr "Nepravilen digest format" 50 | 51 | #: ../lib/Zonemaster/Backend/Validator.pm:167 52 | msgid "Algorithm must be a positive integer" 53 | msgstr "Algoritem mora biti pozitivno število" 54 | 55 | #: ../lib/Zonemaster/Backend/Validator.pm:172 56 | msgid "Digest type must be a positive integer" 57 | msgstr "Tip izvlečka za DS mora biti pozitivno število" 58 | 59 | #: ../lib/Zonemaster/Backend/Validator.pm:177 60 | msgid "Keytag must be a positive integer" 61 | msgstr "Oznaka za ključ mora biti pozitivno število" 62 | 63 | #: ../lib/Zonemaster/Backend/Validator.pm:282 64 | msgid "Domain name required" 65 | msgstr "Domena je obvezna" 66 | 67 | #: ../lib/Zonemaster/Backend/Validator.pm:314 68 | msgid "Invalid language tag format" 69 | msgstr "Nepravilen format zastavice za jezik" 70 | 71 | #: ../lib/Zonemaster/Backend/Validator.pm:317 72 | msgid "Unkown language string" 73 | msgstr "Neznan jezik" 74 | 75 | #: ../lib/Zonemaster/Backend/Validator.pm:332 76 | msgid "Invalid IP address" 77 | msgstr "Neveljaven IP naslov" 78 | 79 | #: ../lib/Zonemaster/Backend/Validator.pm:356 80 | msgid "Invalid profile format" 81 | msgstr "Neveljaven format profila" 82 | 83 | #: ../lib/Zonemaster/Backend/Validator.pm:360 84 | msgid "Unknown profile" 85 | msgstr "Neznan profil" 86 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | Changes 2 | CONTRIBUTING.md 3 | docs/Architecture.md 4 | docs/files-description.md 5 | docs/TypographicConventions.md 6 | inc/Module/Install.pm 7 | inc/Module/Install/Base.pm 8 | inc/Module/Install/Can.pm 9 | inc/Module/Install/External.pm 10 | inc/Module/Install/Fetch.pm 11 | inc/Module/Install/Makefile.pm 12 | inc/Module/Install/Metadata.pm 13 | inc/Module/Install/Scripts.pm 14 | inc/Module/Install/Share.pm 15 | inc/Module/Install/Win32.pm 16 | inc/Module/Install/WriteAll.pm 17 | lib/Zonemaster/Backend.pm 18 | lib/Zonemaster/Backend/Config.pm 19 | lib/Zonemaster/Backend/Config/DCPlugin.pm 20 | lib/Zonemaster/Backend/DB.pm 21 | lib/Zonemaster/Backend/DB/MySQL.pm 22 | lib/Zonemaster/Backend/DB/PostgreSQL.pm 23 | lib/Zonemaster/Backend/DB/SQLite.pm 24 | lib/Zonemaster/Backend/Errors.pm 25 | lib/Zonemaster/Backend/Log.pm 26 | lib/Zonemaster/Backend/Metrics.pm 27 | lib/Zonemaster/Backend/RPCAPI.pm 28 | lib/Zonemaster/Backend/TestAgent.pm 29 | lib/Zonemaster/Backend/Translator.pm 30 | lib/Zonemaster/Backend/Validator.pm 31 | LICENSE 32 | Makefile.PL 33 | MANIFEST This list of files 34 | META.yml 35 | README.md 36 | script/add-batch-job.pl 37 | script/zmb 38 | script/zmtest 39 | script/zonemaster_backend_rpcapi.psgi 40 | script/zonemaster_backend_testagent 41 | share/backend_config.ini 42 | share/cleanup-mysql.sql 43 | share/cleanup-postgres.sql 44 | share/create_db.pl 45 | share/freebsd-pwd.conf 46 | share/GNUmakefile 47 | share/locale/da/LC_MESSAGES/Zonemaster-Backend.mo 48 | share/locale/es/LC_MESSAGES/Zonemaster-Backend.mo 49 | share/locale/fi/LC_MESSAGES/Zonemaster-Backend.mo 50 | share/locale/fr/LC_MESSAGES/Zonemaster-Backend.mo 51 | share/locale/nb/LC_MESSAGES/Zonemaster-Backend.mo 52 | share/locale/sl/LC_MESSAGES/Zonemaster-Backend.mo 53 | share/locale/sv/LC_MESSAGES/Zonemaster-Backend.mo 54 | share/Makefile 55 | share/patch/patch_db_zonemaster_backend_ver_11.1.0.pl 56 | share/patch/patch_db_zonemaster_backend_ver_11.2.0.pl 57 | share/patch/patch_db_zonemaster_backend_ver_9.0.0.pl 58 | share/patch/patch_mysql_db_zonemaster_backend_ver_1.0.3.pl 59 | share/patch/patch_mysql_db_zonemaster_backend_ver_5.0.0.pl 60 | share/patch/patch_mysql_db_zonemaster_backend_ver_5.0.2.pl 61 | share/patch/patch_mysql_db_zonemaster_backend_ver_8.0.0.pl 62 | share/patch/patch_postgresql_db_zonemaster_backend_ver_1.0.3.pl 63 | share/patch/patch_postgresql_db_zonemaster_backend_ver_5.0.0.pl 64 | share/patch/patch_postgresql_db_zonemaster_backend_ver_8.0.0.pl 65 | share/patch/patch_sqlite_db_zonemaster_backend_ver_8.0.0.pl 66 | share/patch/README.txt 67 | share/tmpfiles.conf 68 | share/zm-rpcapi.lsb 69 | share/zm-rpcapi.service 70 | share/zm-testagent.lsb 71 | share/zm-testagent.service 72 | share/zm_rpcapi-bsd 73 | share/zm_testagent-bsd 74 | t/00-load.t 75 | t/batches.t 76 | t/config.t 77 | t/db.t 78 | t/db_ddl.t 79 | t/idn.data 80 | t/idn.t 81 | t/lifecycle.t 82 | t/parameters_validation.t 83 | t/queue.t 84 | t/rpc_validation.t 85 | t/test01.data 86 | t/test01.t 87 | t/test_profile.json 88 | t/test_profile_network_true.json 89 | t/test_profile_no_network.json 90 | t/test_validate_syntax.t 91 | t/TestUtil.pm 92 | t/translator.t 93 | t/validator.t 94 | -------------------------------------------------------------------------------- /share/sv.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: 1.0.0\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2023-05-21 21:29+0000\n" 6 | "PO-Revision-Date: 2023-05-21 21:29+0000\n" 7 | "Last-Translator: mats.dufberg@iis.se\n" 8 | "Language-Team: Zonemaster project\n" 9 | "Language: sv\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | 14 | msgid "Invalid method parameter(s)." 15 | msgstr "Ogiltig metodparameter." 16 | 17 | msgid "Missing property" 18 | msgstr "Attribut saknas" 19 | 20 | msgid "" 21 | "Warning: Zonemaster::LDNS not compiled with IDN support, cannot handle non-" 22 | "ASCII names correctly." 23 | msgstr "" 24 | "Varning: Zonemaster::LDNS är inte kompilerad med IDNA-stöd, så enbart ASCII-" 25 | "namn kan hanteras." 26 | 27 | #. BACKEND_TEST_AGENT:TEST_DIED 28 | msgid "An error occured and Zonemaster could not start or finish the test." 29 | msgstr "" 30 | "Ett fel har inträffat så att Zonemaster inte kunde starta eller slutföra " 31 | "testet." 32 | 33 | #. BACKEND_TEST_AGENT:UNABLE_TO_FINISH_TEST 34 | #, perl-brace-format 35 | msgid "" 36 | "The test took too long to run (the current limit is {max_execution_time} " 37 | "seconds). Maybe there are too many name servers or the name servers are " 38 | "either unreachable or not responsive enough." 39 | msgstr "" 40 | "Det tog för lång tid att köra testet (övre tidsgränsen är f.n. " 41 | "{max_execution_time} sekunder). Kanske har domänen för många namnservrar " 42 | "eller så är namnservrarna oåtkomliga eller så tar namnservrarna för lång på " 43 | "att svara." 44 | 45 | msgid "Invalid digest format" 46 | msgstr "Ogiltigt format på digest-data" 47 | 48 | msgid "Algorithm must be a positive integer" 49 | msgstr "Algoritm måste vara ett positivt heltal" 50 | 51 | msgid "Digest type must be a positive integer" 52 | msgstr "Digest-typ måste vara ett positivt heltal" 53 | 54 | msgid "Keytag must be a positive integer" 55 | msgstr "Keytag måste vara ett positivt heltal" 56 | 57 | msgid "Domain name required" 58 | msgstr "Domännamn är obligatoriskt" 59 | 60 | msgid "The domain name is IDNA invalid" 61 | msgstr "Domännamnet är ogiltigt enligt IDN-standarden" 62 | 63 | msgid "" 64 | "The domain name contains non-ascii characters and IDNA support is not " 65 | "installed" 66 | msgstr "" 67 | "Domännamnet innehåller icke-ASCII-tecken, men stöd för IDN är inte " 68 | "installerat" 69 | 70 | msgid "The domain name character(s) are not supported" 71 | msgstr "Domännamnstecken stöds inte" 72 | 73 | msgid "The domain name contains consecutive dots" 74 | msgstr "Domännamnet innehåller flera punkter i följd" 75 | 76 | msgid "The domain name or label is too long" 77 | msgstr "Domännamnet eller en domännamnsdel är för långt" 78 | 79 | msgid "Invalid language tag format" 80 | msgstr "Ogiltigt format på språkkoden" 81 | 82 | msgid "Unkown language string" 83 | msgstr "Okänd språksträng" 84 | 85 | msgid "Invalid IP address" 86 | msgstr "Ogiltig IP-adress" 87 | 88 | msgid "Invalid profile format" 89 | msgstr "Ogiltigt profilformat" 90 | 91 | msgid "Unknown profile" 92 | msgstr "Okänd profil" 93 | -------------------------------------------------------------------------------- /t/translator.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use v5.16; 3 | use warnings; 4 | use utf8; 5 | use Test::More; 6 | 7 | use Locale::Messages qw( LC_ALL ); 8 | use POSIX qw( setlocale ); 9 | 10 | BEGIN { 11 | # Set correct locale for translation in case not set in calling environment 12 | delete $ENV{"LANG"}; 13 | delete $ENV{"LANGUAGE"}; 14 | delete $ENV{"LC_CTYPE"}; 15 | delete $ENV{"LC_MESSAGES"}; 16 | setlocale( LC_ALL, "C.UTF-8" ); 17 | 18 | use_ok( 'Zonemaster::Backend::Translator' ) 19 | or BAIL_OUT "Cannot continue without translator module"; 20 | } 21 | 22 | my $translator = Zonemaster::Backend::Translator->instance(); 23 | isa_ok $translator, 'Zonemaster::Backend::Translator', "Zonemaster::Backend::Translator->instance()" 24 | or BAIL_OUT "Cannot continue without a translator instance"; 25 | 26 | subtest 'Basic tests' => sub { 27 | isa_ok 'Zonemaster::Backend::Translator', 'Zonemaster::Engine::Translator'; 28 | 29 | my $locale = 'fr_FR.UTF-8'; 30 | ok( $translator->locale( $locale ), "Setting locale to '$locale' works" ); 31 | }; 32 | 33 | subtest 'Testing some translations' => sub { 34 | my $message = { 35 | module => 'System', 36 | testcase => 'Unspecified', 37 | timestamp => '0.000778913497924805', 38 | level => 'INFO', 39 | tag => 'GLOBAL_VERSION', 40 | args => { version => 'v5.0.0' } 41 | }; 42 | my $translation = $translator->translate_tag( $message ); 43 | like $translation, qr/\AUtilisation de la version .* du moteur Zonemaster\.\Z/, 'Translating a GLOBAL_VERSION message tag works'; 44 | }; 45 | 46 | subtest 'Test a message translation from Engine with non-ASCII strings' => sub { 47 | my $message = { 48 | module => 'Basic', 49 | testcase => 'Basic02', 50 | timestamp => '4.085114956678410350', 51 | level => 'ERROR', 52 | tag => 'B02_NS_BROKEN', 53 | args => { ns => 'ns1.example' } 54 | }; 55 | my $translation = $translator->translate_tag( $message ); 56 | 57 | like $translation, qr/\ARéponse cassée du serveur de noms /, 'Translating a B02_NS_BROKEN message works'; 58 | like $translation, qr/cass\x{e9}e/, 'Translation is a string of Unicode codepoints, not bytes'; 59 | }; 60 | 61 | subtest 'Test a Backend-specific translation' => sub { 62 | my $message = { 63 | module => 'Backend', 64 | testcase => '', 65 | timestamp => '59', 66 | level => 'CRITICAL', 67 | tag => 'TEST_DIED', 68 | args => {} 69 | }; 70 | my $translation = $translator->translate_tag( $message ); 71 | 72 | like $translation, qr/\AUne erreur est survenue /, 'Translating a backend-specific TEST_DIED message tag works'; 73 | }; 74 | 75 | subtest 'Test a test case translation with non-ASCII strings' => sub { 76 | my $translation = $translator->test_case_description( 'Consistency01' ); 77 | 78 | like $translation, qr/\ACoh\x{e9}rence du num\x{e9}ro de s\x{e9}rie/, 'Translating Consistency01 gives a string of Unicode codepoints'; 79 | }; 80 | 81 | done_testing; 82 | -------------------------------------------------------------------------------- /share/fi.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: 1.0.0\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2023-05-23 14:04+0000\n" 6 | "PO-Revision-Date: 2023-05-23 14:03+0000\n" 7 | "Last-Translator: mats.dufberg@iis.se\n" 8 | "Language-Team: Zonemaster project\n" 9 | "Language: fi\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "X-Generator: Poedit 3.0\n" 14 | 15 | msgid "Invalid method parameter(s)." 16 | msgstr "Virheelliset asetukset" 17 | 18 | msgid "Missing property" 19 | msgstr "Kenttä puuttuu" 20 | 21 | msgid "" 22 | "Warning: Zonemaster::LDNS not compiled with IDN support, cannot handle non-" 23 | "ASCII names correctly." 24 | msgstr "" 25 | "Varoitus: Zonemaster::LDNS ei ole käännetty IDN tuella, joten se ei pysty " 26 | "käsittelemään ei-ASCII-nimiä oikein." 27 | 28 | #. BACKEND_TEST_AGENT:TEST_DIED 29 | msgid "An error occured and Zonemaster could not start or finish the test." 30 | msgstr "" 31 | "Tapahtui virhe, eikä Zonemaster voinut aloittaa tai suorittaa testiä loppuun." 32 | 33 | #. BACKEND_TEST_AGENT:UNABLE_TO_FINISH_TEST 34 | #, perl-brace-format 35 | msgid "" 36 | "The test took too long to run (the current limit is {max_execution_time} " 37 | "seconds). Maybe there are too many name servers or the name servers are " 38 | "either unreachable or not responsive enough." 39 | msgstr "" 40 | "Testin suorittaminen kesti liian kauan (nykyinen raja on " 41 | "{max_execution_time} sekuntia). Ehkä nimipalvelimia on liian monta, " 42 | "nimipalvelimiin ei saada yhteyttä, tai ne eivät vastaa tarpeeksi nopeasti." 43 | 44 | msgid "Invalid digest format" 45 | msgstr "Virheellinen tiivisteen muoto" 46 | 47 | msgid "Algorithm must be a positive integer" 48 | msgstr "Algoritmin on oltava positiivinen kokonaisluku" 49 | 50 | msgid "Digest type must be a positive integer" 51 | msgstr "Tiivistetyypin (Digest type) on oltava positiivinen kokonaisluku" 52 | 53 | msgid "Keytag must be a positive integer" 54 | msgstr "Tunnisteen (Keytag) on oltava positiivinen kokonaisluku" 55 | 56 | msgid "Domain name required" 57 | msgstr "Vaaditaan verkkotunnus" 58 | 59 | msgid "The domain name is IDNA invalid" 60 | msgstr "Verkkotunnus on IDNA virheellinen" 61 | 62 | msgid "" 63 | "The domain name contains non-ascii characters and IDNA support is not " 64 | "installed" 65 | msgstr "" 66 | "Verkkotunnuksen nimi sisältää muita kuin ascii-merkkejä, eikä IDNA tukea ole " 67 | "asennettu" 68 | 69 | msgid "The domain name character(s) are not supported" 70 | msgstr "Verkkotunnuksen sisältämiä merkkejä ei tueta" 71 | 72 | msgid "The domain name contains consecutive dots" 73 | msgstr "Verkkotunnus sisältää peräkkäisiä pisteitä" 74 | 75 | msgid "The domain name or label is too long" 76 | msgstr "Verkkotunnus tai sen tunnisteet ovat liian pitkiä" 77 | 78 | msgid "Invalid language tag format" 79 | msgstr "Virheellinen kielitunnisteen muoto" 80 | 81 | msgid "Unkown language string" 82 | msgstr "Tuntematon kielitunniste" 83 | 84 | msgid "Invalid IP address" 85 | msgstr "Virheellinen IP-osoite" 86 | 87 | msgid "Invalid profile format" 88 | msgstr "Virheellinen profiilin muoto" 89 | 90 | msgid "Unknown profile" 91 | msgstr "Tuntematon profiili" 92 | -------------------------------------------------------------------------------- /share/patch/patch_mysql_db_zonemaster_backend_ver_8.0.0.pl: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use JSON::PP; 4 | 5 | use DBI qw(:utils); 6 | 7 | use Zonemaster::Backend::Config; 8 | use Zonemaster::Backend::DB::MySQL; 9 | 10 | my $config = Zonemaster::Backend::Config->load_config(); 11 | if ( $config->DB_engine ne 'MySQL' ) { 12 | die "The configuration file does not contain the MySQL backend"; 13 | } 14 | my $db = Zonemaster::Backend::DB::MySQL->from_config( $config ); 15 | my $dbh = $db->dbh; 16 | 17 | 18 | sub patch_db { 19 | # Remove the trigger 20 | $dbh->do( 'DROP TRIGGER IF EXISTS before_insert_test_results' ); 21 | 22 | # Set the "hash_id" field to NOT NULL 23 | eval { 24 | $dbh->do( 'ALTER TABLE test_results MODIFY COLUMN hash_id VARCHAR(16) NOT NULL' ); 25 | }; 26 | print( "Error while changing DB schema: " . $@ ) if ($@); 27 | 28 | # Rename column "params_deterministic_hash" into "fingerprint" 29 | # Since MariaDB 10.5.2 (2020-03-26) 30 | # ALTER TABLE t1 RENAME COLUMN old_col TO new_col; 31 | # Before that we need to use CHANGE COLUMN 32 | eval { 33 | $dbh->do('ALTER TABLE test_results CHANGE COLUMN params_deterministic_hash fingerprint CHARACTER VARYING(32)'); 34 | }; 35 | print( "Error while changing DB schema: " . $@ ) if ($@); 36 | 37 | # Update index 38 | eval { 39 | # retrieve all indexes by key name 40 | my $indexes = $dbh->selectall_hashref( 'SHOW INDEXES FROM test_results', 'Key_name' ); 41 | if ( exists($indexes->{test_results__params_deterministic_hash}) ) { 42 | $dbh->do( "DROP INDEX test_results__params_deterministic_hash ON test_results" ); 43 | } 44 | $dbh->do( "CREATE INDEX test_results__fingerprint ON test_results (fingerprint)" ); 45 | }; 46 | print( "Error while updating the index: " . $@ ) if ($@); 47 | 48 | # Update the "undelegated" column 49 | my $sth1 = $dbh->prepare('SELECT id, params from test_results', undef); 50 | $sth1->execute; 51 | while ( my $row = $sth1->fetchrow_hashref ) { 52 | my $id = $row->{id}; 53 | my $raw_params = decode_json($row->{params}); 54 | my $ds_info_values = scalar grep !/^$/, map { values %$_ } @{$raw_params->{ds_info}}; 55 | my $nameservers_values = scalar grep !/^$/, map { values %$_ } @{$raw_params->{nameservers}}; 56 | my $undelegated = $ds_info_values > 0 || $nameservers_values > 0 || 0; 57 | 58 | $dbh->do('UPDATE test_results SET undelegated = ? where id = ?', undef, $undelegated, $id); 59 | } 60 | 61 | 62 | # remove the "user_info" column from the "users" table 63 | # the IF EXISTS clause is available with MariaDB but not MySQL 64 | eval { 65 | $dbh->do( "ALTER TABLE users DROP COLUMN user_info" ); 66 | }; 67 | print( "Error while dropping the column: " . $@ ) if ($@); 68 | 69 | # remove the "nb_retries" column from the "test_results" table 70 | eval { 71 | $dbh->do( "ALTER TABLE test_results DROP COLUMN nb_retries" ); 72 | }; 73 | print( "Error while dropping the column: " . $@ ) if ($@); 74 | } 75 | 76 | patch_db(); 77 | -------------------------------------------------------------------------------- /share/es.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: 1.0.0\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2023-05-21 21:33+0000\n" 6 | "PO-Revision-Date: 2023-05-21 21:32+0000\n" 7 | "Last-Translator: hsalgado@vulcano.cl\n" 8 | "Language-Team: Zonemaster project\n" 9 | "Language: es\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "X-Generator: Poedit 3.0\n" 14 | 15 | msgid "Invalid method parameter(s)." 16 | msgstr "Parámetro(s) de método inválido." 17 | 18 | msgid "Missing property" 19 | msgstr "Propiedad no incluída" 20 | 21 | msgid "" 22 | "Warning: Zonemaster::LDNS not compiled with IDN support, cannot handle non-" 23 | "ASCII names correctly." 24 | msgstr "" 25 | "Advertencia: Zonemaster::LDNS no fue compilado con soporte IDN, no se puede " 26 | "manejar correctamente los nombres no-ASCII." 27 | 28 | #. BACKEND_TEST_AGENT:TEST_DIED 29 | msgid "An error occured and Zonemaster could not start or finish the test." 30 | msgstr "" 31 | "Ha ocurrido un error y Zonemaster no pudo comenzar o finalizar la prueba." 32 | 33 | #. BACKEND_TEST_AGENT:UNABLE_TO_FINISH_TEST 34 | #, perl-brace-format 35 | msgid "" 36 | "The test took too long to run (the current limit is {max_execution_time} " 37 | "seconds). Maybe there are too many name servers or the name servers are " 38 | "either unreachable or not responsive enough." 39 | msgstr "" 40 | "La prueba tomó demasiado tiempo en terminar (el límite máximo actual es " 41 | "{max_execution_time} segundos). Quizás hay demasiados servidores de nombres, " 42 | "o son inalcanzables, o no lo suficientemente rápidos para responder." 43 | 44 | msgid "Invalid digest format" 45 | msgstr "Formato de resumen (digest) inválido" 46 | 47 | msgid "Algorithm must be a positive integer" 48 | msgstr "El algoritmo debe ser un entero positivo" 49 | 50 | msgid "Digest type must be a positive integer" 51 | msgstr "El tipo de resumen (digest) debe ser un entero positivo" 52 | 53 | msgid "Keytag must be a positive integer" 54 | msgstr "El tag de llave debe ser un entero positivo" 55 | 56 | msgid "Domain name required" 57 | msgstr "Se necesita el nombre de dominio" 58 | 59 | msgid "The domain name is IDNA invalid" 60 | msgstr "El nombre de dominio es inválido según IDNA" 61 | 62 | msgid "" 63 | "The domain name contains non-ascii characters and IDNA support is not " 64 | "installed" 65 | msgstr "" 66 | "El nombre de dominio contiene caracteres no-ascii, y el soporte IDNA no está " 67 | "instalado" 68 | 69 | msgid "The domain name character(s) are not supported" 70 | msgstr "Los caracteres del nombre de dominio no están soportados" 71 | 72 | msgid "The domain name contains consecutive dots" 73 | msgstr "El nombre de dominio contiene puntos consecutivos" 74 | 75 | msgid "The domain name or label is too long" 76 | msgstr "El nombre de dominio o la etiqueta es muy larga" 77 | 78 | msgid "Invalid language tag format" 79 | msgstr "Formato de descriptor de idioma inválido" 80 | 81 | msgid "Unkown language string" 82 | msgstr "Descriptor de idioma desconocido" 83 | 84 | msgid "Invalid IP address" 85 | msgstr "Dirección IP inválida" 86 | 87 | msgid "Invalid profile format" 88 | msgstr "Formato de perfil (profile) inválido" 89 | 90 | msgid "Unknown profile" 91 | msgstr "Perfil desconocido" 92 | -------------------------------------------------------------------------------- /script/zmtest: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | bindir="$(dirname "$0")" 4 | 5 | ZMB="${bindir}/zmb" 6 | JQ="$(which jq)" 7 | 8 | usage () { 9 | status="$1" 10 | message="$2" 11 | [ -n "$message" ] && printf "%s\n" "${message}" >&2 12 | echo "Usage: zmtest [OPTIONS] DOMAIN" >&2 13 | echo >&2 14 | echo "Options:" >&2 15 | echo " -h --help Show usage (this documentation)." >&2 16 | echo " -s URL --server URL Zonemaster Backend to query. Default is http://localhost:5000/" >&2 17 | echo " --noipv4 Run the test with IPv4 disabled." >&2 18 | echo " --noipv6 Run the test with IPv6 disabled." >&2 19 | echo " IPv4 and IPv6 follow the profile setting unless disabled by option." >&2 20 | echo " --lang LANG A language tag. Default is \"en\"." >&2 21 | echo " Valid values are determined by backend_config.ini." >&2 22 | echo " --profile PROFILE The name of a profile. Default is \"default\"." >&2 23 | echo " Valid values are determined by backend_config.ini except that" >&2 24 | echo " \"default\" is always a valid value." >&2 25 | exit "${status}" 26 | } 27 | 28 | error () { 29 | status="$1" 30 | message="$2" 31 | printf "error: %s\n" "${message}" >&2 32 | exit "${status}" 33 | } 34 | 35 | zmb () { 36 | server_url="$1"; shift 37 | output="$("${ZMB}" --server="${server_url}" "$@" 2>&1)" || error 1 "method $1: ${output}" 38 | json="$(printf "%s" "${output}" | "${JQ}" -S . 2>/dev/null)" || error 1 "method $1 did not return valid JSON output: ${output}" 39 | error="$(printf "%s" "${json}" | "${JQ}" -e .error 2>/dev/null)" && error 1 "method $1: ${error}" 40 | printf "%s" "${json}" 41 | } 42 | 43 | [ -n "${JQ}" ] || error 2 "Dependency not found: jq" 44 | 45 | domain="" 46 | server_url="http://localhost:5000/" 47 | ipv4="" 48 | ipv6="" 49 | lang="en" 50 | profile="default" 51 | while [ $# -gt 0 ] ; do 52 | case "$1" in 53 | -h|--help) usage 2; shift 1;; 54 | -s|--server) server_url="$2"; shift 2;; 55 | --noipv4) ipv4='--ipv4 false'; shift 1;; 56 | --noipv6) ipv6='--ipv6 false'; shift 1;; 57 | --lang) lang="$2"; shift 2;; 58 | --profile) profile="$2"; shift 2;; 59 | *) domain="$1" ; shift 1;; 60 | esac 61 | done 62 | [ -n "${domain}" ] || usage 2 "No domain specified" 63 | 64 | # Start test 65 | output="$(zmb "${server_url}" start_domain_test --domain "${domain}" ${ipv4} ${ipv6} --profile "${profile}")" || exit $? 66 | testid="$(printf "%s" "${output}" | "${JQ}" -r .result)" || exit $? 67 | printf "testid: %s\n" "${testid}" >&2 68 | 69 | if echo "${testid}" | grep -qE '[^0-9a-fA-F]' ; then 70 | error 1 "start_domain_test did not return a testid: ${testid}" 71 | fi 72 | 73 | # Wait for test to finish 74 | while true 75 | do 76 | output="$(zmb "${server_url}" test_progress --test-id "${testid}")" || exit $? 77 | progress="$(printf "%s" "${output}" | "${JQ}" -r .result)" || exit $? 78 | printf "\r${progress}%% done" >&2 79 | if [ "${progress}" -eq 100 ] ; then 80 | echo >&2 81 | break 82 | fi 83 | sleep 1 84 | done 85 | 86 | # Get test results 87 | zmb "${server_url}" get_test_results --test-id "${testid}" --lang "${lang}" 88 | -------------------------------------------------------------------------------- /Makefile.PL: -------------------------------------------------------------------------------- 1 | use inc::Module::Install; 2 | use Module::Install::Share; 3 | 4 | name 'Zonemaster-Backend'; 5 | all_from 'lib/Zonemaster/Backend.pm'; 6 | repository 'https://github.com/zonemaster/zonemaster-backend'; 7 | bugtracker 'https://github.com/zonemaster/zonemaster-backend/issues'; 8 | 9 | # "2.1.0" could be declared as "2.001" but not as "2.1" 10 | # (see Zonemaster::LDNS below) 11 | 12 | requires 13 | 'Class::Method::Modifiers' => 1.09, 14 | 'Config::IniFiles' => 0, 15 | 'DBI' => 1.635, 16 | 'Daemon::Control' => 0.001007, 17 | 'File::ShareDir' => 0, 18 | 'File::Slurp' => 0, 19 | 'HTML::Entities' => 0, 20 | 'JSON::PP' => 0, 21 | 'JSON::RPC' => 1.01, 22 | 'JSON::Validator' => 4.00, 23 | 'Locale::TextDomain' => 1.20, 24 | 'Log::Any' => 0, 25 | 'Log::Any::Adapter::Dispatch' => 0, 26 | 'Log::Dispatch' => 0, 27 | 'LWP::UserAgent' => 0, 28 | 'Mojolicious' => 7.28, 29 | 'Moose' => 2.04, 30 | 'Net::IP::XS' => 0, 31 | 'Parallel::ForkManager' => 1.12, 32 | 'Plack::Builder' => 0, 33 | 'Plack::Middleware::ReverseProxy' => 0, 34 | 'Role::Tiny' => 1.001003, 35 | 'Router::Simple::Declare' => 0, 36 | 'Starman' => 0, 37 | 'Try::Tiny' => 0.12, 38 | 'Zonemaster::LDNS' => 5.000001, # v5.0.1 39 | 'Zonemaster::Engine' => 8.001000, # v8.1.0 40 | ; 41 | 42 | test_requires 'DBD::SQLite' => 1.66; 43 | test_requires 'Test::Differences'; 44 | test_requires 'Test::Exception'; 45 | test_requires 'Time::Local' => 1.26; 46 | test_requires 'Test::NoWarnings' => 0; 47 | 48 | recommends 'DBD::mysql'; 49 | recommends 'DBD::Pg'; 50 | recommends 'DBD::SQLite' => 1.66; 51 | 52 | install_share; 53 | 54 | install_script 'zonemaster_backend_rpcapi.psgi'; 55 | install_script 'zonemaster_backend_testagent'; 56 | install_script 'zmtest'; 57 | install_script 'zmb'; 58 | 59 | no_index directory => 'CodeSnippets'; 60 | no_index directory => 'Doc'; 61 | 62 | # Make all platforms include inc/Module/Install/External.pm 63 | requires_external_bin 'find'; 64 | if ($^O eq "freebsd") { 65 | requires_external_bin 'gmake'; 66 | }; 67 | 68 | sub MY::postamble { 69 | my $text; 70 | if ($^O eq "freebsd") { 71 | # Make FreeBSD use gmake for share/Makefile 72 | $text = 'GMAKE ?= "gmake"' . "\n" 73 | . 'pure_all :: share/Makefile' . "\n" 74 | . "\t" . 'cd share && $(GMAKE) all' . "\n"; 75 | } else { 76 | $text = 'pure_all :: share/Makefile' . "\n" 77 | . "\t" . 'cd share && $(MAKE) all' . "\n"; 78 | }; 79 | my $docker = <<'END_DOCKER'; 80 | 81 | docker-build: 82 | docker build --tag zonemaster/backend:local --build-arg version=$(VERSION) . 83 | 84 | docker-tag-version: 85 | docker tag zonemaster/backend:local zonemaster/backend:$(VERSION) 86 | 87 | docker-tag-latest: 88 | docker tag zonemaster/backend:local zonemaster/backend:latest 89 | 90 | END_DOCKER 91 | 92 | return $text . $docker; 93 | }; 94 | 95 | install_share; 96 | 97 | WriteAll; 98 | -------------------------------------------------------------------------------- /share/patch/patch_sqlite_db_zonemaster_backend_ver_8.0.0.pl: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use JSON::PP; 4 | 5 | use DBI qw(:utils); 6 | 7 | use Zonemaster::Backend::Config; 8 | use Zonemaster::Backend::DB::SQLite; 9 | 10 | my $config = Zonemaster::Backend::Config->load_config(); 11 | if ( $config->DB_engine ne 'SQLite' ) { 12 | die "The configuration file does not contain the SQLite backend"; 13 | } 14 | my $db = Zonemaster::Backend::DB::SQLite->from_config( $config ); 15 | my $dbh = $db->dbh; 16 | 17 | 18 | sub patch_db { 19 | 20 | # since we change the default value for a column, the whole table needs to 21 | # be recreated 22 | # 1. rename the "test_results" table to "test_results_old" 23 | # 2. create the new "test_results" table 24 | # 3. populate it with the values from "test_results_old" 25 | # 4. remove old table and indexes 26 | # 5. recreate the indexes 27 | eval { 28 | $dbh->do('ALTER TABLE test_results RENAME TO test_results_old'); 29 | 30 | # create the table 31 | $db->create_schema(); 32 | 33 | # populate it 34 | # - nb_retries is omitted as we remove this column 35 | # - params_deterministic_hash is renamed to fingerprint 36 | $dbh->do(' 37 | INSERT INTO test_results 38 | SELECT id, 39 | hash_id, 40 | domain, 41 | batch_id, 42 | creation_time, 43 | test_start_time, 44 | test_end_time, 45 | priority, 46 | queue, 47 | progress, 48 | params_deterministic_hash, 49 | params, 50 | results, 51 | undelegated 52 | FROM test_results_old 53 | '); 54 | 55 | $dbh->do('DROP TABLE test_results_old'); 56 | 57 | # recreate indexes 58 | $db->create_schema(); 59 | }; 60 | print( "Error while updating the 'test_results' table schema: " . $@ ) if ($@); 61 | 62 | # Update the "undelegated" column 63 | my $sth1 = $dbh->prepare('SELECT id, params from test_results', undef); 64 | $sth1->execute; 65 | while ( my $row = $sth1->fetchrow_hashref ) { 66 | my $id = $row->{id}; 67 | my $raw_params = decode_json($row->{params}); 68 | my $ds_info_values = scalar grep !/^$/, map { values %$_ } @{$raw_params->{ds_info}}; 69 | my $nameservers_values = scalar grep !/^$/, map { values %$_ } @{$raw_params->{nameservers}}; 70 | my $undelegated = $ds_info_values > 0 || $nameservers_values > 0 || 0; 71 | 72 | $dbh->do('UPDATE test_results SET undelegated = ? where id = ?', undef, $undelegated, $id); 73 | } 74 | 75 | 76 | # in order to properly drop a column, the whole table needs to be recreated 77 | # 1. rename the "users" table to "users_old" 78 | # 2. create the new "users" table 79 | # 3. populate it with the values from "users_old" 80 | # 4. remove old table 81 | eval { 82 | $dbh->do('ALTER TABLE users RENAME TO users_old'); 83 | 84 | # create the table 85 | $db->create_schema(); 86 | 87 | # populate it 88 | $dbh->do('INSERT INTO users SELECT id, username, api_key FROM users_old'); 89 | 90 | $dbh->do('DROP TABLE users_old'); 91 | }; 92 | print( "Error while updating the 'users' table schema: " . $@ ) if ($@); 93 | } 94 | 95 | patch_db(); 96 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM zonemaster/cli:local AS build 2 | 3 | ARG version 4 | 5 | USER root 6 | 7 | RUN apk add --no-cache \ 8 | make \ 9 | curl \ 10 | gcc \ 11 | perl-dev \ 12 | musl-dev \ 13 | perl-app-cpanminus 14 | 15 | RUN apk add --no-cache \ 16 | jq \ 17 | perl-class-method-modifiers \ 18 | perl-config-inifiles \ 19 | perl-dbd-sqlite \ 20 | perl-dbi \ 21 | perl-file-share \ 22 | perl-file-slurp \ 23 | perl-html-parser \ 24 | perl-http-parser-xs \ 25 | perl-mojolicious \ 26 | perl-io-stringy \ 27 | perl-log-any \ 28 | perl-log-dispatch \ 29 | perl-moose \ 30 | perl-parallel-forkmanager \ 31 | perl-plack \ 32 | perl-role-tiny \ 33 | perl-test-nowarnings \ 34 | perl-test-differences \ 35 | perl-test-exception \ 36 | perl-try-tiny \ 37 | perl-doc 38 | 39 | # for METRIC 40 | RUN cpanm --notest --no-wget --from https://cpan.metacpan.org/ \ 41 | Net::Statsd 42 | 43 | COPY ./Zonemaster-Backend-${version}.tar.gz ./Zonemaster-Backend-${version}.tar.gz 44 | 45 | RUN cpanm --notest --no-wget --from https://cpan.metacpan.org \ 46 | ./Zonemaster-Backend-${version}.tar.gz 47 | 48 | 49 | FROM zonemaster/cli:local 50 | USER root 51 | 52 | COPY --from=build /usr/local/share/perl5 /usr/local/share/perl5 53 | COPY --from=build /usr/local/bin/ /usr/local/bin/ 54 | COPY --from=build /usr/lib/perl5 /usr/lib/perl5 55 | 56 | RUN apk add --no-cache \ 57 | jq \ 58 | perl-config-inifiles \ 59 | perl-mojolicious \ 60 | perl-moose \ 61 | perl-dbi \ 62 | perl-dbd-sqlite \ 63 | perl-plack \ 64 | perl-parallel-forkmanager 65 | 66 | # Create zonemaster user and group 67 | RUN addgroup -S zonemaster 68 | RUN adduser -S zonemaster -G zonemaster 69 | 70 | RUN cd `perl -MFile::ShareDir=dist_dir -E 'say dist_dir("Zonemaster-Backend")'` && \ 71 | install -v -m 755 -d /etc/zonemaster && \ 72 | install -v -m 775 -g zonemaster -d /var/log/zonemaster && \ 73 | install -v -m 640 -g zonemaster ./backend_config.ini /etc/zonemaster/ 74 | 75 | 76 | # Init SQLite database 77 | RUN install -v -m 755 -o zonemaster -g zonemaster -d /var/lib/zonemaster 78 | USER zonemaster 79 | RUN $(perl -MFile::ShareDir -le 'print File::ShareDir::dist_dir("Zonemaster-Backend")')/create_db.pl 80 | USER zonemaster 81 | COPY zonemaster_launch /usr/local/bin 82 | 83 | USER root 84 | ARG S6_OVERLAY_VERSION=3.2.1.0 85 | 86 | # Install S6 87 | ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz /tmp 88 | RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz 89 | ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-x86_64.tar.xz /tmp 90 | RUN tar -C / -Jxpf /tmp/s6-overlay-x86_64.tar.xz 91 | 92 | 93 | # RPCAPI service 94 | RUN mkdir /etc/s6-overlay/s6-rc.d/rpcapi 95 | RUN echo "longrun" > /etc/s6-overlay/s6-rc.d/rpcapi/type 96 | RUN echo "#!/command/with-contenv sh" > /etc/s6-overlay/s6-rc.d/rpcapi/run 97 | RUN echo "zonemaster_launch rpcapi" >> /etc/s6-overlay/s6-rc.d/rpcapi/run 98 | 99 | # TESTAGENT sevice 100 | RUN mkdir /etc/s6-overlay/s6-rc.d/testagent 101 | RUN echo "longrun" > /etc/s6-overlay/s6-rc.d/testagent/type 102 | RUN echo "#!/command/with-contenv sh" > /etc/s6-overlay/s6-rc.d/testagent/run 103 | RUN echo "zonemaster_launch testagent" >> /etc/s6-overlay/s6-rc.d/testagent/run 104 | 105 | RUN touch /etc/s6-overlay/s6-rc.d/user/contents.d/rpcapi 106 | RUN touch /etc/s6-overlay/s6-rc.d/user/contents.d/testagent 107 | 108 | ENTRYPOINT ["/usr/local/bin/zonemaster_launch"] 109 | -------------------------------------------------------------------------------- /lib/Zonemaster/Backend/Errors.pm: -------------------------------------------------------------------------------- 1 | package Zonemaster::Backend::Error; 2 | use Moose; 3 | use Data::Dumper; 4 | 5 | use overload '""' => \&as_string; 6 | 7 | has 'message' => ( 8 | is => 'ro', 9 | isa => 'Str', 10 | required => 1, 11 | ); 12 | 13 | has 'code' => ( 14 | is => 'ro', 15 | isa => 'Int', 16 | required => 1, 17 | ); 18 | 19 | has 'data' => ( 20 | is => 'ro', 21 | isa => 'Any', 22 | default => undef, 23 | ); 24 | 25 | sub as_hash { 26 | my $self = shift; 27 | my $error = { 28 | code => $self->code, 29 | message => $self->message, 30 | error => ref($self), 31 | }; 32 | $error->{data} = $self->data if defined $self->data; 33 | return $error; 34 | } 35 | 36 | sub as_string { 37 | my $self = shift; 38 | my $str = sprintf "%s (code %d).", $self->message, $self->code; 39 | if (defined $self->data) { 40 | $str .= sprintf " Context: %s", $self->_data_dump; 41 | } 42 | return $str; 43 | } 44 | 45 | sub _data_dump { 46 | my $self = shift; 47 | local $Data::Dumper::Indent = 0; 48 | local $Data::Dumper::Terse = 1; 49 | my $data = Dumper($self->data); 50 | $data =~ s/[\n\r]/ /g; 51 | return $data ; 52 | } 53 | 54 | package Zonemaster::Backend::Error::Internal; 55 | use Moose; 56 | 57 | use overload '""' => \&as_string; 58 | 59 | extends 'Zonemaster::Backend::Error'; 60 | 61 | has '+message' => ( 62 | default => 'Internal server error' 63 | ); 64 | 65 | has '+code' => ( 66 | default => -32603 67 | ); 68 | 69 | has 'reason' => ( 70 | isa => 'Str', 71 | is => 'ro' 72 | ); 73 | 74 | has 'method' => ( 75 | is => 'ro', 76 | isa => 'Str', 77 | builder => '_build_method' 78 | ); 79 | 80 | sub _build_method { 81 | my $s = 0; 82 | while (my @c = caller($s)) { 83 | $s ++; 84 | last if $c[3] eq 'Moose::Object::new'; 85 | } 86 | my @c = caller($s); 87 | if ($c[3] =~ /^(.*)::handle_exception$/ ) { 88 | @c = caller(++$s); 89 | } 90 | 91 | return $c[3]; 92 | } 93 | 94 | sub as_string { 95 | my $self = shift; 96 | 97 | my $reason = $self->reason; 98 | $reason =~ s/\s+/ /g; 99 | $reason =~ s/^\s+|\s+$//g; 100 | 101 | my $str = sprintf "Caught %s in the `%s` method: %s", ref($self), $self->method, $reason; 102 | if (defined $self->data) { 103 | $str .= sprintf " Context: %s", $self->_data_dump; 104 | } 105 | return $str; 106 | } 107 | 108 | around as_hash => sub { 109 | my ($orig, $self) = @_; 110 | 111 | my $hash = $self->$orig; 112 | $hash->{reason} = $self->reason; 113 | $hash->{method} = $self->method; 114 | return $hash; 115 | }; 116 | 117 | 118 | package Zonemaster::Backend::Error::ResourceNotFound; 119 | use Moose; 120 | 121 | extends 'Zonemaster::Backend::Error'; 122 | 123 | has '+message' => ( 124 | default => 'Resource not found' 125 | ); 126 | 127 | has '+code' => ( 128 | default => -32000 129 | ); 130 | 131 | package Zonemaster::Backend::Error::PermissionDenied; 132 | use Moose; 133 | 134 | extends 'Zonemaster::Backend::Error'; 135 | 136 | has '+message' => ( 137 | default => 'Permission denied' 138 | ); 139 | 140 | has '+code' => ( 141 | default => -32001 142 | ); 143 | 144 | package Zonemaster::Backend::Error::Conflict; 145 | use Moose; 146 | 147 | extends 'Zonemaster::Backend::Error'; 148 | 149 | has '+message' => ( 150 | default => 'Conflicting resource' 151 | ); 152 | 153 | has '+code' => ( 154 | default => -32002 155 | ); 156 | 157 | package Zonemaster::Backend::Error::JsonError; 158 | use Moose; 159 | 160 | extends 'Zonemaster::Backend::Error::Internal'; 161 | 162 | 1; 163 | -------------------------------------------------------------------------------- /t/idn.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use 5.14.2; 4 | 5 | use Data::Dumper; 6 | use File::Temp qw[tempdir]; 7 | use Test::Exception; 8 | use Test::More; # see done_testing() 9 | use utf8; 10 | 11 | my $t_path; 12 | BEGIN { 13 | use File::Spec::Functions qw( rel2abs ); 14 | use File::Basename qw( dirname ); 15 | $t_path = dirname( rel2abs( $0 ) ); 16 | } 17 | use lib $t_path; 18 | use TestUtil qw( TestAgent ); 19 | 20 | use Zonemaster::Backend::Config; 21 | 22 | my $db_backend = TestUtil::db_backend(); 23 | 24 | my $datafile = "$t_path/idn.data"; 25 | TestUtil::restore_datafile( $datafile ); 26 | 27 | my $tempdir = tempdir( CLEANUP => 1 ); 28 | 29 | my $configuration = <<"EOF"; 30 | [DB] 31 | engine = $db_backend 32 | 33 | [MYSQL] 34 | host = localhost 35 | user = zonemaster_test 36 | password = zonemaster 37 | database = zonemaster_test 38 | 39 | [POSTGRESQL] 40 | host = localhost 41 | user = zonemaster_test 42 | password = zonemaster 43 | database = zonemaster_test 44 | 45 | [SQLITE] 46 | database_file = $tempdir/zonemaster.sqlite 47 | 48 | [LANGUAGE] 49 | locale = en_US 50 | EOF 51 | 52 | if ( $ENV{ZONEMASTER_RECORD} ) { 53 | $configuration .= <<"EOF"; 54 | [PUBLIC PROFILES] 55 | test_profile=$t_path/test_profile_network_true.json 56 | default=$t_path/test_profile_network_true.json 57 | EOF 58 | } else { 59 | $configuration .= <<"EOF"; 60 | [PUBLIC PROFILES] 61 | test_profile=$t_path/test_profile_no_network.json 62 | default=$t_path/test_profile_no_network.json 63 | EOF 64 | } 65 | 66 | my $config = Zonemaster::Backend::Config->parse( $configuration ); 67 | 68 | my $db = TestUtil::init_db( $config ); 69 | my $agent = TestUtil::create_testagent( $config ); 70 | 71 | # define the default properties for the tests 72 | my $params = { 73 | client_id => 'Unit Test', 74 | client_version => '1.0', 75 | domain => 'café.example', 76 | ipv4 => JSON::PP::true, 77 | ipv6 => JSON::PP::true, 78 | profile => 'default', 79 | }; 80 | 81 | my $test_id; 82 | 83 | subtest 'test IDN domain' => sub { 84 | $test_id = $db->create_new_test( $params->{domain}, $params, 10 ); 85 | 86 | my $res = $db->get_test_params( $test_id ); 87 | note Dumper($res); 88 | is( $res->{domain}, $params->{domain}, 'Retrieve the correct "domain" value' ); 89 | }; 90 | 91 | # run the test 92 | $db->claim_test( $test_id ) 93 | or BAIL_OUT( "test needs to be claimed before calling run()" ); 94 | $agent->run( $test_id ); # blocking call 95 | 96 | subtest 'test get_test_results' => sub { 97 | my $res = $db->test_results( $test_id ); 98 | is( $res->{params}->{domain}, $params->{domain}, 'Retrieve the correct domain name' ); 99 | }; 100 | 101 | 102 | subtest 'test IDN nameserver' => sub { 103 | $params->{nameservers} = [ { ns => "anøthær.example" } ]; 104 | 105 | $test_id = $db->create_new_test( $params->{domain}, $params, 10 ); 106 | 107 | subtest 'get_test_params' => sub { 108 | my $res = $db->get_test_params( $test_id ); 109 | note Dumper($res); 110 | is_deeply( $res->{nameservers}, $params->{nameservers}, 'Retrieve the correct "nameservers" value' ); 111 | }; 112 | 113 | # run the test 114 | $db->claim_test( $test_id ) 115 | or BAIL_OUT( "test needs to be claimed before calling run()" ); 116 | $agent->run( $test_id ); # blocking call 117 | 118 | subtest 'test_results' => sub { 119 | my $res = $db->test_results( $test_id ); 120 | is_deeply( $res->{params}->{nameservers}, $params->{nameservers}, 'Retrieve the correct nameservers parameters' ); 121 | }; 122 | }; 123 | 124 | TestUtil::save_datafile( $datafile ); 125 | 126 | done_testing(); 127 | -------------------------------------------------------------------------------- /lib/Zonemaster/Backend/Log.pm: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | package Zonemaster::Backend::Log; 5 | 6 | use English qw( $PID ); 7 | use POSIX; 8 | use JSON::PP; 9 | use IO::Handle; 10 | use Log::Any::Adapter::Util (); 11 | use Carp; 12 | use Data::Dumper; 13 | 14 | use base qw(Log::Any::Adapter::Base); 15 | 16 | 17 | my $default_level = Log::Any::Adapter::Util::numeric_level('info'); 18 | 19 | sub init { 20 | my ($self) = @_; 21 | 22 | if ( defined $self->{log_level} && $self->{log_level} =~ /\D/ ) { 23 | $self->{log_level} = lc $self->{log_level}; 24 | my $numeric_level = Log::Any::Adapter::Util::numeric_level( $self->{log_level} ); 25 | if ( !defined($numeric_level) ) { 26 | croak "Error: Unrecognized log level " . $self->{log_level} . "\n"; 27 | } 28 | $self->{log_level} = $numeric_level; 29 | } 30 | 31 | $self->{log_level} //= $default_level; 32 | 33 | my $fd; 34 | if ( !exists $self->{file} || $self->{file} eq '-') { 35 | if ( $self->{stderr} ) { 36 | $fd = fileno(STDERR); 37 | } else { 38 | $fd = fileno(STDOUT); 39 | } 40 | } else { 41 | open( $fd, '>>', $self->{file} ) or croak "Can't open log file: $!"; 42 | } 43 | 44 | $self->{handle} = IO::Handle->new_from_fd( $fd, "w" ) or croak "Can't fdopen file: $!"; 45 | $self->{handle}->autoflush(1); 46 | 47 | if ( !exists $self->{formatter} ) { 48 | if ( $self->{json} ) { 49 | $self->{formatter} = \&format_json; 50 | } else { 51 | $self->{formatter} = \&format_text; 52 | } 53 | } 54 | } 55 | 56 | sub format_text { 57 | my ($self, $log_params) = @_; 58 | my $msg; 59 | $msg .= sprintf "%s ", $log_params->{timestamp}; 60 | delete $log_params->{timestamp}; 61 | $msg .= sprintf( 62 | "[%d] [%s] [%s] %s", 63 | delete $log_params->{pid}, 64 | uc delete $log_params->{level}, 65 | delete $log_params->{category}, 66 | delete $log_params->{message} 67 | ); 68 | 69 | if ( %$log_params ) { 70 | local $Data::Dumper::Indent = 0; 71 | local $Data::Dumper::Terse = 1; 72 | my $data = Dumper($log_params); 73 | 74 | $msg .= " Extra parameters: $data"; 75 | } 76 | 77 | return $msg 78 | } 79 | 80 | sub format_json { 81 | my ($self, $log_params) = @_; 82 | 83 | my $js = JSON::PP->new; 84 | $js->canonical( 1 ); 85 | 86 | return $js->encode( $log_params ); 87 | } 88 | 89 | 90 | sub structured { 91 | my ($self, $level, $category, $string, @items) = @_; 92 | 93 | my $log_level = Log::Any::Adapter::Util::numeric_level($level); 94 | 95 | return if $log_level > $self->{log_level}; 96 | 97 | my %log_params = ( 98 | timestamp => strftime( "%FT%TZ", gmtime ), 99 | level => $level, 100 | category => $category, 101 | message => $string, 102 | pid => $PID, 103 | ); 104 | 105 | for my $item ( @items ) { 106 | if (ref($item) eq 'HASH') { 107 | for my $key (keys %$item) { 108 | $log_params{$key} = $item->{$key}; 109 | } 110 | } 111 | } 112 | 113 | my $msg = $self->{formatter}->($self, \%log_params); 114 | $self->{handle}->print($msg . "\n"); 115 | } 116 | 117 | # From Log::Any::Adapter::File 118 | foreach my $method ( Log::Any::Adapter::Util::detection_methods() ) { 119 | no strict 'refs'; 120 | my $base = substr($method,3); 121 | my $method_level = Log::Any::Adapter::Util::numeric_level( $base ); 122 | *{$method} = sub { 123 | return !!( $method_level <= $_[0]->{log_level} ); 124 | }; 125 | } 126 | 127 | 1; 128 | -------------------------------------------------------------------------------- /docs/Architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | The Zonemaster *Backend* is a system for performing domain health checks and 4 | keeping records of performed domain health checks. 5 | 6 | A Zonemaster *Backend* system consists of at least three components: a 7 | single *Database*, a single *Test Agent* and one or more *RPC API daemons*. 8 | 9 | 10 | ## Components 11 | 12 | ### Database 13 | 14 | The *Database* stores health check requests and results. The *Backend* 15 | architecture is oriented around a single central *Database*. 16 | 17 | All times in the database are stored in UTC. 18 | 19 | 20 | ### Test Agent 21 | 22 | A Zonemaster *Test Agent* is a daemon that picks up *test* requests from the 23 | *Database*, runs them using the *Zonemaster Engine* library, and records the results back 24 | to the *Database*. A single *Test Agent* may handle several requests concurrently. 25 | The *Backend* architecture supports a single *Test Agent* daemon interacting with a single *Database*. 26 | 27 | > 28 | > TODO: List all files these processes read and write. 29 | > 30 | > TODO: List everything these processes open network connections to. 31 | > 32 | > TODO: Describe in which order *test* are processed. 33 | > 34 | > TODO: Describe how concurrency, parallelism and synchronization works within a single *Test Agent*. 35 | > 36 | > TODO: Describe how synchronization works among parallel *Test Agents*. 37 | > 38 | 39 | 40 | ### Web backend 41 | 42 | A Zonemaster *Web backend* is a daemon providing a JSON-RPC interface for 43 | recording *test* requests in the *Database* and fetching *test* results from the 44 | *Database*. The *Backend* architecture supports multiple *RPC API daemons* 45 | interacting with the same *Database*. 46 | 47 | This only needs to be run as root in order to make sure the log file 48 | can be opened. The `starman` process will change to the `www-data` user as 49 | soon as it can, and all of the real work will be done as that user. 50 | 51 | > 52 | > TODO: List all ports these processes listen to. 53 | > 54 | > TODO: List all files these processes read and write. 55 | > 56 | > TODO: List everything these processes open network connections to. 57 | > 58 | 59 | 60 | ## Glossary 61 | 62 | ### Test 63 | 64 | ### Batch 65 | 66 | ### Test result 67 | 68 | ### Test module 69 | 70 | ### Message 71 | 72 | ### Profile 73 | 74 | A profile is a configuration for Zonemaster Engine; see the [profiles 75 | overview] for context. 76 | Zonemaster Backend allows administrators to [configure the set of 77 | available profiles]. 78 | 79 | Each available profile has a [profile name]. 80 | A profile named `default` is always available. 81 | Each available profile is based on the [Zonemaster Engine default profile]. 82 | Each one (with the possible exception of `default`) has a [profile file] 83 | with profile data overriding the Zonemaster Engine default profile. 84 | 85 | The [RPC-API] contains several methods that accept profile name arguments. 86 | 87 | 88 | ### Engine 89 | 90 | The Zonemaster *Engine* is a library for performing *tests*. It's hosted in [its 91 | own repository](https://github.com/zonemaster/zonemaster-engine/). 92 | 93 | -------- 94 | [Configure the set of available profiles]: https://github.com/zonemaster/zonemaster/blob/develop/docs/public/configuration/backend.md#profiles-section 95 | [Profile file]: https://metacpan.org/pod/Zonemaster::Engine::Config#PROFILE-DATA 96 | [Profile name]: https://github.com/zonemaster/zonemaster/blob/master/docs/public/using/backend/rpcapi-reference.md#profile-name 97 | [Profiles overview]: https://github.com/zonemaster/zonemaster/blob/master/docs/internal/design/Profiles.md 98 | [RPC-API]: https://github.com/zonemaster/zonemaster/blob/master/docs/public/using/backend/rpcapi-reference.md 99 | [Zonemaster Engine default profile]: https://metacpan.org/pod/Zonemaster::Engine::Config#DESCRIPTION 100 | -------------------------------------------------------------------------------- /lib/Zonemaster/Backend/Config/DCPlugin.pm: -------------------------------------------------------------------------------- 1 | package Zonemaster::Backend::Config::DCPlugin; 2 | 3 | use strict; 4 | use warnings; 5 | 6 | =head1 NAME 7 | 8 | Zonemaster::Backend::Config::DCPlugin - Daemon::Control plugin that 9 | loads the backend configuration. 10 | 11 | =head1 SYNOPSIS 12 | 13 | Provides validated and sanity-checked backend configuration through the 14 | L, L and L properties. 15 | 16 | my $daemon = Daemon::Control 17 | ->with_plugins('+Zonemaster::Backend::Config::DCPlugin') 18 | ->new({ 19 | program => sub { 20 | my $self = shift; 21 | 22 | $self->init_backend_config(); 23 | 24 | my $config = $self->config; 25 | my $db = $self->db; 26 | my $pm = $self->pm; 27 | ... 28 | }, 29 | }); 30 | 31 | No configuration is loaded automatically. 32 | Instead a successful call to init_backend_config() is required. 33 | 34 | On restart the reload_config() method is called automatically. 35 | 36 | =head1 AUTHOR 37 | 38 | Mattias P, C<< >> 39 | 40 | =cut 41 | 42 | use parent 'Daemon::Control'; 43 | use Role::Tiny; # Must be loaded before Class::Method::Modifiers or it will warn 44 | 45 | use Carp; 46 | use Class::Method::Modifiers; 47 | use Hash::Util::FieldHash qw( fieldhash ); 48 | use Log::Any qw( $log ); 49 | use Zonemaster::Backend::Config; 50 | 51 | before do_restart => \&init_backend_config; 52 | 53 | # Using the inside-out technique to avoid collisions with other instance 54 | # variables. 55 | fieldhash my %config; 56 | fieldhash my %db; 57 | fieldhash my %pm; 58 | 59 | =head1 INSTANCE METHODS 60 | 61 | =head2 init_backend_config 62 | 63 | Initializes or reinitializes the L, L and L properties. 64 | 65 | A candidate for the L property is either accepted as an argument, 66 | or L is invoked to provide one. 67 | Candidates for the L and L properties are constructed according to the 68 | L candidate. 69 | 70 | Returns 1 if all candidates are successfully constructed. 71 | In this case all properties are assigned their respective candidate values. 72 | 73 | Returns 0 if the construction of any one of the candidates fails. 74 | Details about the construction failure are logged. 75 | None of the properties are updated. 76 | 77 | =cut 78 | 79 | sub init_backend_config { 80 | my ( $self, $config_candidate ) = @_; 81 | 82 | eval { 83 | $config_candidate //= Zonemaster::Backend::Config->load_config(); 84 | my $db_candidate = $config_candidate->new_DB(); 85 | my $pm_candidate = $config_candidate->new_PM(); 86 | 87 | $config{$self} = $config_candidate; 88 | $db{$self} = $db_candidate; 89 | $pm{$self} = $pm_candidate; 90 | }; 91 | 92 | if ( $@ ) { 93 | $log->warn( "Failed to load the configuration: $@" ); 94 | return 0; 95 | } 96 | 97 | return 1; 98 | } 99 | 100 | =head1 PROPERTIES 101 | 102 | =head2 config 103 | 104 | Getter for the currently loaded configuration. 105 | 106 | Throws an exception if no successful call to init_backend_config() has been 107 | made prior to this call. 108 | 109 | =cut 110 | 111 | sub config { 112 | my ( $self ) = @_; 113 | 114 | exists $config{$self} or croak "Not initialized"; 115 | 116 | return $config{$self}; 117 | } 118 | 119 | =head2 db 120 | 121 | Getter for a database adapter constructed according to the current 122 | configuration. 123 | 124 | Throws an exception if no successful call to init_backend_config() has been 125 | made prior to this call. 126 | 127 | =cut 128 | 129 | sub db { 130 | my ( $self ) = @_; 131 | 132 | exists $db{$self} or croak "Not initialized"; 133 | 134 | return $db{$self}; 135 | } 136 | 137 | =head2 pm 138 | 139 | Getter for a processing manager constructed according to the current 140 | configuration. 141 | 142 | Throws an exception if no successful call to init_backend_config() has been 143 | made prior to this call. 144 | 145 | =cut 146 | 147 | sub pm { 148 | my ( $self ) = @_; 149 | 150 | exists $pm{$self} or croak "Not initialized"; 151 | 152 | return $pm{$self}; 153 | } 154 | 155 | 1; 156 | -------------------------------------------------------------------------------- /share/patch/patch_postgresql_db_zonemaster_backend_ver_8.0.0.pl: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use JSON::PP; 4 | use Encode; 5 | 6 | use DBI qw(:utils); 7 | 8 | use Zonemaster::Backend::Config; 9 | use Zonemaster::Backend::DB::PostgreSQL; 10 | 11 | my $config = Zonemaster::Backend::Config->load_config(); 12 | if ( $config->DB_engine ne 'PostgreSQL' ) { 13 | die "The configuration file does not contain the PostgreSQL backend"; 14 | } 15 | my $db = Zonemaster::Backend::DB::PostgreSQL->from_config( $config ); 16 | my $dbh = $db->dbh; 17 | 18 | 19 | sub patch_db { 20 | # Drop default value for the "hash_id" field 21 | $dbh->do( 'ALTER TABLE test_results ALTER COLUMN hash_id DROP DEFAULT' ); 22 | 23 | # Rename column "params_deterministic_hash" into "fingerprint" 24 | eval { 25 | $dbh->do( 'ALTER TABLE test_results RENAME COLUMN params_deterministic_hash TO fingerprint' ); 26 | }; 27 | print( "Error while changing DB schema: " . $@ ) if ($@); 28 | 29 | # Update index 30 | eval { 31 | $dbh->do( "DROP INDEX IF EXISTS test_results__params_deterministic_hash" ); 32 | $dbh->do( "CREATE INDEX test_results__fingerprint ON test_results (fingerprint)" ); 33 | }; 34 | print( "Error while updating the index: " . $@ ) if ($@); 35 | 36 | # test_start_time and test_end_time default to NULL 37 | eval { 38 | $dbh->do('ALTER TABLE test_results ALTER COLUMN test_start_time SET DEFAULT NULL'); 39 | $dbh->do('ALTER TABLE test_results ALTER COLUMN test_end_time SET DEFAULT NULL'); 40 | }; 41 | print( "Error while changing DB schema: " . $@ ) if ($@); 42 | 43 | 44 | # Add missing "domain" and "undelegated" columns 45 | eval { 46 | $dbh->do( "ALTER TABLE test_results ADD COLUMN domain VARCHAR(255) NOT NULL DEFAULT ''" ); 47 | $dbh->do( 'ALTER TABLE test_results ADD COLUMN undelegated integer NOT NULL DEFAULT 0' ); 48 | }; 49 | print( "Error while changing DB schema: " . $@ ) if ($@); 50 | 51 | # Update index 52 | eval { 53 | $dbh->do( "DROP INDEX IF EXISTS test_results__domain_undelegated" ); 54 | $dbh->do( "CREATE INDEX test_results__domain_undelegated ON test_results (domain, undelegated)" ); 55 | }; 56 | print( "Error while updating the index: " . $@ ) if ($@); 57 | 58 | # New index 59 | eval { 60 | $dbh->do( 'CREATE INDEX IF NOT EXISTS test_results__progress_priority_id ON test_results (progress, priority DESC, id) WHERE (progress = 0)' ); 61 | }; 62 | print( "Error while creating the index: " . $@ ) if ($@); 63 | 64 | # Update the "domain" column 65 | $dbh->do( "UPDATE test_results SET domain = (params->>'domain')" ); 66 | # remove default value to "domain" column 67 | $dbh->do( "ALTER TABLE test_results ALTER COLUMN domain DROP DEFAULT" ); 68 | 69 | # Update the "undelegated" column 70 | my $sth1 = $dbh->prepare('SELECT id, params from test_results', undef); 71 | $sth1->execute; 72 | while ( my $row = $sth1->fetchrow_hashref ) { 73 | my $id = $row->{id}; 74 | my $raw_params; 75 | 76 | if (utf8::is_utf8($row->{params}) ) { 77 | $raw_params = decode_json( encode_utf8 ( $row->{params} ) ); 78 | } else { 79 | $raw_params = decode_json( $row->{params} ); 80 | } 81 | 82 | my $ds_info_values = scalar grep !/^$/, map { values %$_ } @{$raw_params->{ds_info}}; 83 | my $nameservers_values = scalar grep !/^$/, map { values %$_ } @{$raw_params->{nameservers}}; 84 | my $undelegated = $ds_info_values > 0 || $nameservers_values > 0 || 0; 85 | 86 | $dbh->do('UPDATE test_results SET undelegated = ? where id = ?', undef, $undelegated, $id); 87 | } 88 | 89 | # add "username" and "api_key" columns to the "users" table 90 | eval { 91 | $dbh->do( 'ALTER TABLE users ADD COLUMN username VARCHAR(128)' ); 92 | $dbh->do( 'ALTER TABLE users ADD COLUMN api_key VARCHAR(512)' ); 93 | }; 94 | print( "Error while changing DB schema: " . $@ ) if ($@); 95 | 96 | # update the columns 97 | eval { 98 | $dbh->do( "UPDATE users SET username = (user_info->>'username'), api_key = (user_info->>'api_key')" ); 99 | }; 100 | print( "Error while updating the users table: " . $@ ) if ($@); 101 | 102 | # remove the "user_info" column from the "users" table 103 | $dbh->do( "ALTER TABLE users DROP COLUMN IF EXISTS user_info" ); 104 | 105 | # remove the "nb_retries" column from the "test_results" table 106 | $dbh->do( "ALTER TABLE test_results DROP COLUMN IF EXISTS nb_retries" ); 107 | } 108 | 109 | patch_db(); 110 | -------------------------------------------------------------------------------- /t/TestUtil.pm: -------------------------------------------------------------------------------- 1 | package TestUtil; 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Test::More; 7 | 8 | use Zonemaster::Engine; 9 | use Zonemaster::Backend::Config; 10 | 11 | =head1 NAME 12 | 13 | TestUtil - a set of methods to ease Zonemaster::Backend unit testing 14 | 15 | =head1 SYNOPSIS 16 | 17 | Because this package lies in the testing folder C and that folder is 18 | unknown to the include path @INC, it can be including using the following code: 19 | 20 | my $t_path; 21 | BEGIN { 22 | use File::Spec::Functions qw( rel2abs ); 23 | use File::Basename qw( dirname ); 24 | $t_path = dirname( rel2abs( $0 ) ); 25 | } 26 | use lib $t_path; 27 | use TestUtil; 28 | 29 | Explicitely load any dependencies to Zonemaster::Backend::RPCAPI or 30 | Zonemaster::Backend::TestAgent modules with 31 | 32 | use TestUtil qw( RPCAPI TestAgent ); 33 | 34 | =head1 ENVIRONMENT 35 | 36 | =head2 TARGET 37 | 38 | Set the database to use. 39 | Can be C, C or C. 40 | Default to C. 41 | 42 | =head2 ZONEMASTER_RECORD 43 | 44 | If set, the data from the test is recorded to a file. Otherwise the data is 45 | loaded from a file. 46 | 47 | =cut 48 | 49 | # Use the TARGET environment variable to set the database to use 50 | # default to SQLite 51 | my $db_backend = Zonemaster::Backend::Config->check_db( $ENV{TARGET} || 'SQLite' ); 52 | note "database: $db_backend"; 53 | 54 | sub import { 55 | my ( $class, @args ) = @_; 56 | if ( grep { $_ eq 'RPCAPI' } @args ) { 57 | require Zonemaster::Backend::RPCAPI; 58 | Zonemaster::Backend::RPCAPI->import(); 59 | } 60 | if ( grep { $_ eq 'TestAgent' } @args ) { 61 | require Zonemaster::Backend::TestAgent; 62 | Zonemaster::Backend::TestAgent->import(); 63 | } 64 | } 65 | 66 | sub db_backend { 67 | return $db_backend; 68 | } 69 | 70 | sub restore_datafile { 71 | my ( $datafile ) = @_; 72 | 73 | if ( not $ENV{ZONEMASTER_RECORD} ) { 74 | die q{Stored data file missing} if not -r $datafile; 75 | Zonemaster::Engine->preload_cache( $datafile ); 76 | Zonemaster::Engine->profile->set( q{no_network}, 1 ); 77 | } else { 78 | diag "recording"; 79 | } 80 | } 81 | 82 | sub save_datafile { 83 | my ( $datafile ) = @_; 84 | 85 | if ( $ENV{ZONEMASTER_RECORD} ) { 86 | Zonemaster::Engine->save_cache( $datafile ); 87 | } 88 | } 89 | 90 | sub prepare_db { 91 | my ( $db ) = @_; 92 | 93 | $db->drop_tables(); 94 | $db->create_schema(); 95 | } 96 | 97 | sub init_db { 98 | my ( $config ) = @_; 99 | 100 | my $dbclass = Zonemaster::Backend::DB->get_db_class( $db_backend ); 101 | my $db = $dbclass->from_config( $config ); 102 | 103 | prepare_db( $db ); 104 | 105 | return $db; 106 | } 107 | 108 | sub create_rpcapi { 109 | my ( $config ) = @_; 110 | 111 | my $rpcapi; 112 | eval { 113 | $rpcapi = Zonemaster::Backend::RPCAPI->new( 114 | { 115 | dbtype => $db_backend, 116 | config => $config, 117 | } 118 | ); 119 | }; 120 | if ( $@ ) { 121 | diag explain( $@ ); 122 | BAIL_OUT( 'Could not connect to database' ); 123 | } 124 | 125 | if ( not $rpcapi->isa('Zonemaster::Backend::RPCAPI' ) ) { 126 | BAIL_OUT( 'Not a Zonemaster::Backend::RPCAPI object' ); 127 | } 128 | 129 | prepare_db( $rpcapi->{db} ); 130 | 131 | return $rpcapi; 132 | } 133 | 134 | sub create_testagent { 135 | my ( $config ) = @_; 136 | 137 | my $agent = Zonemaster::Backend::TestAgent->new( 138 | { 139 | dbtype => "$db_backend", 140 | config => $config 141 | } 142 | ); 143 | 144 | if ( not $agent->isa('Zonemaster::Backend::TestAgent' ) ) { 145 | BAIL_OUT( 'Not a Zonemaster::Backend::TestAgent object' ); 146 | } 147 | 148 | return $agent; 149 | } 150 | 151 | =head1 METHODS 152 | 153 | =over 154 | 155 | =item db_backend() 156 | 157 | Returns the name of the currently used database engine. This value is set via 158 | the TARGET environment variable. 159 | 160 | =item restore_datafile($datafile) 161 | 162 | If the ZONEMASTER_RECORD environment variable is unset, the data from 163 | C<$datafile> is used for all the current tests. 164 | 165 | =item save_datafile($datafile) 166 | 167 | If the ZONEMASTER_RECORD environment variable is set, the data from the current 168 | tests are stored to C<$datafile>. 169 | 170 | =item prepare_db($db) 171 | 172 | Recreate all tables anew for the associated C<$db>. 173 | 174 | =item init_db($config) 175 | 176 | Returns a new Zonemaster::Backend::DB object using the provided C<$config> 177 | file. 178 | 179 | Database tables are dropped and created anew. 180 | 181 | =item create_rpcapi($config) 182 | 183 | Returns a new Zonemaster::Backend::RPCAPI object using the provided C<$config> 184 | file. 185 | 186 | Database tables are dropped and created anew. 187 | 188 | =item create_testagent($config) 189 | 190 | Returns a new Zonemaster::Backend::TestAgent object using the provided 191 | C<$config> file. 192 | 193 | =back 194 | 195 | =cut 196 | 197 | 1; 198 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | on: 5 | push: 6 | branches: 7 | - develop 8 | - master 9 | - 'release/**' 10 | pull_request: 11 | branches: 12 | - develop 13 | - master 14 | - 'release/**' 15 | 16 | env: 17 | ZONEMASTER_RECORD: 0 18 | compatibility: develop 19 | # compatibility: latest 20 | 21 | jobs: 22 | run-tests: 23 | strategy: 24 | matrix: 25 | db: [sqlite, mysql, postgresql] 26 | perl: ['5.40'] 27 | include: 28 | - db: sqlite 29 | perl: '5.36' 30 | - db: sqlite 31 | perl: '5.26' 32 | 33 | runs-on: ubuntu-22.04 34 | 35 | steps: 36 | - uses: actions/checkout@v2 37 | 38 | - uses: shogo82148/actions-setup-perl@v1 39 | with: 40 | perl-version: ${{ matrix.perl }} 41 | 42 | - name: Install binary dependencies 43 | run: | 44 | # * These were taken from the installation instruction. 45 | # * Gettext was added so we can run cpanm . on the Engine sources. 46 | # * The Perl modules were left out because I couldn't get all of them 47 | # to work with custom Perl versions. 48 | # * Cpanminus was left out because actions-setup-perl installs it. 49 | sudo apt-get install -y \ 50 | autoconf \ 51 | automake \ 52 | build-essential \ 53 | gettext \ 54 | libidn2-dev \ 55 | libssl-dev \ 56 | libtool \ 57 | m4 \ 58 | 59 | - name: "Install development versions of Zonemaster::LDNS and Zonemaster::Engine" 60 | if: ${{ env.compatibility == 'develop' }} 61 | run: | 62 | cpanm --sudo --notest \ 63 | Devel::CheckLib \ 64 | Module::Install \ 65 | ExtUtils::PkgConfig \ 66 | Module::Install::XSUtil 67 | git clone --branch=develop --depth=1 \ 68 | https://github.com/zonemaster/zonemaster-ldns.git 69 | git clone --branch=develop --depth=1 \ 70 | https://github.com/zonemaster/zonemaster-engine.git 71 | ( cd zonemaster-ldns ; perl Makefile.PL ) # Generate MYMETA.yml to appease cpanm 72 | ( cd zonemaster-engine ; perl Makefile.PL ) # Generate MYMETA.yml to appease cpanm 73 | make -C zonemaster-engine # Generate MO files so they get installed 74 | cpanm --sudo --notest ./zonemaster-ldns ./zonemaster-engine 75 | rm -rf zonemaster-ldns zonemaster-engine 76 | 77 | # Installing Zonemaster::Engine requires root privileges, because of a 78 | # bug in Mail::SPF preventing normal installation with cpanm as 79 | # non-root user (see link below [1]). 80 | # 81 | # The alternative, if one still wishes to install Zonemaster::Engine 82 | # as non-root user, is to install Mail::SPF first with a command like: 83 | # 84 | # % cpanm --notest \ 85 | # --install-args="--install_path sbin=$HOME/.local/sbin" \ 86 | # Mail::SPF 87 | # 88 | # For the sake of consistency, other Perl packages installed from CPAN 89 | # are also installed as root. 90 | # 91 | # [1]: https://rt.cpan.org/Public/Bug/Display.html?id=34768 92 | - name: Install remaining dependencies 93 | run: cpanm --sudo --notest --installdeps . 94 | 95 | - name: Install Zonemaster::Backend 96 | run: | 97 | perl Makefile.PL 98 | make # Generate MO files so they get installed 99 | cpanm --sudo --notest --verbose . 100 | 101 | - name: Set up database 102 | if: ${{ matrix.db != 'sqlite' }} 103 | run: | 104 | case "${{ matrix.db }}" in 105 | mariadb) 106 | cpanm --sudo --notest DBD::mysql 107 | docker run --detach --name ci-mariadb mariadb:10.11 108 | mysql -u root -e "CREATE USER 'ci'@'localhost' IDENTIFIED BY 'password';" 109 | mysql -u root -e "CREATE DATABASE zonemaster CHARACTER SET utf8 COLLATE utf8_bin;" 110 | mysql -u root -e "GRANT ALL ON zonemaster.* TO 'ci'@'localhost';" 111 | ;; 112 | postgresql) 113 | cpanm --sudo --notest DBD::Pg 114 | # PGPASSWORD is used by psql 115 | export PGPASSWORD=password 116 | docker run --detach --name ci-postgres -p 5432:5432 --env POSTGRES_PASSWORD="$PGPASSWORD" postgres:16 117 | for i in {1..20} ; do 118 | pg_isready -h localhost -p 5432 && break 119 | sleep 2 120 | done 121 | psql -h localhost -U postgres -c "CREATE USER ci WITH PASSWORD 'password';" 122 | psql -h localhost -U postgres -c 'CREATE DATABASE zonemaster OWNER ci;' 123 | ;; 124 | esac 125 | 126 | - name: Install locales 127 | run: | 128 | sudo perl -pi -e 's/^# (da_DK\.UTF-8.*|en_US\.UTF-8.*|es_ES\.UTF-8.*|fi_FI\.UTF-8.*|fr_FR\.UTF-8.*|nb_NO\.UTF-8.*|sl_SI\.UTF-8.*|sv_SE\.UTF-8.*)/$1/' /etc/locale.gen 129 | sudo locale-gen 130 | 131 | - name: Show content of log files 132 | if: ${{ failure() }} 133 | run: cat /home/runner/.cpanm/work/*/build.log 134 | 135 | - name: Test 136 | env: 137 | ZONEMASTER_BACKEND_CONFIG_FILE: ./share/backend_config.ci_${{ matrix.db }}.ini 138 | run: make test TEST_VERBOSE=1 139 | -------------------------------------------------------------------------------- /t/db_ddl.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use Test::More tests => 2; 5 | use Test::Exception; 6 | use Test::NoWarnings qw(warnings clear_warnings); 7 | 8 | use File::ShareDir qw[dist_file]; 9 | use File::Temp qw[tempdir]; 10 | 11 | my $t_path; 12 | BEGIN { 13 | use File::Spec::Functions qw( rel2abs ); 14 | use File::Basename qw( dirname ); 15 | $t_path = dirname( rel2abs( $0 ) ); 16 | } 17 | use lib $t_path; 18 | use TestUtil; 19 | 20 | use Zonemaster::Engine; 21 | use Zonemaster::Backend::Config; 22 | 23 | my $db_backend = TestUtil::db_backend(); 24 | 25 | my $tempdir = tempdir( CLEANUP => 1 ); 26 | my $config = Zonemaster::Backend::Config->parse( <get_db_class( $db_backend ); 50 | my $db = $dbclass->from_config( $config ); 51 | 52 | 53 | subtest 'Everything but Test::NoWarnings' => sub { 54 | 55 | subtest 'drop and create' => sub { 56 | subtest 'first drop (cleanup) ... ' => sub { 57 | $db->drop_tables(); 58 | dies_ok { 59 | $db->dbh->do( 'SELECT 1 FROM test_results' ) 60 | } 61 | 'table "test_results" sould not exist'; 62 | }; 63 | subtest '... then drop after create ...' => sub { 64 | $db->create_schema(); 65 | my ( $res ) = $db->dbh->selectrow_array( 'SELECT count(*) FROM test_results' ); 66 | is $res, 0, 'a. after create, table "test_results" should exist and be empty'; 67 | 68 | $db->drop_tables(); 69 | dies_ok { 70 | $db->dbh->do( 'SELECT 1 FROM test_results' ) 71 | } 72 | 'b. after drop, table "test_results" sould be removed'; 73 | }; 74 | }; 75 | 76 | subtest 'constraints' => sub { 77 | $db->create_schema(); 78 | 79 | subtest 'constraint unique' => sub { 80 | my $time = $db->format_time( time() ); 81 | my @constraints = ( 82 | { 83 | table => 'test_results', 84 | key => 'hash_id', 85 | sql => "INSERT INTO test_results (hash_id,domain,created_at,params) 86 | VALUES ('0123456789abcdef', 'domain.test', '$time', '{}')" 87 | }, 88 | { 89 | table => 'log_level', 90 | key => 'level', 91 | sql => "INSERT INTO log_level (level, value) VALUES ('OTHER', 10)" 92 | }, 93 | { 94 | table => 'users', 95 | key => 'username', 96 | sql => "INSERT INTO users (username) VALUES ('user1')" 97 | }, 98 | ); 99 | 100 | for my $c (@constraints) { 101 | $db->dbh->do( $c->{sql} ); 102 | throws_ok { 103 | $db->dbh->do( $c->{sql} ); 104 | } 105 | qr/(unique constraint|duplicate entry)/i, "$c->{table}($c->{key}) key should be unique"; 106 | } 107 | }; 108 | 109 | subtest 'constraint on foreign key' => sub { 110 | subtest 'result_entries - hash_id should exist in test_results(hash_id)' => sub { 111 | my $hash_id_ok = "0123456789abcdef"; 112 | # INFO is 1 113 | my $sql = "INSERT INTO result_entries (hash_id, level, module, testcase, tag, timestamp, args) 114 | VALUES ('$hash_id_ok', 1, 'MODULE', 'TESTCASE', 'TAG', 42, '{}')"; 115 | my $inserted_rows = $db->dbh->do( $sql ); 116 | is $inserted_rows, 1, 'can insert an entry with an existing hash_id'; 117 | 118 | throws_ok { 119 | my $hash_id_ko = "aaaaaaaaaaaaaaaa"; 120 | my $sql = "INSERT INTO result_entries (hash_id, level, module, testcase, tag, timestamp, args) 121 | VALUES ('$hash_id_ko', 1, 'MODULE', 'TESTCASE', 'TAG', 42, '{}')"; 122 | $db->dbh->do( $sql ); 123 | } 124 | qr/foreign key/i, 'cannot insert an entry with an non-existing hash_id'; 125 | }; 126 | 127 | subtest 'result_entries - level should exist in log_level(level)' => sub { 128 | my $level = 1; # INFO 129 | my $sql = "INSERT INTO result_entries (hash_id, level, module, testcase, tag, timestamp, args) 130 | VALUES ('0123456789abcdef', '$level', 'MODULE', 'TESTCASE', 'TAG', 42, '{}')"; 131 | my $inserted_rows = $db->dbh->do( $sql ); 132 | is $inserted_rows, 1, 'can insert an entry with an existing level'; 133 | 134 | throws_ok { 135 | my $level = 42; # does not exist 136 | my $sql = "INSERT INTO result_entries (hash_id, level, module, testcase, tag, timestamp, args) 137 | VALUES ('0123456789abcdef', '$level', 'MODULE', 'TESTCASE', 'TAG', 42, '{}')"; 138 | $db->dbh->do( $sql ); 139 | } 140 | qr/foreign key/i, 'cannot insert an entry with an non-existing level'; 141 | }; 142 | }; 143 | }; 144 | }; 145 | 146 | # FIXME: hack to avoid getting warnings from Test::NoWarnings 147 | my @warn = warnings(); 148 | if ( @warn == 7 ) { 149 | clear_warnings(); 150 | } 151 | -------------------------------------------------------------------------------- /script/zonemaster_backend_rpcapi.psgi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use strict; 3 | use warnings; 4 | 5 | our $VERSION = '1.1.0'; 6 | 7 | use 5.14.2; 8 | 9 | use English qw( $PID ); 10 | use JSON::PP; 11 | use JSON::RPC::Dispatch; 12 | use Log::Any qw( $log ); 13 | use Log::Any::Adapter; 14 | use POSIX; 15 | use Plack::Builder; 16 | use Plack::Response; 17 | use Router::Simple::Declare; 18 | use Try::Tiny; 19 | 20 | BEGIN { 21 | $ENV{PERL_JSON_BACKEND} = 'JSON::PP'; 22 | undef $ENV{LANGUAGE}; 23 | }; 24 | 25 | use Zonemaster::Backend::RPCAPI; 26 | use Zonemaster::Backend::Config; 27 | use Zonemaster::Backend::Metrics; 28 | 29 | local $| = 1; 30 | 31 | Log::Any::Adapter->set( 32 | '+Zonemaster::Backend::Log', 33 | log_level => $ENV{ZM_BACKEND_RPCAPI_LOGLEVEL}, 34 | json => $ENV{ZM_BACKEND_RPCAPI_LOGJSON}, 35 | stderr => 1 36 | ); 37 | 38 | $SIG{__WARN__} = sub { 39 | $log->warning(map s/^\s+|\s+$//gr, map s/\n/ /gr, @_); 40 | }; 41 | 42 | my $config = Zonemaster::Backend::Config->load_config(); 43 | 44 | Zonemaster::Backend::Metrics->setup($config->METRICS_statsd_host, $config->METRICS_statsd_port); 45 | Zonemaster::Engine::init_engine(); 46 | 47 | builder { 48 | enable sub { 49 | my $app = shift; 50 | 51 | # Make sure we can connect to the database 52 | $config->new_DB(); 53 | 54 | return $app; 55 | }; 56 | }; 57 | 58 | my $handler = Zonemaster::Backend::RPCAPI->new( { config => $config } ); 59 | 60 | my $router = router { 61 | ############## FRONTEND #################### 62 | connect "version_info" => { 63 | handler => $handler, 64 | action => "version_info" 65 | }; 66 | 67 | # Experimental 68 | connect "system_versions" => { 69 | handler => $handler, 70 | action => "system_versions" 71 | }; 72 | 73 | connect "profile_names" => { 74 | handler => $handler, 75 | action => "profile_names" 76 | }; 77 | 78 | # Experimental 79 | connect "conf_profiles" => { 80 | handler => $handler, 81 | action => "conf_profiles" 82 | }; 83 | 84 | connect "get_language_tags" => { 85 | handler => $handler, 86 | action => "get_language_tags" 87 | }; 88 | 89 | # Experimental 90 | connect "conf_languages" => { 91 | handler => $handler, 92 | action => "conf_languages" 93 | }; 94 | 95 | connect "get_host_by_name" => { 96 | handler => $handler, 97 | action => "get_host_by_name" 98 | }; 99 | 100 | # Experimental 101 | connect "lookup_address_records" => { 102 | handler => $handler, 103 | action => "lookup_address_records" 104 | }; 105 | 106 | connect "get_data_from_parent_zone" => { 107 | handler => $handler, 108 | action => "get_data_from_parent_zone" 109 | }; 110 | 111 | # Experimental 112 | connect "lookup_delegation_data" => { 113 | handler => $handler, 114 | action => "lookup_delegation_data" 115 | }; 116 | 117 | connect "start_domain_test" => { 118 | handler => $handler, 119 | action => "start_domain_test" 120 | }; 121 | 122 | # Experimental 123 | connect "job_create" => { 124 | handler => $handler, 125 | action => "job_create" 126 | }; 127 | 128 | connect "test_progress" => { 129 | handler => $handler, 130 | action => "test_progress" 131 | }; 132 | 133 | # Experimental 134 | connect "job_status" => { 135 | handler => $handler, 136 | action => "job_status" 137 | }; 138 | 139 | connect "get_test_params" => { 140 | handler => $handler, 141 | action => "get_test_params" 142 | }; 143 | 144 | # Experimental 145 | connect "job_params" => { 146 | handler => $handler, 147 | action => "job_params" 148 | }; 149 | 150 | connect "get_test_results" => { 151 | handler => $handler, 152 | action => "get_test_results" 153 | }; 154 | 155 | # Experimental 156 | connect "job_results" => { 157 | handler => $handler, 158 | action => "job_results" 159 | }; 160 | 161 | connect "get_test_history" => { 162 | handler => $handler, 163 | action => "get_test_history" 164 | }; 165 | 166 | # Experimental 167 | connect "domain_history" => { 168 | handler => $handler, 169 | action => "domain_history" 170 | }; 171 | 172 | connect "batch_status" => { 173 | handler => $handler, 174 | action => "batch_status" 175 | }; 176 | }; 177 | 178 | if ( $config->RPCAPI_enable_user_create or $config->RPCAPI_enable_add_api_user ) { 179 | $log->info('Enabling add_api_user method'); 180 | $router->connect("add_api_user", { 181 | handler => $handler, 182 | action => "add_api_user" 183 | }); 184 | $router->connect("user_create", { 185 | handler => $handler, 186 | action => "user_create" 187 | }); 188 | } 189 | 190 | if ( $config->RPCAPI_enable_batch_create or $config->RPCAPI_enable_add_batch_job ) { 191 | $log->info('Enabling add_batch_job method'); 192 | $router->connect("add_batch_job", { 193 | handler => $handler, 194 | action => "add_batch_job" 195 | }); 196 | $router->connect("batch_create", { 197 | handler => $handler, 198 | action => "batch_create" 199 | }); 200 | } 201 | 202 | my $dispatch = JSON::RPC::Dispatch->new( 203 | router => $router, 204 | ); 205 | 206 | my $rpcapi_app = sub { 207 | my $env = shift; 208 | my $req = Plack::Request->new($env); 209 | my $res = {}; 210 | my $content = {}; 211 | my $json_error = ''; 212 | try { 213 | my $json = $req->content; 214 | $content = decode_json($json); 215 | } catch { 216 | $json_error = (split /at \//, $_)[0]; 217 | }; 218 | 219 | if ($json_error eq '') { 220 | my $errors = $handler->jsonrpc_validate($content); 221 | if ($errors ne '') { 222 | $res = Plack::Response->new(200); 223 | $res->content_type('application/json'); 224 | $res->body( encode_json($errors) ); 225 | $res->finalize; 226 | } else { 227 | local $log->context->{rpc_method} = $content->{method}; 228 | $res = $dispatch->handle_psgi($env, $env->{REMOTE_ADDR}); 229 | my $status = Zonemaster::Backend::Metrics->code_to_status(decode_json(@{@$res[2]}[0])->{error}->{code}); 230 | Zonemaster::Backend::Metrics::increment("zonemaster.rpcapi.requests.$content->{method}.$status"); 231 | $res; 232 | } 233 | } else { 234 | $res = Plack::Response->new(200); 235 | $res->content_type('application/json'); 236 | $res->body( encode_json({ 237 | jsonrpc => '2.0', 238 | id => undef, 239 | error => { 240 | code => '-32700', 241 | message => 'Invalid JSON was received by the server.', 242 | data => "$json_error" 243 | }}) ); 244 | $res->finalize; 245 | 246 | } 247 | }; 248 | 249 | builder { 250 | enable "Plack::Middleware::ReverseProxy"; 251 | mount "/" => $rpcapi_app; 252 | }; 253 | -------------------------------------------------------------------------------- /lib/Zonemaster/Backend/TestAgent.pm: -------------------------------------------------------------------------------- 1 | package Zonemaster::Backend::TestAgent; 2 | our $VERSION = '1.1.0'; 3 | 4 | use strict; 5 | use warnings; 6 | use 5.14.2; 7 | 8 | use DBI qw(:utils); 9 | use JSON::PP; 10 | use Scalar::Util qw( blessed ); 11 | use File::Slurp; 12 | use Locale::TextDomain qw[Zonemaster-Backend]; 13 | use Time::HiRes qw[time sleep gettimeofday tv_interval]; 14 | 15 | use Zonemaster::LDNS; 16 | 17 | use Zonemaster::Engine; 18 | use Zonemaster::Engine::Translator; 19 | use Zonemaster::Engine::Profile; 20 | use Zonemaster::Engine::Util; 21 | use Zonemaster::Engine::Logger::Entry; 22 | use Zonemaster::Backend::Config; 23 | use Zonemaster::Backend::Metrics; 24 | 25 | sub new { 26 | my ( $class, $params ) = @_; 27 | my $self = {}; 28 | 29 | if ( !$params || !$params->{config} ) { 30 | die "missing 'config' parameter"; 31 | } 32 | 33 | my $config = $params->{config}; 34 | 35 | my $dbtype; 36 | if ( $params->{dbtype} ) { 37 | $dbtype = $config->check_db( $params->{dbtype} ); 38 | } 39 | else { 40 | $dbtype = $config->DB_engine; 41 | } 42 | 43 | my $dbclass = Zonemaster::Backend::DB->get_db_class( $dbtype ); 44 | $self->{_db} = $dbclass->from_config( $config ); 45 | 46 | $self->{_profiles} = Zonemaster::Backend::Config->load_profiles( # 47 | $config->PUBLIC_PROFILES, 48 | $config->PRIVATE_PROFILES, 49 | ); 50 | 51 | bless( $self, $class ); 52 | return $self; 53 | } 54 | 55 | sub run { 56 | my ( $self, $test_id, $show_progress ) = @_; 57 | my @accumulator; 58 | 59 | my $params; 60 | 61 | $params = $self->{_db}->get_test_params( $test_id ); 62 | 63 | my ( $domain ) = $params->{domain}; 64 | if ( !$domain ) { 65 | die "Must give the name of a domain to test.\n"; 66 | } 67 | $domain = $self->to_idn( $domain ); 68 | my %numeric = Zonemaster::Engine::Logger::Entry->levels(); 69 | 70 | if ( $params->{nameservers} && @{ $params->{nameservers} } > 0 ) { 71 | $self->add_fake_delegation( $domain, $params->{nameservers} ); 72 | } 73 | 74 | if ( $params->{ds_info} && @{ $params->{ds_info} } > 0 ) { 75 | $self->add_fake_ds( $domain, $params->{ds_info} ); 76 | } 77 | 78 | # If the profile parameter has been set in the API, then load a profile 79 | if ( $params->{profile} ) { 80 | $params->{profile} = lc($params->{profile}); 81 | if ( defined $self->{_profiles}{ $params->{profile} } ) { 82 | Zonemaster::Engine::Profile->effective->merge( $self->{_profiles}{ $params->{profile} } ); 83 | } 84 | else { 85 | die "The profile [$params->{profile}] is not defined in the backend_config ini file"; 86 | } 87 | } 88 | 89 | # If IPv4 or IPv6 transport has been explicitly disabled or enabled, then load it after 90 | # any explicitly set profile has been loaded. 91 | if (defined $params->{ipv4}) { 92 | Zonemaster::Engine::Profile->effective->set( q{net.ipv4}, ( $params->{ipv4} ) ? ( 1 ) : ( 0 ) ); 93 | } 94 | 95 | if (defined $params->{ipv6}) { 96 | Zonemaster::Engine::Profile->effective->set( q{net.ipv6}, ( $params->{ipv6} ) ? ( 1 ) : ( 0 ) ); 97 | } 98 | 99 | if ( $show_progress ) { 100 | my %methods = Zonemaster::Engine->all_methods; 101 | 102 | # BASIC methods are always run: Basic0{0..4} 103 | my $nbr_testcases_planned = 5; 104 | my $nbr_testcases_finished = 0; 105 | 106 | foreach my $module ( keys %methods ) { 107 | foreach my $method ( @{ $methods{$module} } ) { 108 | if ( Zonemaster::Engine::Util::should_run_test( $method ) ) { 109 | $nbr_testcases_planned++; 110 | } 111 | } 112 | } 113 | 114 | Zonemaster::Engine->logger->callback( 115 | sub { 116 | my ( $entry ) = @_; 117 | 118 | if ( $entry->{tag} and $entry->{tag} eq 'TEST_CASE_END' ) { 119 | $nbr_testcases_finished++; 120 | my $progress_percent = int( 100 * $nbr_testcases_finished / $nbr_testcases_planned ); 121 | $self->{_db}->test_progress( $test_id, $progress_percent ); 122 | } 123 | } 124 | ); 125 | } 126 | 127 | # Actually run tests! 128 | eval { Zonemaster::Engine->test_zone( $domain ); }; 129 | if ( $@ ) { 130 | my $err = $@; 131 | if ( blessed $err and $err->isa( "NormalExit" ) ) { 132 | say STDERR "Exited early: " . $err->message; 133 | } 134 | else { 135 | die "$err\n"; # Don't know what it is, rethrow 136 | } 137 | } 138 | 139 | my $insert_result_start_time = [ gettimeofday ]; 140 | 141 | # TODO: Make minimum level configurable 142 | my @entries = grep { $_->numeric_level >= $numeric{INFO} } @{ Zonemaster::Engine->logger->entries }; 143 | 144 | Zonemaster::Backend::Metrics::timing("zonemaster.testagent.log_callback_add_result_entry_filter_duration", tv_interval($insert_result_start_time) * 1000); 145 | 146 | $self->{_db}->add_result_entries( $test_id, @entries); 147 | 148 | my $callback_add_result_entry_duration = tv_interval($insert_result_start_time); 149 | Zonemaster::Backend::Metrics::timing("zonemaster.testagent.log_callback_add_result_entry_duration", $callback_add_result_entry_duration * 1000); 150 | 151 | $self->{_db}->set_test_completed( $test_id ); 152 | 153 | return; 154 | } ## end sub run 155 | 156 | sub reset { 157 | my ( $self ) = @_; 158 | Zonemaster::Engine->reset(); 159 | } 160 | 161 | sub add_fake_delegation { 162 | my ( $self, $domain, $nameservers ) = @_; 163 | my @ns_with_no_ip; 164 | my %data; 165 | 166 | foreach my $ns_ip_pair ( @$nameservers ) { 167 | if ( $ns_ip_pair->{ns} && $ns_ip_pair->{ip} ) { 168 | push( @{ $data{ $self->to_idn( $ns_ip_pair->{ns} ) } }, $ns_ip_pair->{ip} ); 169 | } 170 | elsif ($ns_ip_pair->{ns}) { 171 | push(@ns_with_no_ip, $self->to_idn( $ns_ip_pair->{ns} ) ); 172 | } 173 | else { 174 | die "Invalid ns_ip_pair"; 175 | } 176 | } 177 | 178 | foreach my $ns ( @ns_with_no_ip ) { 179 | if ( not exists $data{ $ns } ) { 180 | $data{ $self->to_idn( $ns ) } = undef; 181 | } 182 | } 183 | 184 | Zonemaster::Engine->add_fake_delegation( $domain => \%data ); 185 | 186 | return; 187 | } 188 | 189 | sub add_fake_ds { 190 | my ( $self, $domain, $ds_info ) = @_; 191 | my @data; 192 | 193 | foreach my $ds ( @{ $ds_info } ) { 194 | push @data, { keytag => $ds->{keytag}, algorithm => $ds->{algorithm}, type => $ds->{digtype}, digest => $ds->{digest} }; 195 | } 196 | 197 | Zonemaster::Engine->add_fake_ds( $domain => \@data ); 198 | 199 | return; 200 | } 201 | 202 | sub to_idn { 203 | my ( $self, $str ) = @_; 204 | 205 | if ( $str =~ m/^[[:ascii:]]+$/ ) { 206 | return $str; 207 | } 208 | 209 | if ( Zonemaster::LDNS::has_idn() ) { 210 | return Zonemaster::LDNS::to_idn( $str ); 211 | } 212 | else { 213 | warn __( "Warning: Zonemaster::LDNS not compiled with IDN support, cannot handle non-ASCII names correctly." ); 214 | return $str; 215 | } 216 | } 217 | 218 | 1; 219 | -------------------------------------------------------------------------------- /t/parameters_validation.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use 5.14.2; 4 | use utf8; 5 | 6 | use Test::More tests => 4; 7 | use Test::NoWarnings; 8 | 9 | use Cwd; 10 | use File::Temp qw[tempdir]; 11 | use Zonemaster::Backend::Config; 12 | use Zonemaster::Backend::RPCAPI; 13 | use JSON::Validator::Joi "joi"; 14 | use JSON::PP; 15 | 16 | my $tempdir = tempdir( CLEANUP => 1 ); 17 | my $cwd = cwd(); 18 | 19 | my $config = Zonemaster::Backend::Config->parse( <new( 31 | { 32 | dbtype => $config->DB_engine, 33 | config => $config, 34 | } 35 | ); 36 | 37 | sub test_validation { 38 | my ( $method_name, $method_schema, $test_cases ) = @_; 39 | 40 | subtest "Method $method_name" => sub { 41 | for my $test_case (@$test_cases) { 42 | subtest 'Test case: ' . $test_case->{name} => sub { 43 | my @res = $rpcapi->validate_params( $method_schema, $test_case->{input}); 44 | is_deeply(\@res, $test_case->{output}, 'Matched validation output' ) or diag( encode_json \@res); 45 | }; 46 | } 47 | }; 48 | } 49 | 50 | subtest 'Test JSON schema' => sub { 51 | my $test_joi_schema = joi->new->object->strict->props( 52 | hostname => joi->new->string->max(10)->required 53 | ); 54 | 55 | my $test_raw_schema = { 56 | type => 'object', 57 | additionalProperties => 0, 58 | required => [ 'hostname' ], 59 | properties => { 60 | hostname => { 61 | type => 'string', 62 | maxLength => 10 63 | } 64 | } 65 | }; 66 | 67 | my $test_cases = [ 68 | { 69 | name => 'Empty request', 70 | input => {}, 71 | output => [{ 72 | message => 'Missing property', 73 | path => '/hostname' 74 | }] 75 | }, 76 | { 77 | name => 'Correct request', 78 | input => { 79 | hostname => 'example' 80 | }, 81 | output => [] 82 | }, 83 | { 84 | name => 'Bad request', 85 | input => { 86 | hostname => 'example.toolong' 87 | }, 88 | output => [{ 89 | message => 'String is too long: 15/10.', 90 | path => '/hostname' 91 | }] 92 | } 93 | ]; 94 | 95 | test_validation 'test_joi', $test_joi_schema, $test_cases; 96 | test_validation 'test_raw', $test_raw_schema, $test_cases; 97 | }; 98 | 99 | subtest 'Test custom error message' => sub { 100 | my $test_custom_error_schema = { 101 | type => 'object', 102 | additionalProperties => 0, 103 | required => [ 'hostname' ], 104 | additionalProperties => 0, 105 | properties => { 106 | hostname => { 107 | type => 'string', 108 | 'x-error-message' => 'Bad hostname, should be a string less than 10 characters long', 109 | maxLength => 10 110 | }, 111 | nameservers => { 112 | type => 'array', 113 | items => { 114 | type => 'object', 115 | required => [ 'ip' ], 116 | additionalProperties => 0, 117 | properties => { 118 | ip => { 119 | type => 'string', 120 | 'x-error-message' => 'Bad IP address', 121 | pattern => '^[a-f0-9\.:]+$' 122 | } 123 | } 124 | } 125 | } 126 | } 127 | }; 128 | 129 | my $test_cases = [ 130 | { 131 | name => 'Bad input', 132 | input => { 133 | hostname => 'This is a bad input', 134 | nameservers => [ 135 | { ip => 'Very bad indeed'}, 136 | { ip => '10.10.10.10' }, 137 | { ip => 'But not the previous property' } 138 | ] 139 | }, 140 | output => [ 141 | { 142 | path => '/hostname', 143 | message => 'Bad hostname, should be a string less than 10 characters long', 144 | }, 145 | { 146 | path => '/nameservers/0/ip', 147 | message => 'Bad IP address', 148 | }, 149 | { 150 | path => '/nameservers/2/ip', 151 | message => 'Bad IP address', 152 | } 153 | ] 154 | } 155 | ]; 156 | 157 | test_validation 'test_custom_error', $test_custom_error_schema, $test_cases; 158 | }; 159 | 160 | subtest 'Test custom formats' => sub { 161 | my $test_extra_validator_schema = { 162 | type => 'object', 163 | properties => { 164 | my_ip => { 165 | type => 'string', 166 | format => 'ip', 167 | }, 168 | my_lang => { 169 | type => 'string', 170 | format => 'language_tag', 171 | }, 172 | my_domain => { 173 | type => 'string', 174 | format => 'domain', 175 | }, 176 | my_profile => { 177 | type => 'string', 178 | format => 'profile', 179 | }, 180 | } 181 | }; 182 | 183 | my $test_cases = [ 184 | { 185 | name => 'Input ok', 186 | input => { 187 | my_ip => '192.0.2.1', 188 | my_lang => 'en', 189 | my_domain => 'zonemaster.net', 190 | my_profile => 'test', 191 | }, 192 | output => [] 193 | }, 194 | { 195 | name => 'Bad ip', 196 | input => { 197 | my_ip => 'abc', 198 | }, 199 | output => [{ 200 | path => '/my_ip', 201 | message => 'Invalid IP address' 202 | }] 203 | }, 204 | { 205 | name => 'Bad language format', 206 | input => { 207 | my_lang => 'abc', 208 | }, 209 | output => [{ 210 | path => '/my_lang', 211 | message => 'Invalid language tag format' 212 | }] 213 | }, 214 | { 215 | name => 'Bad domain', 216 | input => { 217 | my_domain => 'not a domain', 218 | }, 219 | output => [{ 220 | path => '/my_domain', 221 | message => 'Domain name has an ASCII label ("not a domain") with a character not permitted.' 222 | }] 223 | }, 224 | { 225 | name => 'Bad profile', 226 | input => { 227 | my_profile => 'other_profile', 228 | }, 229 | output => [{ 230 | path => '/my_profile', 231 | message => 'Unknown profile' 232 | }] 233 | }, 234 | ]; 235 | 236 | test_validation 'test_extra_validator', $test_extra_validator_schema, $test_cases; 237 | }; 238 | -------------------------------------------------------------------------------- /t/rpc_validation.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | use 5.14.2; 4 | use utf8; 5 | 6 | use Test::More tests => 30; 7 | use Test::NoWarnings; 8 | 9 | use Cwd; 10 | use File::Temp qw[tempdir]; 11 | use Zonemaster::Backend::Config; 12 | use Zonemaster::Backend::RPCAPI; 13 | use JSON::Validator::Joi "joi"; 14 | use JSON::PP; 15 | 16 | ### 17 | ### Setup 18 | ### 19 | 20 | my $tempdir = tempdir( CLEANUP => 1 ); 21 | my $cwd = cwd(); 22 | 23 | my $config = Zonemaster::Backend::Config->parse( <new( 35 | { 36 | dbtype => $config->DB_engine, 37 | config => $config, 38 | } 39 | ); 40 | 41 | ### 42 | ### JSONRPC request object construction helper 43 | ### 44 | 45 | sub jsonrpc 46 | { 47 | my ($method, $params, $force_undef) = @_; 48 | my $object = { 49 | jsonrpc => '2.0', 50 | id => 'testing', 51 | method => $method 52 | }; 53 | if (defined $params or $force_undef) { 54 | $object->{params} = $params; 55 | } 56 | 57 | return $object; 58 | } 59 | 60 | ### 61 | ### JSONRPC error response construction helpers 62 | ### 63 | 64 | sub jsonrpc_error 65 | { 66 | my ($message, $code, $data, $id) = @_; 67 | my $object = { 68 | jsonrpc => '2.0', 69 | id => $id, 70 | error => { 71 | message => $message, 72 | code => $code 73 | } 74 | }; 75 | $object->{error}{data} = $data if defined $data; 76 | return $object; 77 | } 78 | 79 | sub error_bad_jsonrpc 80 | { 81 | my ($data) = @_; 82 | 83 | jsonrpc_error('The JSON sent is not a valid request object.', '-32600', $data, undef); 84 | } 85 | 86 | sub error_missing_params 87 | { 88 | jsonrpc_error("Missing 'params' object", '-32602', undef, 'testing'); 89 | } 90 | 91 | sub error_bad_params 92 | { 93 | my ($messages) = @_; 94 | 95 | my @data; 96 | 97 | while (@$messages) { 98 | my $path = shift @$messages; 99 | my $message = shift @$messages; 100 | push @data, { path => $path, message => $message }; 101 | } 102 | 103 | jsonrpc_error('Invalid method parameter(s).', '-32602', \@data, 'testing'); 104 | } 105 | 106 | sub no_error 107 | { 108 | return ''; 109 | } 110 | 111 | ### 112 | ### Test wrapper functions 113 | ### 114 | 115 | sub test_validation 116 | { 117 | my ($input, $output, $message) = @_; 118 | 119 | my $res = $rpcapi->jsonrpc_validate($input); 120 | is_deeply($res, $output, $message) or diag(encode_json($res)); 121 | } 122 | 123 | 124 | ### 125 | ### The tests themselves 126 | ### 127 | 128 | test_validation undef, 129 | error_bad_jsonrpc('/: Expected object - got null.'), 130 | "Sending undef is an error"; 131 | 132 | test_validation JSON::PP::false, 133 | error_bad_jsonrpc('/: Expected object - got boolean.'), 134 | "Sending a boolean is an error"; 135 | 136 | test_validation -1, 137 | error_bad_jsonrpc('/: Expected object - got number.'), 138 | "Sending a number is an error"; 139 | 140 | test_validation "hello", 141 | error_bad_jsonrpc('/: Expected object - got string.'), 142 | "Sending a string is an error"; 143 | 144 | test_validation [qw(a b c)], 145 | error_bad_jsonrpc('/: Expected object - got array.'), 146 | "Sending an array is an error"; 147 | 148 | test_validation {}, 149 | error_bad_jsonrpc('/jsonrpc: Missing property. /method: Missing property.'), 150 | "Sending an empty object is an error"; 151 | 152 | test_validation { jsonrpc => '2.0' }, 153 | error_bad_jsonrpc('/method: Missing property.'), 154 | "Sending an incomplete object is an error"; 155 | 156 | test_validation { jsonrpc => '2.0', method => 'system_versions' }, 157 | error_bad_jsonrpc(''), 158 | "Sending an object with no ID is an error"; 159 | 160 | test_validation { jsonrpc => '2.0', method => 'system_versions', id => JSON::PP::false }, 161 | error_bad_jsonrpc('/id: Expected null/number/string - got boolean.'), 162 | "Sending an object whose ID is a boolean is an error"; 163 | 164 | test_validation { jsonrpc => '2.0', method => 'system_versions', id => [qw(a b c)] }, 165 | error_bad_jsonrpc('/id: Expected null/number/string - got array.'), 166 | "Sending an object whose ID is an array is an error"; 167 | 168 | test_validation { jsonrpc => '2.0', method => 'system_versions', id => { a => 1 } }, 169 | error_bad_jsonrpc('/id: Expected null/number/string - got object.'), 170 | "Sending an object whose ID is an object is an error"; 171 | 172 | test_validation jsonrpc("job_status"), 173 | error_missing_params(), 174 | "Calling job_status without parameters is an error"; 175 | 176 | test_validation jsonrpc("job_status", undef, 1), 177 | error_bad_params(["/" => "Expected object - got null."]), 178 | "Passing null as parameter to job_status is an error"; 179 | 180 | test_validation jsonrpc("job_status", JSON::PP::false), 181 | error_bad_params(["/" => "Expected object - got boolean."]), 182 | "Passing boolean as parameter to job_status is an error"; 183 | 184 | test_validation jsonrpc("job_status", 1), 185 | error_bad_params(["/" => "Expected object - got number."]), 186 | "Passing number as parameter to job_status is an error"; 187 | 188 | test_validation jsonrpc("job_status", "hello"), 189 | error_bad_params(["/" => "Expected object - got string."]), 190 | "Passing string as parameter to job_status is an error"; 191 | 192 | test_validation jsonrpc("job_status", [qw(a b c)]), 193 | error_bad_params(["/" => "Expected object - got array."]), 194 | "Passing array as parameter to job_status is an error"; 195 | 196 | test_validation jsonrpc("job_status", {}), 197 | error_bad_params(["/job_id" => "Missing property"]), 198 | "Passing empty object as parameter to job_status is an error"; 199 | 200 | test_validation jsonrpc("job_status", { job_id => 'this_will_definitely_never_ever_exist' }), 201 | error_bad_params(["/job_id" => 'String does not match (?^u:^[0-9a-f]{16}$).']), 202 | "Calling job_status with a bad job_id is an error"; 203 | 204 | test_validation jsonrpc("job_status", { job_id => '0123456789abcdef', data => "something" }), 205 | error_bad_params(["/" => "Properties not allowed: data."]), 206 | "Calling job_status with unknown parameters is an error"; 207 | 208 | test_validation jsonrpc("job_status", { job_id => '0123456789abcdef' }), 209 | no_error, 210 | "Calling job_status with a good job_id succeeds"; 211 | 212 | test_validation jsonrpc("system_versions"), 213 | no_error, 214 | "Calling system_versions with no parameters is OK"; 215 | 216 | test_validation jsonrpc("system_versions", undef, 1), 217 | error_bad_params(["/" => "Expected object - got null."]), 218 | "Passing null as parameter to system_versions is an error"; 219 | 220 | test_validation jsonrpc("system_versions", JSON::PP::false), 221 | error_bad_params(["/" => "Expected object - got boolean."]), 222 | "Passing number as parameter to system_versions is an error"; 223 | 224 | test_validation jsonrpc("system_versions", -1), 225 | error_bad_params(["/" => "Expected object - got number."]), 226 | "Passing number as parameter to system_versions is an error"; 227 | 228 | test_validation jsonrpc("system_versions", "hello"), 229 | error_bad_params(["/" => "Expected object - got string."]), 230 | "Passing string as parameter to system_versions is an error"; 231 | 232 | test_validation jsonrpc("system_versions", [qw(a b c)]), 233 | error_bad_params(["/" => "Expected object - got array."]), 234 | "Passing array as parameter to system_versions is an error"; 235 | 236 | test_validation jsonrpc("system_versions", { data => "something" }), 237 | error_bad_params(["/" => "Properties not allowed: data."]), 238 | "Calling system_versions with unrecognized parameter is an error"; 239 | 240 | test_validation jsonrpc("system_versions", {}), 241 | no_error, 242 | "Calling system_versions with empty object succeeds"; 243 | -------------------------------------------------------------------------------- /share/patch/patch_db_zonemaster_backend_ver_9.0.0.pl: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use Try::Tiny; 5 | 6 | use Zonemaster::Backend::Config; 7 | 8 | my $config = Zonemaster::Backend::Config->load_config(); 9 | 10 | my %patch = ( 11 | mysql => \&patch_db_mysql, 12 | postgresql => \&patch_db_postgresql, 13 | sqlite => \&patch_db_sqlite, 14 | ); 15 | 16 | my $db_engine = $config->DB_engine; 17 | 18 | if ( $db_engine =~ /^(MySQL|PostgreSQL|SQLite)$/ ) { 19 | $patch{ lc $db_engine }(); 20 | } 21 | else { 22 | die "Unknown database engine configured: $db_engine\n"; 23 | } 24 | 25 | sub patch_db_mysql { 26 | use Zonemaster::Backend::DB::MySQL; 27 | 28 | my $db = Zonemaster::Backend::DB::MySQL->from_config( $config ); 29 | my $dbh = $db->dbh; 30 | 31 | # add table constraints 32 | $dbh->do( 'ALTER TABLE users ADD CONSTRAINT UNIQUE (username)' ); 33 | $dbh->do( 'ALTER TABLE test_results ADD CONSTRAINT UNIQUE (hash_id)' ); 34 | 35 | # update columns names, data type and default value 36 | $dbh->do( 'ALTER TABLE test_results MODIFY COLUMN id BIGINT AUTO_INCREMENT' ); 37 | $dbh->do( 'ALTER TABLE test_results CHANGE COLUMN creation_time created_at DATETIME NOT NULL' ); 38 | $dbh->do( 'ALTER TABLE test_results CHANGE COLUMN test_start_time started_at DATETIME DEFAULT NULL' ); 39 | $dbh->do( 'ALTER TABLE test_results CHANGE COLUMN test_end_time ended_at DATETIME DEFAULT NULL' ); 40 | 41 | $dbh->do( 'ALTER TABLE batch_jobs CHANGE COLUMN creation_time created_at DATETIME NOT NULL' ); 42 | 43 | $dbh->{AutoCommit} = 0; 44 | 45 | try { 46 | # normalize "domain" column 47 | $dbh->do( 48 | q[ 49 | UPDATE test_results 50 | SET domain = LOWER(domain) 51 | WHERE CAST(domain AS BINARY) RLIKE '[A-Z]' 52 | ] 53 | ); 54 | $dbh->do( 55 | q[ 56 | UPDATE test_results 57 | SET domain = '.' 58 | WHERE domain = '..' OR domain = '...' OR domain = '....' 59 | ] 60 | ); 61 | $dbh->do( 62 | q[ 63 | UPDATE test_results 64 | SET domain = TRIM( TRAILING '.' FROM domain ) 65 | WHERE domain != '.' AND domain LIKE '%.' 66 | ] 67 | ); 68 | 69 | $dbh->commit(); 70 | } catch { 71 | print( "Could not upgrade database: " . $_ ); 72 | 73 | eval { $dbh->rollback() }; 74 | }; 75 | } 76 | 77 | sub patch_db_postgresql { 78 | use Zonemaster::Backend::DB::PostgreSQL; 79 | 80 | my $db = Zonemaster::Backend::DB::PostgreSQL->from_config( $config ); 81 | my $dbh = $db->dbh; 82 | 83 | $dbh->{AutoCommit} = 0; 84 | 85 | try { 86 | # update sequence data type to BIGINT 87 | $dbh->do( 'ALTER SEQUENCE test_results_id_seq AS BIGINT' ); 88 | $dbh->do( 'ALTER TABLE test_results ALTER COLUMN id SET DATA TYPE BIGINT' ); 89 | 90 | # remove default value for "creation_time" 91 | $dbh->do( 'ALTER TABLE test_results ALTER COLUMN creation_time DROP DEFAULT' ); 92 | $dbh->do( 'ALTER TABLE batch_jobs ALTER COLUMN creation_time DROP DEFAULT' ); 93 | 94 | # rename columns 95 | $dbh->do( 'ALTER TABLE test_results RENAME COLUMN creation_time TO created_at' ); 96 | $dbh->do( 'ALTER TABLE test_results RENAME COLUMN test_start_time TO started_at' ); 97 | $dbh->do( 'ALTER TABLE test_results RENAME COLUMN test_end_time TO ended_at' ); 98 | $dbh->do( 'ALTER TABLE batch_jobs RENAME COLUMN creation_time TO created_at' ); 99 | 100 | # add table constraints 101 | $dbh->do( 'ALTER TABLE test_results ADD UNIQUE (hash_id)' ); 102 | $dbh->do( 'ALTER TABLE users ADD UNIQUE (username)' ); 103 | 104 | # normalize "domain" column 105 | $dbh->do( 106 | q[ 107 | UPDATE test_results 108 | SET domain = LOWER(domain) 109 | WHERE domain != LOWER(domain) 110 | ] 111 | ); 112 | $dbh->do( 113 | q[ 114 | UPDATE test_results 115 | SET domain = '.' 116 | WHERE domain = '..' OR domain = '...' OR domain = '....' 117 | ] 118 | ); 119 | $dbh->do( 120 | q[ 121 | UPDATE test_results 122 | SET domain = RTRIM(domain, '.') 123 | WHERE domain != '.' AND domain LIKE '%.' 124 | ] 125 | ); 126 | 127 | $dbh->commit(); 128 | } catch { 129 | print( "Could not upgrade database: " . $_ ); 130 | 131 | eval { $dbh->rollback() }; 132 | }; 133 | } 134 | 135 | sub patch_db_sqlite { 136 | use Zonemaster::Backend::DB::SQLite; 137 | 138 | my $db = Zonemaster::Backend::DB::SQLite->from_config( $config ); 139 | my $dbh = $db->dbh; 140 | 141 | $dbh->{AutoCommit} = 0; 142 | 143 | # since we change the default value for a column, the whole table needs to 144 | # be recreated 145 | # 1. rename the table to "_old" 146 | # 2. recreate a clean table schema 147 | # 3. populate it with the values from "
_old" 148 | # 4. remove "
_old" and indexes 149 | # 5. recreate the indexes 150 | try { 151 | $dbh->do('ALTER TABLE test_results RENAME TO test_results_old'); 152 | $dbh->do('ALTER TABLE batch_jobs RENAME TO batch_jobs_old'); 153 | $dbh->do('ALTER TABLE users RENAME TO users_old'); 154 | 155 | # create the tables 156 | $db->create_schema(); 157 | 158 | # populate the tables 159 | $dbh->do( 160 | q[ 161 | INSERT INTO test_results 162 | ( 163 | id, 164 | hash_id, 165 | domain, 166 | batch_id, 167 | created_at, 168 | started_at, 169 | ended_at, 170 | priority, 171 | queue, 172 | progress, 173 | fingerprint, 174 | params, 175 | results, 176 | undelegated 177 | ) 178 | SELECT 179 | id, 180 | hash_id, 181 | lower(domain), 182 | batch_id, 183 | creation_time, 184 | test_start_time, 185 | test_end_time, 186 | priority, 187 | queue, 188 | progress, 189 | fingerprint, 190 | params, 191 | results, 192 | undelegated 193 | FROM test_results_old 194 | ] 195 | ); 196 | $dbh->do( 197 | q[ 198 | UPDATE test_results 199 | SET domain = '.' 200 | WHERE domain = '..' OR domain = '...' OR domain = '....' 201 | ] 202 | ); 203 | $dbh->do( 204 | q[ 205 | UPDATE test_results 206 | SET domain = RTRIM(domain, '.') 207 | WHERE domain != '.' AND domain LIKE '%.' 208 | ] 209 | ); 210 | 211 | $dbh->do(' 212 | INSERT INTO batch_jobs 213 | ( 214 | id, 215 | username, 216 | created_at 217 | ) 218 | SELECT 219 | id, 220 | username, 221 | creation_time 222 | FROM batch_jobs_old 223 | '); 224 | 225 | $dbh->do(' 226 | INSERT INTO users 227 | ( 228 | id, 229 | username, 230 | api_key 231 | ) 232 | SELECT 233 | id, 234 | username, 235 | api_key 236 | FROM users_old 237 | '); 238 | 239 | # delete old tables 240 | $dbh->do('DROP TABLE test_results_old'); 241 | $dbh->do('DROP TABLE batch_jobs_old'); 242 | $dbh->do('DROP TABLE users_old'); 243 | 244 | # recreate indexes 245 | $db->create_schema(); 246 | 247 | $dbh->commit(); 248 | } catch { 249 | print( "Error while upgrading database: " . $_ ); 250 | 251 | eval { $dbh->rollback() }; 252 | }; 253 | } 254 | -------------------------------------------------------------------------------- /script/zonemaster_backend_testagent: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use 5.14.2; 4 | use warnings; 5 | 6 | use Zonemaster::Backend::TestAgent; 7 | use Zonemaster::Backend::Config; 8 | use Zonemaster::Backend::Metrics; 9 | 10 | use Parallel::ForkManager; 11 | use Daemon::Control; 12 | use Log::Any qw( $log ); 13 | use Log::Any::Adapter; 14 | 15 | use English; 16 | use Pod::Usage; 17 | use Getopt::Long; 18 | use POSIX; 19 | use Time::HiRes qw[time sleep gettimeofday tv_interval]; 20 | use sigtrap qw(die normal-signals); 21 | 22 | ### 23 | ### Compile-time stuff. 24 | ### 25 | 26 | BEGIN { 27 | $ENV{PERL_JSON_BACKEND} = 'JSON::PP'; 28 | undef $ENV{LANGUAGE}; 29 | } 30 | 31 | # Enable immediate flush to stdout and stderr 32 | $|++; 33 | 34 | ### 35 | ### More global variables, and initialization. 36 | ### 37 | 38 | my $pidfile; 39 | my $user; 40 | my $group; 41 | my $logfile; 42 | my $loglevel; 43 | my $logjson; 44 | my $opt_outfile; 45 | my $opt_help; 46 | GetOptions( 47 | 'help!' => \$opt_help, 48 | 'pidfile=s' => \$pidfile, 49 | 'user=s' => \$user, 50 | 'group=s' => \$group, 51 | 'logfile=s' => \$logfile, 52 | 'loglevel=s' => \$loglevel, 53 | 'logjson!' => \$logjson, 54 | 'outfile=s' => \$opt_outfile, 55 | ) or pod2usage( "Try '$0 --help' for more information." ); 56 | 57 | pod2usage( -verbose => 1 ) if $opt_help; 58 | 59 | $pidfile //= '/tmp/zonemaster_backend_testagent.pid'; 60 | $logfile //= '/var/log/zonemaster/zonemaster_backend_testagent.log'; 61 | $opt_outfile //= '/var/log/zonemaster/zonemaster_backend_testagent.out'; 62 | $loglevel //= 'info'; 63 | $loglevel = lc $loglevel; 64 | 65 | Log::Any::Adapter->set( 66 | '+Zonemaster::Backend::Log', 67 | log_level => $loglevel, 68 | json => $logjson, 69 | file => $logfile, 70 | ); 71 | 72 | $SIG{__WARN__} = sub { 73 | $log->warning(map s/^\s+|\s+$//gr, map s/\n/ /gr, @_); 74 | }; 75 | 76 | ### 77 | ### Actual functionality 78 | ### 79 | 80 | sub main { 81 | my $self = shift; 82 | 83 | my $caught_sigterm = 0; 84 | my $catch_sigterm; 85 | $catch_sigterm = sub { 86 | $SIG{TERM} = $catch_sigterm; 87 | $caught_sigterm = 1; 88 | $log->notice( "Daemon caught SIGTERM" ); 89 | return; 90 | }; 91 | local $SIG{TERM} = $catch_sigterm; 92 | 93 | my $agent = Zonemaster::Backend::TestAgent->new( { config => $self->config } ); 94 | 95 | while ( !$caught_sigterm ) { 96 | my $cleanup_timer = [ gettimeofday ]; 97 | 98 | $self->pm->reap_finished_children(); # Reaps terminated child processes 99 | $self->pm->on_wait(); # Sends SIGKILL to overdue child processes 100 | 101 | Zonemaster::Backend::Metrics::gauge("zonemaster.testagent.maximum_processes", $self->pm->max_procs); 102 | Zonemaster::Backend::Metrics::gauge("zonemaster.testagent.running_processes", scalar($self->pm->running_procs)); 103 | 104 | Zonemaster::Backend::Metrics::timing("zonemaster.testagent.cleanup_duration_seconds", tv_interval($cleanup_timer) * 1000); 105 | 106 | my $fetch_test_timer = [ gettimeofday ]; 107 | 108 | my ( $test_id, $batch_id ); 109 | eval { 110 | $self->db->process_unfinished_tests( 111 | $self->config->ZONEMASTER_lock_on_queue, 112 | $self->config->ZONEMASTER_max_zonemaster_execution_time, 113 | ); 114 | 115 | ( $test_id, $batch_id ) = $self->db->get_test_request( $self->config->ZONEMASTER_lock_on_queue ); 116 | 117 | Zonemaster::Backend::Metrics::timing("zonemaster.testagent.fetchtests_duration_seconds", tv_interval($fetch_test_timer) * 1000); 118 | }; 119 | if ( $@ ) { 120 | $log->error( $@ ); 121 | } 122 | 123 | my $show_progress = defined $batch_id ? 0 : 1; 124 | 125 | if ( $test_id ) { 126 | $log->infof( "Test found: %s", $test_id ); 127 | if ( $self->pm->start( $test_id ) == 0 ) { # Forks off child process 128 | $log->infof( "Test starting: %s", $test_id ); 129 | Zonemaster::Backend::Metrics::increment("zonemaster.testagent.tests_started"); 130 | my $start_time = [ gettimeofday ]; 131 | eval { $agent->run( $test_id, $show_progress ) }; 132 | if ( $@ ) { 133 | chomp $@; 134 | Zonemaster::Backend::Metrics::increment("zonemaster.testagent.tests_died"); 135 | $log->errorf( "Test died: %s: %s", $test_id, $@ ); 136 | $self->db->process_dead_test( $test_id ) 137 | } 138 | else { 139 | Zonemaster::Backend::Metrics::increment("zonemaster.testagent.tests_completed"); 140 | $log->infof( "Test completed: %s", $test_id ); 141 | } 142 | Zonemaster::Backend::Metrics::timing("zonemaster.testagent.tests_duration_seconds", tv_interval($start_time) * 1000); 143 | $agent->reset(); 144 | $self->pm->finish; # Terminates child process 145 | } 146 | } 147 | else { 148 | sleep $self->config->DB_polling_interval; 149 | } 150 | } 151 | 152 | $log->notice( "Daemon entered graceful shutdown" ); 153 | 154 | $self->pm->wait_all_children(); # Includes SIGKILLing overdue child processes 155 | 156 | return; 157 | } 158 | 159 | sub preflight_checks { 160 | # Make sure we can load the configuration file 161 | $log->debug("Starting pre-flight check"); 162 | my $initial_config = Zonemaster::Backend::Config->load_config(); 163 | 164 | Zonemaster::Backend::Metrics->setup($initial_config->METRICS_statsd_host, $initial_config->METRICS_statsd_port); 165 | 166 | # Validate the Zonemaster-Engine profile 167 | Zonemaster::Backend::TestAgent->new( { config => $initial_config } ); 168 | 169 | # Connect to the database 170 | $initial_config->new_DB(); 171 | $log->debug("Completed pre-flight check"); 172 | 173 | return $initial_config; 174 | } 175 | 176 | 177 | 178 | my $initial_config; 179 | 180 | # Make sure the environment is alright before forking (only on startup) 181 | if ( grep /^foreground$|^restart$|^start$/, @ARGV ) { 182 | eval { 183 | $initial_config = preflight_checks(); 184 | }; 185 | if ( $@ ) { 186 | $log->critical( "Aborting startup: $@" ); 187 | print STDERR "Aborting startup: $@"; 188 | exit 1; 189 | } 190 | } 191 | 192 | ### 193 | ### Daemon Control stuff. 194 | ### 195 | 196 | my $daemon = Daemon::Control->with_plugins( qw( +Zonemaster::Backend::Config::DCPlugin ) )->new( 197 | { 198 | name => 'zonemaster-testagent', 199 | program => sub { 200 | my $self = shift; 201 | $log->notice( "Daemon spawned" ); 202 | 203 | $self->init_backend_config( $initial_config ); 204 | undef $initial_config; 205 | 206 | eval { main( $self ) }; 207 | if ( $@ ) { 208 | chomp $@; 209 | $log->critical( $@ ); 210 | } 211 | $log->notice( "Daemon terminating" ); 212 | }, 213 | pid_file => $pidfile, 214 | stderr_file => $opt_outfile, 215 | stdout_file => $opt_outfile, 216 | } 217 | ); 218 | 219 | $daemon->init_config( $ENV{PERLBREW_ROOT} . '/etc/bashrc' ) if ( $ENV{PERLBREW_ROOT} ); 220 | $daemon->user($user) if $user; 221 | $daemon->group($group) if $group; 222 | 223 | exit $daemon->run; 224 | 225 | =head1 NAME 226 | 227 | zonemaster_backend_testagent - Init script for Zonemaster Test Agent. 228 | 229 | =head1 SYNOPSIS 230 | 231 | zonemaster_backend_testagent [OPTIONS] [COMMAND] 232 | 233 | =head1 OPTIONS 234 | 235 | =over 4 236 | 237 | =item B<--help> 238 | 239 | Print a brief help message and exits. 240 | 241 | =item B<--user=USER> 242 | 243 | When specified the daemon will drop to the user with this username when forked. 244 | 245 | =item B<--group=GROUP> 246 | 247 | When specified the daemon will drop to the group with this groupname when forked. 248 | 249 | =item B<--pidfile=FILE> 250 | 251 | The location of the PID file to use. 252 | 253 | =item B<--logfile=FILE> 254 | 255 | The location of the log file to use. 256 | 257 | When FILE is -, the log is written to standard output. 258 | 259 | =item B<--loglevel=LEVEL> 260 | 261 | The location of the log level to use. 262 | 263 | The allowed values are specified at L. 264 | 265 | =item B<--logjson> 266 | 267 | Enable JSON logging when specified. 268 | 269 | =item B 270 | 271 | One of the following: 272 | 273 | =over 4 274 | 275 | =item start 276 | 277 | =item foreground 278 | 279 | =item stop 280 | 281 | =item restart 282 | 283 | =item reload 284 | 285 | =item status 286 | 287 | =item get_init_file 288 | 289 | =back 290 | 291 | =back 292 | 293 | =cut 294 | -------------------------------------------------------------------------------- /t/idn.data: -------------------------------------------------------------------------------- 1 | i.root-servers.net 192.36.148.17 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"answerfrom":"192.36.148.17","querytime":10,"timestamp":1646935543.48935,"data":"+U2EAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA=="}}}} 2 | i.root-servers.net 2001:07fe:0000:0000:0000:0000:0000:0053 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"data":"zQCEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","timestamp":1646935543.51238,"answerfrom":"2001:7fe::53","querytime":9}}}} 3 | d.root-servers.net 199.7.91.13 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"timestamp":1646935543.15536,"data":"QraEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","querytime":14,"answerfrom":"199.7.91.13"}}}} 4 | d.root-servers.net 2001:0500:002d:0000:0000:0000:0000:000d {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"querytime":10,"answerfrom":"2001:500:2d::d","timestamp":1646935543.18271,"data":"nQOEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA=="}}}} 5 | c.root-servers.net 2001:0500:0002:0000:0000:0000:0000:000c {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"data":"7VmEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","timestamp":1646935543.1185,"answerfrom":"2001:500:2::c","querytime":23}}}} 6 | c.root-servers.net 192.33.4.12 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"querytime":24,"answerfrom":"192.33.4.12","timestamp":1646935543.08159,"data":"+EmEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA=="}}}} 7 | j.root-servers.net 2001:0503:0c27:0000:0000:0000:0002:0030 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"timestamp":1646935543.54733,"data":"bdyEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","answerfrom":"2001:503:c27::2:30","querytime":10}}}} 8 | j.root-servers.net 192.58.128.30 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"timestamp":1646935543.53484,"data":"8oWEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","answerfrom":"192.58.128.30","querytime":2}}}} 9 | b.root-servers.net 2001:0500:0200:0000:0000:0000:0000:000b {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"answerfrom":"2001:500:200::b","querytime":13,"data":"rtKEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","timestamp":1646935543.05494}}}} 10 | b.root-servers.net 199.9.14.201 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"data":"eSqEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","timestamp":1646935543.02769,"querytime":14,"answerfrom":"199.9.14.201"}}}} 11 | l.root-servers.net 2001:0500:009f:0000:0000:0000:0000:0042 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"data":"3tWEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","timestamp":1646935543.63711,"answerfrom":"2001:500:9f::42","querytime":10}}}} 12 | l.root-servers.net 199.7.83.42 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"timestamp":1646935543.61372,"data":"LXeEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","querytime":10,"answerfrom":"199.7.83.42"}}}} 13 | m.root-servers.net 2001:0dc3:0000:0000:0000:0000:0000:0035 {"WcLt/i2sUcZA//eE56F52g":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"querytime":3,"answerfrom":"2001:dc3::35","timestamp":1646935543.86015,"data":"LDiEAwABAAAAAQAAB2V4YW1wbGUAAAYAAQAABgABAAAAAABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA=="}}},"5e42wXPdot60bOvWyxbkKQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"timestamp":1646935543.85123,"data":"ysiEAwABAAAAAQAAD3huLS1hbnRoci12cmE3agdleGFtcGxlAAABAAEAAAYAAQABUYAAQAFhDHJvb3Qtc2VydmVycwNuZXQABW5zdGxkDHZlcmlzaWduLWdycwNjb20AeIW+mQAABwgAAAOEAAk6gAABUYA=","querytime":3,"answerfrom":"2001:dc3::35"}}},"Su5WLHq4snuuB/mBxXyF0Q":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"timestamp":1646935542.94235,"data":"mhKEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAYAAQAABgABAAAAAABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","querytime":3,"answerfrom":"2001:dc3::35"}}},"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"querytime":3,"answerfrom":"2001:dc3::35","data":"k96EAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","timestamp":1646935543.66816}}}} 14 | m.root-servers.net 202.12.27.33 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"querytime":3,"answerfrom":"202.12.27.33","timestamp":1646935543.65873,"data":"yH6EAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA=="}}}} 15 | k.root-servers.net 2001:07fd:0000:0000:0000:0000:0000:0001 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"querytime":8,"answerfrom":"2001:7fd::1","data":"oQmEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","timestamp":1646935543.59331}}}} 16 | k.root-servers.net 193.0.14.129 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"querytime":12,"answerfrom":"193.0.14.129","data":"pCmEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","timestamp":1646935543.56745}}}} 17 | g.root-servers.net 2001:0500:0012:0000:0000:0000:0000:0d0d {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"querytime":55,"answerfrom":"2001:500:12::d0d","data":"NNGEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","timestamp":1646935543.35495}}}} 18 | g.root-servers.net 192.112.36.4 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"timestamp":1646935543.26682,"data":"6cSEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","querytime":75,"answerfrom":"192.112.36.4"}}}} 19 | a.root-servers.net 198.41.0.4 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"timestamp":1646935542.98134,"data":"ZYGEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","answerfrom":"198.41.0.4","querytime":10}}}} 20 | a.root-servers.net 2001:0503:ba3e:0000:0000:0000:0002:0030 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"data":"16WEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","timestamp":1646935543.00459,"querytime":10,"answerfrom":"2001:503:ba3e::2:30"}}}} 21 | f.root-servers.net 2001:0500:002f:0000:0000:0000:0000:000f {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"data":"P+2EAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","timestamp":1646935543.25261,"answerfrom":"2001:500:2f::f","querytime":4}}}} 22 | f.root-servers.net 192.5.5.241 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"timestamp":1646935543.23842,"data":"VuuEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","querytime":3,"answerfrom":"192.5.5.241"}}}} 23 | h.root-servers.net 2001:0500:0001:0000:0000:0000:0000:0053 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"answerfrom":"2001:500:1::53","querytime":9,"timestamp":1646935543.46742,"data":"NyuEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA=="}}}} 24 | h.root-servers.net 198.97.190.53 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"data":"zsqEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","timestamp":1646935543.4237,"answerfrom":"198.97.190.53","querytime":31}}}} 25 | e.root-servers.net 192.203.230.10 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"querytime":3,"answerfrom":"192.203.230.10","timestamp":1646935543.20502,"data":"C8CEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA=="}}}} 26 | e.root-servers.net 2001:0500:00a8:0000:0000:0000:0000:000e {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"data":"O0+EAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","timestamp":1646935543.21714,"querytime":9,"answerfrom":"2001:500:a8::e"}}}} 27 | -------------------------------------------------------------------------------- /t/db.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use utf8; 5 | use Encode; 6 | use Test::More; # see done_testing() 7 | 8 | use_ok( 'Zonemaster::Backend::DB' ); 9 | 10 | sub encode_and_fingerprint { 11 | my $params = shift; 12 | 13 | my $self = "Zonemaster::Backend::DB"; 14 | my $encoded_params = $self->encode_params( $params ); 15 | my $fingerprint = $self->generate_fingerprint( $params ); 16 | 17 | return ( $encoded_params, $fingerprint ); 18 | } 19 | 20 | subtest 'encoding and fingerprint' => sub { 21 | 22 | subtest 'missing properties' => sub { 23 | my %params = ( domain => "example.com" ); 24 | 25 | my $expected_encoded_params = '{"domain":"example.com","ds_info":[],"ipv4":null,"ipv6":null,"nameservers":[],"profile":"default"}'; 26 | my ( $encoded_params, $fingerprint ) = encode_and_fingerprint( \%params ); 27 | is $encoded_params, $expected_encoded_params, 'domain only: the encoded strings should match'; 28 | #diag ($fingerprint); 29 | 30 | my $expected_encoded_params_v4_true = '{"domain":"example.com","ds_info":[],"ipv4":true,"ipv6":null,"nameservers":[],"profile":"default"}'; 31 | $params{ipv4} = JSON::PP->true; 32 | my ( $encoded_params_ipv4, $fingerprint_ipv4 ) = encode_and_fingerprint( \%params ); 33 | is $encoded_params_ipv4, $expected_encoded_params_v4_true, 'add ipv4: the encoded strings should match'; 34 | isnt $fingerprint_ipv4, $fingerprint, 'fingerprints should not match'; 35 | }; 36 | 37 | subtest 'array properties' => sub { 38 | subtest 'ds_info' => sub { 39 | my %params1 = ( 40 | domain => "example.com", 41 | ds_info => [{ 42 | algorithm => 8, 43 | keytag => 11627, 44 | digtype => 2, 45 | digest => "a6cca9e6027ecc80ba0f6d747923127f1d69005fe4f0ec0461bd633482595448" 46 | }] 47 | ); 48 | my %params2 = ( 49 | ds_info => [{ 50 | digtype => 2, 51 | algorithm => 8, 52 | keytag => 11627, 53 | digest => "a6cca9e6027ecc80ba0f6d747923127f1d69005fe4f0ec0461bd633482595448" 54 | }], 55 | domain => "example.com" 56 | ); 57 | my ( $encoded_params1, $fingerprint1 ) = encode_and_fingerprint( \%params1 ); 58 | my ( $encoded_params2, $fingerprint2 ) = encode_and_fingerprint( \%params2 ); 59 | is $fingerprint1, $fingerprint2, 'ds_info same fingerprint'; 60 | is $encoded_params1, $encoded_params2, 'ds_info same encoded string'; 61 | }; 62 | 63 | subtest 'nameservers order' => sub { 64 | my %params1 = ( 65 | domain => "example.com", 66 | nameservers => [ 67 | { ns => "ns2.nic.fr", ip => "192.134.4.1" }, 68 | { ns => "ns1.nic.fr" }, 69 | { ip => "192.0.2.1", ns => "ns3.nic.fr"} 70 | ] 71 | ); 72 | my %params2 = ( 73 | nameservers => [ 74 | { ns => "ns3.nic.fr", ip => "192.0.2.1" }, 75 | { ns => "ns1.nic.fr" }, 76 | { ip => "192.134.4.1", ns => "ns2.nic.fr"} 77 | ], 78 | domain => "example.com" 79 | ); 80 | my %params3 = ( 81 | domain => "example.com", 82 | nameservers => [ 83 | { ip => "", ns => "ns1.nic.fr" }, 84 | { ns => "ns3.nic.FR", ip => "192.0.2.1" }, 85 | { ns => "ns2.nic.fr", ip => "192.134.4.1" } 86 | ] 87 | ); 88 | my %params4 = ( 89 | domain => "example.com", 90 | nameservers => [ 91 | { ip => "192.134.4.1", ns => "nS2.Nic.FR"}, 92 | { ns => "Ns1.nIC.fR", ip => "" }, 93 | { ns => "ns3.nic.fr", ip => "192.0.2.1" } 94 | ] 95 | ); 96 | 97 | my ( $encoded_params1, $fingerprint1 ) = encode_and_fingerprint( \%params1 ); 98 | my ( $encoded_params2, $fingerprint2 ) = encode_and_fingerprint( \%params2 ); 99 | my ( $encoded_params3, $fingerprint3 ) = encode_and_fingerprint( \%params3 ); 100 | my ( $encoded_params4, $fingerprint4 ) = encode_and_fingerprint( \%params4 ); 101 | 102 | is $fingerprint1, $fingerprint2, 'nameservers: same fingerprint'; 103 | is $encoded_params1, $encoded_params2, 'nameservers: same encoded string'; 104 | 105 | is $fingerprint1, $fingerprint3, 'nameservers: same fingerprint (empty ip)'; 106 | is $encoded_params1, $encoded_params3, 'nameservers: same encoded string (empty ip)'; 107 | 108 | is $fingerprint1, $fingerprint4, 'nameservers: same fingerprint (ignore nameservers\' ns case)'; 109 | is $encoded_params1, $encoded_params4, 'nameservers: same encoded string (ignore nameservers\' ns case)'; 110 | }; 111 | }; 112 | 113 | subtest 'should be case insensitive' => sub { 114 | my %params1 = ( domain => "example.com" ); 115 | my %params2 = ( domain => "eXamPLe.COm" ); 116 | 117 | my ( $encoded_params1, $fingerprint1 ) = encode_and_fingerprint( \%params1 ); 118 | my ( $encoded_params2, $fingerprint2 ) = encode_and_fingerprint( \%params2 ); 119 | is $fingerprint1, $fingerprint2, 'same fingerprint'; 120 | is $encoded_params1, $encoded_params2, 'same encoded string'; 121 | }; 122 | 123 | subtest 'garbage properties set' => sub { 124 | my $expected_encoded_params = '{"client":"GUI v3.3.0","domain":"example.com","ds_info":[],"ipv4":null,"ipv6":null,"nameservers":[],"profile":"default"}'; 125 | my %params1 = ( 126 | domain => "example.com", 127 | ); 128 | my %params2 = ( 129 | domain => "example.com", 130 | client => "GUI v3.3.0" 131 | ); 132 | my ( $encoded_params1, $fingerprint1 ) = encode_and_fingerprint( \%params1 ); 133 | my ( $encoded_params2, $fingerprint2 ) = encode_and_fingerprint( \%params2 ); 134 | 135 | is $fingerprint1, $fingerprint2, 'leave out garbage property in fingerprint computation...'; 136 | is $encoded_params2, $expected_encoded_params, '...but keep it in the encoded string'; 137 | }; 138 | 139 | subtest 'should have different fingerprints' => sub { 140 | subtest 'different profiles' => sub { 141 | my %params1 = ( 142 | domain => "example.com", 143 | profile => "profile_1" 144 | ); 145 | my %params2 = ( 146 | domain => "example.com", 147 | profile => "profile_2" 148 | ); 149 | my ( undef, $fingerprint1 ) = encode_and_fingerprint( \%params1 ); 150 | my ( undef, $fingerprint2 ) = encode_and_fingerprint( \%params2 ); 151 | 152 | isnt $fingerprint1, $fingerprint2, 'different profiles, different fingerprints'; 153 | }; 154 | subtest 'different IP protocols' => sub { 155 | my %params1 = ( 156 | domain => "example.com", 157 | ipv4 => "true", 158 | ipv6 => "false" 159 | ); 160 | my %params2 = ( 161 | domain => "example.com", 162 | ipv4 => "false", 163 | ipv6 => "true" 164 | ); 165 | my ( undef, $fingerprint1 ) = encode_and_fingerprint( \%params1 ); 166 | my ( undef, $fingerprint2 ) = encode_and_fingerprint( \%params2 ); 167 | 168 | isnt $fingerprint1, $fingerprint2, 'different IP protocols, different fingerprints'; 169 | }; 170 | }; 171 | 172 | subtest 'IDN domain' => sub { 173 | my $expected_encoded_params = encode_utf8( '{"domain":"xn--caf-dma.example","ds_info":[],"ipv4":true,"ipv6":true,"nameservers":[],"profile":"default"}' ); 174 | my $expected_fingerprint = '8cb027ff2c175f48aed2623abad0cdd2'; 175 | 176 | my %params = ( domain => "café.example" ); 177 | $params{ipv4} = JSON::PP->true; 178 | $params{ipv6} = JSON::PP->true; 179 | 180 | my ( $encoded_params, $fingerprint ) = encode_and_fingerprint( \%params ); 181 | is $encoded_params, $expected_encoded_params, 'IDN domain: the encoded strings should match'; 182 | is $fingerprint, $expected_fingerprint, 'IDN domain: correct fingerprint'; 183 | }; 184 | 185 | subtest 'final dots' => sub { 186 | subtest 'in domain' => sub { 187 | my %params1 = ( domain => "example.com" ); 188 | my %params2 = ( domain => "example.com." ); 189 | my $expected_encoded_params = encode_utf8( '{"domain":"example.com","ds_info":[],"ipv4":null,"ipv6":null,"nameservers":[],"profile":"default"}' ); 190 | 191 | my ( $encoded_params1, $fingerprint1 ) = encode_and_fingerprint( \%params1 ); 192 | my ( $encoded_params2, $fingerprint2 ) = encode_and_fingerprint( \%params2 ); 193 | is $fingerprint1, $fingerprint2, 'same fingerprint'; 194 | is $encoded_params1, $expected_encoded_params, 'the encoded strings should match'; 195 | 196 | }; 197 | 198 | subtest 'in nameserver' => sub { 199 | my %params1 = ( domain => "example.com", nameservers => [ { ns => "ns1.example.com." } ] ); 200 | my %params2 = ( domain => "example.com", nameservers => [ { ns => "ns1.example.com" } ] ); 201 | my $expected_encoded_params = encode_utf8( '{"domain":"example.com","ds_info":[],"ipv4":null,"ipv6":null,"nameservers":[{"ns":"ns1.example.com"}],"profile":"default"}' ); 202 | 203 | my ( $encoded_params1, $fingerprint1 ) = encode_and_fingerprint( \%params1 ); 204 | my ( $encoded_params2, $fingerprint2 ) = encode_and_fingerprint( \%params2 ); 205 | is $fingerprint1, $fingerprint2, 'same fingerprint'; 206 | is $encoded_params1, $expected_encoded_params, 'the encoded strings should match'; 207 | 208 | }; 209 | 210 | subtest 'root is not modified' => sub { 211 | my %params = ( domain => "." ); 212 | my $expected_encoded_params = encode_utf8( '{"domain":".","ds_info":[],"ipv4":null,"ipv6":null,"nameservers":[],"profile":"default"}' ); 213 | 214 | my ( $encoded_params, $fingerprint ) = encode_and_fingerprint( \%params ); 215 | is $encoded_params, $expected_encoded_params, 'the encoded strings should match'; 216 | 217 | }; 218 | }; 219 | }; 220 | 221 | done_testing(); 222 | -------------------------------------------------------------------------------- /lib/Zonemaster/Backend/DB/SQLite.pm: -------------------------------------------------------------------------------- 1 | package Zonemaster::Backend::DB::SQLite; 2 | 3 | our $VERSION = '1.1.0'; 4 | 5 | use Moose; 6 | use 5.14.2; 7 | 8 | use DBI qw(:utils); 9 | use Digest::MD5 qw(md5_hex); 10 | use JSON::PP; 11 | 12 | 13 | use Zonemaster::Backend::Errors; 14 | 15 | with 'Zonemaster::Backend::DB'; 16 | 17 | =head1 CLASS METHODS 18 | 19 | =head2 from_config 20 | 21 | Construct a new instance from a Zonemaster::Backend::Config. 22 | 23 | my $db = Zonemaster::Backend::DB::SQLite->from_config( $config ); 24 | 25 | =cut 26 | 27 | sub from_config { 28 | my ( $class, $config ) = @_; 29 | 30 | my $file = $config->SQLITE_database_file; 31 | 32 | my $data_source_name = "DBI:SQLite:dbname=$file"; 33 | 34 | return $class->new( 35 | { 36 | data_source_name => $data_source_name, 37 | user => '', 38 | password => '', 39 | dbhandle => undef, 40 | } 41 | ); 42 | } 43 | 44 | sub DEMOLISH { 45 | my ( $self ) = @_; 46 | $self->dbh->disconnect() if defined $self->dbhandle && $self->dbhandle->ping; 47 | } 48 | 49 | sub get_dbh_specific_attributes { 50 | return { sqlite_extended_result_codes => 1 }; 51 | } 52 | 53 | sub create_schema { 54 | my ( $self ) = @_; 55 | 56 | my $dbh = $self->dbh; 57 | 58 | # enable FOREIGN KEY support 59 | $dbh->do( 'PRAGMA foreign_keys = ON;' ); 60 | 61 | #################################################################### 62 | # TEST RESULTS 63 | #################################################################### 64 | $dbh->do( 65 | 'CREATE TABLE IF NOT EXISTS test_results ( 66 | id integer PRIMARY KEY AUTOINCREMENT, 67 | hash_id VARCHAR(16) NOT NULL, 68 | domain VARCHAR(255) NOT NULL, 69 | batch_id integer NULL, 70 | created_at DATETIME NOT NULL, 71 | started_at DATETIME DEFAULT NULL, 72 | ended_at DATETIME DEFAULT NULL, 73 | priority integer DEFAULT 10, 74 | queue integer DEFAULT 0, 75 | progress integer DEFAULT 0, 76 | fingerprint character varying(32), 77 | params text NOT NULL, 78 | results text DEFAULT NULL, 79 | undelegated boolean NOT NULL DEFAULT false, 80 | 81 | UNIQUE (hash_id) 82 | ) 83 | ' 84 | ) or die Zonemaster::Backend::Error::Internal->new( reason => "SQLite error, could not create 'test_results' table", data => $dbh->errstr() ); 85 | 86 | $dbh->do( 87 | 'CREATE INDEX IF NOT EXISTS test_results__hash_id ON test_results (hash_id)' 88 | ); 89 | $self->dbh->do( 90 | 'CREATE INDEX IF NOT EXISTS test_results__fingerprint ON test_results (fingerprint)' 91 | ); 92 | $dbh->do( 93 | 'CREATE INDEX IF NOT EXISTS test_results__batch_id_progress ON test_results (batch_id, progress)' 94 | ); 95 | $dbh->do( 96 | 'CREATE INDEX IF NOT EXISTS test_results__progress ON test_results (progress)' 97 | ); 98 | $dbh->do( 99 | 'CREATE INDEX IF NOT EXISTS test_results__domain_undelegated ON test_results (domain, undelegated)' 100 | ); 101 | 102 | #################################################################### 103 | # LOG LEVEL 104 | #################################################################### 105 | $dbh->do( 106 | "CREATE TABLE IF NOT EXISTS log_level ( 107 | value INTEGER, 108 | level VARCHAR(15), 109 | 110 | UNIQUE (value) 111 | ) 112 | " 113 | ) or die Zonemaster::Backend::Error::Internal->new( reason => "SQLite error, could not create 'log_level' table", data => $dbh->errstr() ); 114 | 115 | my ( $c ) = $dbh->selectrow_array( "SELECT count(*) FROM log_level" ); 116 | if ( $c == 0 ) { 117 | $dbh->do( 118 | "INSERT INTO log_level (value, level) 119 | VALUES 120 | (-2, 'DEBUG3'), 121 | (-1, 'DEBUG2'), 122 | ( 0, 'DEBUG'), 123 | ( 1, 'INFO'), 124 | ( 2, 'NOTICE'), 125 | ( 3, 'WARNING'), 126 | ( 4, 'ERROR'), 127 | ( 5, 'CRITICAL') 128 | " 129 | ); 130 | } 131 | 132 | #################################################################### 133 | # RESULT ENTRIES 134 | #################################################################### 135 | $dbh->do( 136 | 'CREATE TABLE IF NOT EXISTS result_entries ( 137 | hash_id VARCHAR(16) NOT NULL, 138 | level INT NOT NULL, 139 | module VARCHAR(255) NOT NULL, 140 | testcase VARCHAR(255) NOT NULL, 141 | tag VARCHAR(255) NOT NULL, 142 | timestamp REAL NOT NULL, 143 | args BLOB NOT NULL, 144 | 145 | FOREIGN KEY(hash_id) REFERENCES test_results(hash_id), 146 | FOREIGN KEY(level) REFERENCES log_level(value) 147 | ) 148 | ' 149 | ) or die Zonemaster::Backend::Error::Internal->new( reason => "SQLite error, could not create 'result_entries' table", data => $dbh->errstr() ); 150 | 151 | $dbh->do( 152 | 'CREATE INDEX IF NOT EXISTS result_entries__hash_id ON result_entries (hash_id)' 153 | ); 154 | 155 | $dbh->do( 156 | 'CREATE INDEX IF NOT EXISTS result_entries__level ON result_entries (level)' 157 | ); 158 | 159 | #################################################################### 160 | # BATCH JOBS 161 | #################################################################### 162 | $dbh->do( 163 | 'CREATE TABLE IF NOT EXISTS batch_jobs ( 164 | id integer PRIMARY KEY, 165 | username character varying(50) NOT NULL, 166 | created_at DATETIME NOT NULL 167 | ) 168 | ' 169 | ) or die Zonemaster::Backend::Error::Internal->new( reason => "SQLite error, could not create 'batch_jobs' table", data => $dbh->errstr() ); 170 | 171 | 172 | #################################################################### 173 | # USERS 174 | #################################################################### 175 | $dbh->do( 176 | 'CREATE TABLE IF NOT EXISTS users ( 177 | id INTEGER PRIMARY KEY AUTOINCREMENT, 178 | username varchar(128), 179 | api_key varchar(512), 180 | 181 | UNIQUE (username) 182 | ) 183 | ' 184 | ) or die Zonemaster::Backend::Error::Internal->new( reason => "SQLite error, could not create 'users' table", data => $dbh->errstr() ); 185 | 186 | return; 187 | } 188 | 189 | =head2 drop_tables 190 | 191 | Drop all the tables if they exist. 192 | 193 | =cut 194 | 195 | sub drop_tables { 196 | my ( $self ) = @_; 197 | 198 | $self->dbh->do( "DROP TABLE IF EXISTS test_results" ); 199 | $self->dbh->do( "DROP TABLE IF EXISTS result_entries" ); 200 | $self->dbh->do( "DROP TABLE IF EXISTS log_level" ); 201 | $self->dbh->do( "DROP TABLE IF EXISTS users" ); 202 | $self->dbh->do( "DROP TABLE IF EXISTS batch_jobs" ); 203 | 204 | return; 205 | } 206 | 207 | sub add_batch_job { 208 | my ( $self, $params ) = @_; 209 | my $batch_id; 210 | 211 | my $dbh = $self->dbh; 212 | 213 | if ( $self->user_authorized( $params->{username}, $params->{api_key} ) ) { 214 | $batch_id = $self->create_new_batch_job( $params->{username} ); 215 | 216 | my $test_params = $params->{test_params}; 217 | my $priority = $test_params->{priority}; 218 | my $queue_label = $test_params->{queue}; 219 | 220 | $dbh->{AutoCommit} = 0; 221 | eval {$dbh->do( "DROP INDEX IF EXISTS test_results__hash_id " );}; 222 | eval {$dbh->do( "DROP INDEX IF EXISTS test_results__fingerprint " );}; 223 | eval {$dbh->do( "DROP INDEX IF EXISTS test_results__batch_id_progress " );}; 224 | eval {$dbh->do( "DROP INDEX IF EXISTS test_results__progress " );}; 225 | eval {$dbh->do( "DROP INDEX IF EXISTS test_results__domain_undelegated " );}; 226 | 227 | my $sth = $dbh->prepare( ' 228 | INSERT INTO test_results ( 229 | hash_id, 230 | domain, 231 | batch_id, 232 | created_at, 233 | priority, 234 | queue, 235 | fingerprint, 236 | params, 237 | undelegated 238 | ) VALUES (?,?,?,?,?,?,?,?,?)' 239 | ); 240 | foreach my $domain ( @{$params->{domains}} ) { 241 | $test_params->{domain} = _normalize_domain( $domain ); 242 | 243 | my $fingerprint = $self->generate_fingerprint( $test_params ); 244 | my $encoded_params = $self->encode_params( $test_params ); 245 | my $undelegated = $self->undelegated ( $test_params ); 246 | 247 | my $hash_id = substr(md5_hex(time().rand()), 0, 16); 248 | $sth->execute( 249 | $hash_id, 250 | $test_params->{domain}, 251 | $batch_id, 252 | $self->format_time( time() ), 253 | $priority, 254 | $queue_label, 255 | $fingerprint, 256 | $encoded_params, 257 | $undelegated, 258 | ); 259 | } 260 | $dbh->do( "CREATE INDEX test_results__hash_id ON test_results (hash_id, created_at)" ); 261 | $dbh->do( "CREATE INDEX test_results__fingerprint ON test_results (fingerprint)" ); 262 | $dbh->do( "CREATE INDEX test_results__batch_id_progress ON test_results (batch_id, progress)" ); 263 | $dbh->do( "CREATE INDEX test_results__progress ON test_results (progress)" ); 264 | $dbh->do( "CREATE INDEX test_results__domain_undelegated ON test_results (domain, undelegated)" ); 265 | 266 | $dbh->commit(); 267 | $dbh->{AutoCommit} = 1; 268 | } 269 | else { 270 | die Zonemaster::Backend::Error::PermissionDenied->new( message => 'User not authorized to use batch mode', data => { username => $params->{username}} ); 271 | } 272 | 273 | return $batch_id; 274 | } 275 | 276 | sub get_relative_start_time { 277 | my ( $self, $hash_id ) = @_; 278 | 279 | return $self->dbh->selectrow_array( 280 | q[ 281 | SELECT (julianday(?) - julianday(started_at)) * 3600 * 24 282 | FROM test_results 283 | WHERE hash_id = ? 284 | ], 285 | undef, 286 | $self->format_time( time() ), 287 | $hash_id, 288 | ); 289 | } 290 | 291 | sub is_duplicate { 292 | my ( $self ) = @_; 293 | 294 | # for the list of codes see: https://sqlite.org/rescode.html 295 | return ( $self->dbh->err == 2067 ); 296 | } 297 | 298 | no Moose; 299 | __PACKAGE__->meta()->make_immutable(); 300 | 301 | 1; 302 | --------------------------------------------------------------------------------