feat: add full Zonemaster stack with Docker and Spanish UI

- Clone all 5 Zonemaster component repos (LDNS, Engine, CLI, Backend, GUI)
- Dockerfile.backend: 8-stage multi-stage build LDNS→Engine→CLI→Backend
- Dockerfile.gui: Astro static build served via nginx
- docker-compose.yml: backend (internal) + frontend (port 5353)
- nginx.conf: root redirects to /es/, /api/ proxied to backend
- zonemaster-gui/config.ts: defaultLanguage set to 'es' (Spanish)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-21 08:19:24 +02:00
commit 8d4eaa1489
1567 changed files with 204155 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
## Purpose
This PR...
## Context
(e.g. Fixes #9999, Follow-up to #9999, etc.)
## Changes
...
## How to test this PR
...

View File

@@ -0,0 +1,138 @@
---
name: CI
on:
push:
branches:
- develop
- master
- 'release/**'
pull_request:
branches:
- develop
- master
- 'release/**'
env:
ZONEMASTER_RECORD: 0
compatibility: develop
# compatibility: latest
jobs:
run-tests:
strategy:
matrix:
db: [sqlite, mysql, postgresql]
perl: ['5.40']
include:
- db: sqlite
perl: '5.36'
- db: sqlite
perl: '5.26'
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- uses: shogo82148/actions-setup-perl@v1
with:
perl-version: ${{ matrix.perl }}
- name: Install binary dependencies
run: |
# * These were taken from the installation instruction.
# * Gettext was added so we can run cpanm . on the Engine sources.
# * The Perl modules were left out because I couldn't get all of them
# to work with custom Perl versions.
# * Cpanminus was left out because actions-setup-perl installs it.
sudo apt-get install -y \
autoconf \
automake \
build-essential \
gettext \
libidn2-dev \
libssl-dev \
libtool \
m4 \
- name: "Install development versions of Zonemaster::LDNS and Zonemaster::Engine"
if: ${{ env.compatibility == 'develop' }}
run: |
cpanm --sudo --notest \
Devel::CheckLib \
Module::Install \
ExtUtils::PkgConfig \
Module::Install::XSUtil
git clone --branch=develop --depth=1 \
https://github.com/zonemaster/zonemaster-ldns.git
git clone --branch=develop --depth=1 \
https://github.com/zonemaster/zonemaster-engine.git
( cd zonemaster-ldns ; perl Makefile.PL ) # Generate MYMETA.yml to appease cpanm
( cd zonemaster-engine ; perl Makefile.PL ) # Generate MYMETA.yml to appease cpanm
make -C zonemaster-engine # Generate MO files so they get installed
cpanm --sudo --notest ./zonemaster-ldns ./zonemaster-engine
rm -rf zonemaster-ldns zonemaster-engine
# Installing Zonemaster::Engine requires root privileges, because of a
# bug in Mail::SPF preventing normal installation with cpanm as
# non-root user (see link below [1]).
#
# The alternative, if one still wishes to install Zonemaster::Engine
# as non-root user, is to install Mail::SPF first with a command like:
#
# % cpanm --notest \
# --install-args="--install_path sbin=$HOME/.local/sbin" \
# Mail::SPF
#
# For the sake of consistency, other Perl packages installed from CPAN
# are also installed as root.
#
# [1]: https://rt.cpan.org/Public/Bug/Display.html?id=34768
- name: Install remaining dependencies
run: cpanm --sudo --notest --installdeps .
- name: Install Zonemaster::Backend
run: |
perl Makefile.PL
make # Generate MO files so they get installed
cpanm --sudo --notest --verbose .
- name: Set up database
if: ${{ matrix.db != 'sqlite' }}
run: |
case "${{ matrix.db }}" in
mariadb)
cpanm --sudo --notest DBD::mysql
docker run --detach --name ci-mariadb mariadb:10.11
mysql -u root -e "CREATE USER 'ci'@'localhost' IDENTIFIED BY 'password';"
mysql -u root -e "CREATE DATABASE zonemaster CHARACTER SET utf8 COLLATE utf8_bin;"
mysql -u root -e "GRANT ALL ON zonemaster.* TO 'ci'@'localhost';"
;;
postgresql)
cpanm --sudo --notest DBD::Pg
# PGPASSWORD is used by psql
export PGPASSWORD=password
docker run --detach --name ci-postgres -p 5432:5432 --env POSTGRES_PASSWORD="$PGPASSWORD" postgres:16
for i in {1..20} ; do
pg_isready -h localhost -p 5432 && break
sleep 2
done
psql -h localhost -U postgres -c "CREATE USER ci WITH PASSWORD 'password';"
psql -h localhost -U postgres -c 'CREATE DATABASE zonemaster OWNER ci;'
;;
esac
- name: Install locales
run: |
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
sudo locale-gen
- name: Show content of log files
if: ${{ failure() }}
run: cat /home/runner/.cpanm/work/*/build.log
- name: Test
env:
ZONEMASTER_BACKEND_CONFIG_FILE: ./share/backend_config.ci_${{ matrix.db }}.ini
run: make test TEST_VERBOSE=1

25
zonemaster-backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,25 @@
/Makefile
Makefile.old
Build
Build.bat
META.*
MYMETA.*
.build/
_build/
cover_db/
blib/
.lwpcookies
.last_cover_stats
nytprof.out
pod2htm*.tmp
pm_to_blib
Zonemaster-*
Zonemaster-*.tar.gz
.orig
inc
*.mo
# Ignore Emacs and other backup files
*.bak
*~
.*.swp

View File

@@ -0,0 +1,30 @@
# Contributing to Zonemaster::Backend
Contribution to this repository is welcome. Contribution can be either an issue
report or a code or a documentation update. Also see the information in the
[main README][Zonemaster/Zonemaster README] in the main Zonemaster respository.
## Issue
First search for a similar issue in the [issues list]. If a relevant issue is
found, add your information as a comment. If no relevant issue is found, create
[a new issue][create issue]. Give as many details as you have and describe, if
possible, how the issue can be reproduced.
## Pull request
If you would like to contribute an update, first please look for issues and open
[pull requests] that are about the same thing. If nothing relevant is found or
you have a different solution, create [a new pull request][create pull request].
Creating a pull request assumes that you have your proposal in a fork repository.
When you create a pull request, please always start with the `develop` branch
and create the pull request against the same branch.
[issues list]: https://github.com/zonemaster/zonemaster-backend/issues
[create issue]: https://github.com/zonemaster/zonemaster-backend/issues/new
[pull requests]: https://github.com/zonemaster/zonemaster-backend/pulls
[create pull request]: https://github.com/zonemaster/zonemaster-backend/compare
[Zonemaster/Zonemaster README]: https://github.com/zonemaster/zonemaster#readme

758
zonemaster-backend/Changes Normal file
View File

@@ -0,0 +1,758 @@
Release history for Zonemaster component Zonemaster-Backend
v12.0.0 2025-12-17 (part of Zonemaster v2025.2 release)
[Breaking changes]
- Removes deprecated API method get_batch_job_result (#1228)
[Features]
- Adds Dockerfile for Backend (#1225)
v11.5.0 2025-06-26 (part of Zonemaster v2025.1 release)
[Deprecations]
- API method "get_batch_job_result" is deprecated to be removed in v2025.2.
Use method "batch_status" instead. #1215
[Features]
- Redefines "batch_status" to replace "get_batch_job_result" #1215
[Fixes]
- Updates translation (Norwegian) #1213
- Adapts to changed Engine API #1143
- Sorts historic tests by creation date and not ID #1212
v11.4.0 2025-03-04 (part of Zonemaster v2024.2.1 release)
[Features]
- Adds translation to Slovenian language (#1195, #1185)
[Fixes]
- Updates translations (#1199)
v11.3.0 2024-12-09 (part of Zonemaster v2024.2 release)
[Release information]
- Translations have not been fully updated in this release. They will
be updated in an upcoming release.
[Features]
- Makes utilities zmb() and zmtest() supported (not experimental) and adds file
function for batch creation in zmb() (#1186)
v11.2.0 2024-07-01 (part of Zonemaster v2024.1 release)
[Release information]
- Translations have not been fully updated in this release. They will
be updated in an upcoming release.
- Migration of the database is required by this release to retain
consistency in results in the database.
[Features]
- Fixes regression that caused Backends messages not to be
translated (#1166)
[Fixes]
- Avoids double UTF-8 encoding in zmb (#1144)
- Makes job_status report proper percentages (#1150)
- Updates language in experimental RPC API parameters (#1155)
- Makes RPCAPI call Translators instance() method, instead of new()
(#1167)
- Bumps DBD::SQLite to 1.66 for Rocky Linux 8 (#1174)
v11.1.1 2024-03-28
[Fixes]
- Corrects the database migration script for version
v11.1.0 (#1159)
v11.1.0 2024-03-18 (public release version)
[Release information]
- New database schema requires migration of existing database.
[Features]
- Migrates to new test results database table (#1092, #1145, #1147)
- Adds possibility to run several Test Agents on the same or multiple
servers to the same queue to increase capacity (#1121)
- Adds input name normalization (#1132)
[Fixes]
- Fixes FreeBSD testagent start script (#1146)
- Fixes a change in JSON::Validator code (#1109)
v11.0.2 2023-09-08 (public fix version)
[Fixes]
- This version contains no real changes. It has been created to
require a higher (fixed) version of Zonemaster-Engine.
v11.0.1 2023-08-07 (public fix version)
[Fixes]
- This version contains no real changes. It has been created to
require a higher (fixed) version of Zonemaster-Engine.
v11.0.0 2023-06-21 (public release version)
[Breaking changes]
- Removes deprecated features related to locale (#1097)
- Remove deprecated "creation_time" key (#1081)
[Features]
- Adds new experimental API methods and method names
(#1111, #1096, #1083, #1054)
[Fixes]
- Updates translations (#1108, #1103, #1102)
- Adds systemd start scripts for Rocky Linux (#1107)
- Removes documentation moved to the zonemaster/zonemaster
repository (#1104)
- Removes dependency to String::ShellQuote (#1093)
- Dissociate died test from reaching max execution time (#1082)
v10.0.2 2023-03-01 (public fix release)
[Fixes]
- Updates translation (#1079)
v10.0.1 2023-01-31 (public fix release)
[Fixes]
- Updates translation (#1073, #1074)
- Fixes uninitialized value warning in zm-rpcapi.log (#1072)
v10.0.0 2022-12-19 (public release version)
[Deprecation]
- The use of language tags that includes country code is
deprecated. See "docs/API.md#language-tag". Such
language codes will be made illegal in the v2023.1 release.
Use language codes without country code instead.
- The use of an empty string in the "LANGUAGE.locale"
setting is deprecated and will be made illegal in the
v2023.1 release. See "docs/Configuration.md#language-section".
- The use of two or more locale tags with the same language code
in the configuration is deprecated and will be made illegal in the
v2023.1 release. See "docs/Configuration.md#language-section".
[Breaking changes]
- Removes database primary key from API method "get_test_results"
(#946, also see #949).
[Features]
- Provides human readable messages when the test agent dies (#1058)
- Adds testcase descriptions in test results (#1055)
- Adds Zonemaster-LDNS version to API method "version_info" (#1050)
[Fixes]
- Updates installation instructions (#1069)
- Updates deprecation information in "docs/API.pm" and
"docs/Configuration.md" (#1067)
- Corrects the license statement in Backend.pm (#1059)
- Fixes get_data_from_parent_zone method (#1057)
- Updates to use Net::IP::XS directly (#1053)
- Updates documents to use lowercase fragments when referring
to internal headings (#1052)
- Sets IPV6_DISABLED message to INFO in testing profile (#1051)
- Removes enabling IPv4 and IPv6 by default in zmtest
(#1041, #1040, #1066)
v9.0.1 2022-07-08 (public fix release)
[Fixes]
- Corrects a bug where Zonemaster-Backend does not respect the IPv4 or
IPV6 setting in a custom profile (#1046, #1039)
- Updates the Danish translation (#1034)
v9.0.0 2022-06-09 (public release version)
[Deprecation]
- The use of language tags that includes country code is
deprecated. See "docs/API.md#language-tag". Such
language codes will be made illegal in the v2022.2 release.
Use language codes without country code instead.
- The use of an empty string in the "LANGUAGE.locale"
setting is deprecated and will be made illegal in the
v2022.2 release. See "docs/Configuration.md#language-section".
- The API methods "get_test_results" and "get_test_history"
returns two keys with the same timestamp in different
formats. The "creation_time" key is deprecated and will
be removed with release v2023.1. Use the "created_at"
key instead. See "docs/API.md#api-method-get_test_results"
and "docs/API.md#api-method-get_test_history".
- API method "get_test_results" returns key "id", which is now
deprecated and will be removed in v2022.2 release. See
"docs/API.md#api-method-get_test_results". (#949)
- Removes deprecated configuration options (#954)
[Breaking changes]
- Domain names cannot be entered with consecutive trailing
dots (#983)
[Features]
- Adds support for CentOS Linux 7 again - to be removed again
with release v2023.1 (#1027)
- Updates zmb for "start_domain_test" and "add_batch_job" to include
support for "queue" (#1006, #1002)
- Makes "/" valid character in domain names and fixes handling of
"_" in domain names (#969)
[Fixes]
- Adds migration script and instructions for database for
this version (#1031, #1030, 1037)
- Fixes libraries and dependencies (#1029, #1022, #991, #990)
- Updates language translations (#1028, #1019, #1024, #1020,
#1025, #1018, #1016)
- Updates installation instructions (#1021, #1023)
- Improves logging (#1014, #1013, #1004, #996, #966)
- Improves test progress computation (#988)
- Improves reuse of previous test (#1010, #979)
- Makes sure domain names are stored without trailing dot even if
submitted with trailing dot (#1005)
- Improved documentation (#1007, #993, #737, #974, #976, #968, #950)
- Fixes handling of trailing dots in domain names and name server
names. Consecutive dots are not permitted. Name server name with
trailing dot will not crash. (#983)
- Fixes bug that prevented domain "0" to be tested (#998)
- Improves code (#997, #981, #980, #957, #919, #975, #973, #972,
#964, #962, #958, #942, #888, #769)
- Updates messages (#986)
- Uses libidn2 instead of libidn (#984)
- Updates zmb (#985, #920)
- Makes batch tests no longer update progress while running (#944, #274)
- Improved database schema (#977, #970)
- Uses ISO 8601 format on all datetimes (#967)
- Makes call for non-existing batch ID return error (#965, #860)
- Improves IDN support (#963)
- Makes Testagent survive database connection failure (#955, #878)
- Improves error handling in TestAgent (#956, #933)
- Cleans up determination of config file default path (#937)
- Fixes unstoppable Testagent daemon (#943, #905)
v8.1.0 2021-12-20 (public fix release)
[Features]
- Adds support of Spanish language (#900)
- Adds Spanish translation of error messages (#936, #910)
- Adds Danish translation of error messages (#930, #909)
- Adds Norwegian translation of error messages (#935, #912)
[Fixes]
- Corrects installation instruction for FreeBSD (#926, #925)
- Fixes Perl code to be compatible with older versions of Perl (#929)
- Fixes inconsistency of test age definition (#928)
- Fixes handling of UTF-8 in PostgreSQL. The error made testing of IDN
names in U-label format break when PostgreSQL was database backend (#932,
#931)
v8.0.0 2021-12-03 (public release version)
[Breaking changes]
- Removes support of PostgreSQL before version 10 (#892)
- Removes "retry" configuration setting (#896, 881)
[Deprecation]
- Deprecates country code in RPC API calls (#796, #794)
[Features]
- Adds translation of error messages (#891)
- Adds Finnish translation of error messages (#921, #911)
- Adds Swedish translation of error messages (#899)
- Replaces CentOS with Rocky Linux (#908, #895)
- Adds upgrade documentation (#907, #879, #618)
- Adds support for collecting metrics (#844)
- Optimization of batches when PostgreSQL is used (#890)
- Improved result history response in API (#837, #830)
- Makes database port configurable (#755, #496)
[Fixes]
- Document clean-up and improvement (#913, #907, #882, #873, #871,
#822, #814, #816, #797)
- Improves error catching (#916, #914)
- Database clean-up and improvement (#906, #887, #859, #833, #839, #831,
#815, #824, #826, #812, #798, #775, #804, #805, 806)
- Updates zmb command line tool (#825, #628, #810, #768, #787, #780)
- Updates dependencies (#902, #901, 903)
- Fix translation issue (#894, #811, #809)
- Improves error message when creation of API user is blocked (#889)
- Improves RPC API error messages (#853, #789, #847, #819, #817, #820,
#703)
- Harmonize database code (#841, #832, #840, #865, #834, #689, #805)
- Use SQLite by default (#855)
- Improves handling of crashed tests (#845)
- Fixes configuration loading error (#851, #813)
- Improves log handling (#843)
- Fixes local adress mapping (#836)
- Updates default location for configuration file (#835)
- Improves validation (#801, #808, #685, #808, #802, #799, #757)
v7.0.0 2021-09-15 (public security release)
[Fixes]
- By design adding a API user (needed for the batch function) is limited to
connections over localhost. With a default GUI installation with reverse
proxy all connections are over localhost, which means that adding a API
user is publicly available if the GUI is publicly available. If you can
add API users, then you can start several large batch jobs which may
overload the Zonemaster system. (#838, #850)
- Makes RPCAPI use the real remote IP for verification to restore the
limitation that the API key can only be added from localhost.
- Disables RPCAPI method "add_api_user" by default.
- Adds configuration key for "backend_config.ini" to enable RPCAPI method
"add_api_user".
- Adds configuration key for "backend_config.ini" to disnable RPCAPI method
"add_batch_job".
- Prevents RPCAPI daemon to recreate workers when workers crashed to do error
in configuration file. This is a FreeBSD specific error. (#813, #862)
v6.2.0 2021-05-28 (public release version)
[Features]
- Adds support for Finnish translation (#712, #711)
[Fixes]
- Updates installation document (#771, #766, #714)
- Improves daemon and configuration handling (#782, #781 #767, #756, #759
#752, #753, #685, #754, #749, #745, #730, #719, #734, #725, #724, #717,
#727, #726, #692, #713)
- Improves database layer (#732, #715, #723, #722)
- Updates unit tests (#764, #738, #731, #720, #721, #490)
- Cleans-up (#739, #733, #627, #654, #718)
v6.1.0 2021-02-06 (public release version)
[Features]
- Adds full support of a light weight SQLite database backend
as an alternative to MariaDB or PostgreSQL database daemons
(#695, #690, #177)
- Extends zmtest with support for selecting profile. Extends zmb
with support for methods "profile_names" and "get_language_tags"
and support for selecting profile (#675)
- Installs zmtest and zmb test tools in path (#693, #687)
- Deprecates "force_hash_id_use_in_API_starting_from_id"
configuration item (#694)
[Fixes]
- Creates a work-around to allow for empty "ip" in nameserver
objects in "start_domain_test" method (#705, #702)
- Updates installation document (#701, #700, #699, #698, #697,
#696, #695, #686)
- Removes unused configuration properties (#688, #273)
- Updates the configuration file document (#684, #520)
- Resolves issue where selected profile and selected IP
transport are in conflict (#682, #683)
- Removes unused configuration file (#672)
- Corrects unit test (#677, #679, #671)
v6.0.2 2020-11-18 (public fix release)
[Fixes]
- Fixes a bug in unit test that prevented update in
some cases (#670, #671)
v6.0.1 2020-11-09 (public release version)
[Fixes]
- Fixed a version specification error in Makefile.PL
(#668)
v6.0.0 2020-11-06 (public release version)
[Breaking changes]
- Updated language tag format in the RPCAPI (#629)
- Restricted language tag.
- Language tag is set in backend.ini.
- New languages can be added without code change.
- RPCAPI can report supported language tags.
[Features]
- New tools for command line test (#662, #658, #652, #632,
#628, #626, #536, #534)
- Improved log handling in RPCAPI (#653, #656, #650, #840)
- Improved log handling in testagent (#644, #612)
[Fixes]
- Updated installation instructions (#665, #663, #660, #658,
#633, #638, #449, #620)
- Corrected MANIFEST (#657)
- Improved error handling handling RPCAPI daemon (#545, #213)
- Garbage collection testing documentation (#567, #578)
- Corrected API documentation (#647, #648)
- Updates and corrections in Translator (#655, #649, #631)
- Add Norwegian in documentation and configuration (#643)
- Clean-up (#642, #598, #597, #639, #638, #641, #193)
- Fix warning in test agent (#635, #630, #625, #607)
- Fix warning in RPCAPI (#636, #624, #634)
v5.0.2 2020-05-22
[Fixes]
- Some zones create too large result to fit into the zonemaster
database which made the Test Agent crasch (only MySQL or
MariaDB, not PostgreSQL, database was affected). This version
increases the database size (#616, #617).
- The "Upgrade" section in the Zonemaster-Backend main README
has updated instructions for the zonemaster database upgrade.
- This release does not introduce any changes to neither
RPC-API nor Test Agents.
v5.0.1 2020-05-15
[Fixes]
- This release does not introduce any changes to neither
RPC-API nor Test Agents.
- Making database patch files being installed and updated
instruction for database patch. The tools are moved to
the share folder (#613).
- Adding reference in the installation instructions on how
to do when upgrading (#611)
v5.0.0 2020-04-30
[High light]
- The database schema has been changed. Create a new
database or migrate it with tool found in scripts folder
[Changes]
- The database schema has been changed (#544).
- Adds inline Ubuntu implementation of status_of_proc (#592, #591)
- Extends compatibility with systemd-tmpfiles on CentOS (#588, #574)
- Implement status command for RPCAPI daemon on LSB (#586, #582, #580)
- Use one service script per daemon on System V (#579, #576)
- Update instructions after dropping Debian 8 (#556)
- Added garbage collector for unfinished tests (#544, #525)
- Updating installation instructions for FreeBSD (#535)
- Added graceful shutdown for TestAgent (#533, #532
- Log details of terminated tests (#531, #341)
- Avoid reloading configuration in Test Agent (#529, #528, #214)
- Give more feedback to the user in case something goes wrong (#524)
[Fixes]
- Avoid rerunning unrelated tmpfiles configs (#601, #600)
- Fix for translation on FreeBSD 12.1 (#594, #593)
- Missing files in MANIFEST added (#585)
- Add forgotten dependency on CentOS (#590, #589)
- Corrected config documentation (#568)
- Added missing use JSON::PP (#577)
- Updated the upgrade instructions (#555, #558, #572)
- Update of installation instructions (#564, #604, #584, #606, #608)
- Fixes UTF-8 issue for domain names with U-label (#570, #571)
- Fixed Backend not starting after reboot on Debian/Ubuntu
(#513, #565)
- Fix warning in test agent daemon (#563, #557)
- Fix crash in RPCAPI (#562, #559)
- Work around deprecation warning (#537, #507)
- Partial fix for translation on FreeBSD 11 (#526, #353, #512)
- Fixing error handling and logging (#527)
v4.0.1 2019-05-31 (public fix version)
[Fixes]
- Corrects in Makefile.PL the versions of Zonemaster::Engine
and Zonemaster::LDNS that this version of Zonemaster::Backend
depends on (#518, #519)
v4.0.0 2019-05-22 (public release version)
[Status]
- This a public release fully tested before release. This version
will be available on CPAN.
[Fixes]
- Updated installation instructions (#502, #510, #511)
- Updated initial sql script for postgres (#504)
- Correction in API document (#501)
- Corrected unit test (#491)
- Fixed memory leak issue (#481, #489, #482)
- Updated recorded data for unit tests (#486, #492)
- Dropped support for Ubuntu 14.04 (#495)
- Added dependency (#483)
- Configuration for Travis (#484)
[API change]
- Stricter API (#494, #317)
v3.0.0 2019-01-27 (pre-release version)
* Status
* This is a pre-release version not fully tested on all supported
OS's and Perl versions. This version will not be available on
CPAN.
* API change
* The RPC-API has been updated (see below)
* Features
* Make file locations overridable #442
* Update of RPC-API
* Change to unsensitive profile name check in the API #458
* Create an API method to get the list of public profile name #457
* Remove deprecated code, key/param and documentation #459
* Complete specification of API input parameter formats 460
* Also see adoption of new profile
* Adopt to the new profile in Zonemaster-Engine
* Use the new profiles feature of the Zonemaster-Engine #461
* Profile documentation updates #455
* Various fixes of new feature #466, #467
* Fixed old 'default_profile' syntax and changed to 'default' #469
* Added logging in Test Agent #398
* Fixes
* All link references on Github now to zonemaster/zonemaster instead
of old dotse/zonemaster #443
* Added missing support for "filter" in "get_test_history" #446
* Add a note about the empty string, stating that it's
allowed but deprecated. #413
* Fixed invalid Zonemaster::Backend::Config call #472
* Remove geolocation code #462
* Updated .travis.yml with supported versions of Perl #464
* Updated .travis.yml to build against Engine develop branch #463
* Fixed an issue with the JSON boolean type #470
* Add docs/Configuration.md to MANIFEST #475
v2.1.0 2018-06-25
Updates
* Updated API specification (#320)
* Add input validation layer (#399)
* Deprecate validate_syntax (#411, #231
* Update "get_test_history" with the "filter" property (#412, #293)
* "get_ns_ips" should have an alias to "get_host_by_name" (#421)
* Remove personnal information (GDPR) (#420)
* delete user info from start_domain request (#425)
Fixed
* Update paths and permissions in installation doc and startup script (#383)
* Make the installation instruction easier to follow (#391)
* Corrected error message for IDN (#382)
* Deduplicate sanity checks across OSes (#393)
* Editorial updates in Installation instructions (#394)
* Wrong policy file loaded by TestAgent.pm (#392, #400)
* More explicit error message when ini file cannot be loaded (#302, #419)
* Update JSON::Validator from 2.06 to 2.07 (#422)
* Update manifest with Validator.pm (#426)
* Updated the install instructions to use cpanm and others (#430)
* Allow null, string, number as id in json rpc request (#431, #432)
* Update sed commands on FreeBSD in installation instructions (#435)
* Editorial updates of installation instructions (#438)
* Removed unused file and its entry in MANIFEST (#437)
* Updates for FreeBSD consistently making /usr/local/etc/zonemaster
its directory for Zonemaster-Backend configuration file (#436)
v2.0.2 2018-02-23
* Protect domain name input field from disallowed characters (#380, #381)
v2.0.1 2018-01-12
Natural Language support
* Update Translator.pm Add support for Danish "da" in Backend. (#346)
Fixed
* Workaround for "query of death" problem (#287, #325)
* Partial fix of leakage of system path information (#334)
* Fixed the issue that validate_syntax and start_domain_test functions do not correctly validate
IPv4 and IPv6 addresses (#173, #328)
* Fixed: Use of uninitialized value $config/$policy (#268, #329)
* Fixed incomplete stored data for unit test and fixed bug in
TestAgent.pm (#337, #342)
* Fix config and start files (#336)
* Fixed: ipv4 || ipv4 must be ipv4 || ipv6 (#319, #326)
* Create a new index for get_test_history API (#292, #324)
* Update Installation.md (#344, #351, #352, #356, #359)
* Update lowest version of dependency in Makefile.PL (#356)
* Update Translator.pm Add support for Danish "da" in Backend. (#346)
* Start script fix (#345)
* Add repo and issue tracker to metadata (#348)
* Adjusting the locale environment before the daemons are started (#347)
v2.0.0 2017-11-02
Switch version scheme to Semantic Versioning.
Changed
* Renamed distribution from Zonemaster-WebBackend to Zonemaster-Backend (#284)
* Renamed zonemaster_backend.psgi to zonemaster_rpcapi.psgi (#284)
* Renamed zm_wb_daemon to zonemaster_backend_testagent (#284)
* Renamed CHANGES to Changes
Fixed
* Fixed licensing discrepancy (#308)
* Fixed broken imports (#301)
* Replaced dependencies on obsolete Net::LDNS and Zonemaster with Zonemaster::LDNS and Zonemaster::Engine (#299)
* Commented out bogus line in backend_config.ini (#307)
* Updated installation instructions (#300, #303, #304, #305, #306)
* Fixed Commonmark rendering on Github (d25c36a)
v1.2.0 2017-04-11 Public Pre-Release
Fixes #182 - Tests fail with `unexpected end of string while parsing JSON string
Fixes #206 - Perl warning when creating a batch
Fixes #270 - "default_profile" would not be loaded
Fixes #276 - Accept SHA-384 digests in DS records.
Fixes #277 - Modified instances of JSON to JSON::PP in Engine.pm and DB/SQlite.pm
Fixes #279 - Add mysql (no more available by default in travisci)
Fixes #266 - Undelegated test (Backend) ignores name that does not resolve to address
Fixes #267 - Undelegated test (Backend) does lookup on in-zone names
v1.1.0 2016-12-08
Fixes #247 - Error while creating database in Ubuntu 16.0.4
Fixes #237 - Update installation instructions
Fixes #236 - Key/parameter "advanced" should be deprecated in the backend
Fixes #233 - API documentatuion improvments
Fixes #232 - Various improvements to the installation guide
Fixes #230 - Add lots of structure to the installation guide
Fixes #219 - Make IPv4 or IPv6 optionnal. If none are provided the engine will run with both enabled
Fixes #211 - Create a config_file parameter allowing a backend to execute only tests of a certain priority
Fixes #210 - Make IP adresses for nameservers optional
Fixes #207 - Batch never completes
Fixes #201 - Add IP '127.0.0.1' to the list of accepted IP addresses to the add_api_user API method
Fixes #200 - Syntac errors in the API.md documentation
Fixes #197 - Addeed an Empty WebBackend.pm module to the distribution
Fixes #196 - Fixes errors in marameters retruned by the backend with MySQL DB
Fixes #191 - The add_batch_job API takes hours to schedule several hundread thousand domains for thesting
Fixes #186 - Why is "ip" required in "start_domain_test" ?
Fixes #165 - .sql files belong in "share", not doc
Fixes #161 - Bug in Zonemaster/WebBackend/Engine.pm when validating parameters
v1.0.7 2016-10-17
Fixes #168 - Published to CPAN
v1.0.6 2016-10-11
Fixes #189 - Fixes s/cutom/custom/ typos
Fixes #188 - Fixes s/professes/processes/ typos
Fixes #185 - Document "add_api_user" and "add_batch_job" completely
Fixes #183 - Banckend doesn't pass "make test" on perl 5.24
Fixes #174 - Filters feature (allows to use this possibility of the engine in the backend APIs)
Fixes #159 - "make test" of zonemaster-backend fails with non-default locale under Ubuntu 14.04
Fixes #158 - Dates needs to be returned in UTC from the database
Fixes #156 - Add Docker based installation instructions
Fixes #155 - Change de preflight test to block only on Basic00
Fixes #153 - Improve the batch API (Fixed and added bulk testing methods)
v1.0.5 2015-12-17
Fixes #148 - Use iana_profile.josn instead of iana.json as source file for IANA tests
Fixes #141 - Database initialisation files (.sql) not updated with the new hash_id column
Fixes #138 - The Bacakend's generated JSON is locale dependant
Fixes #134 - Bug fix of the crontab job runner
Fixes #127 - The Bakend Translator does not handle non scalar message parameters
Fixes #125 - Non numeric IDs for tests
Fixes #124 - modified all instances of .SE to IIS
Fixes #123 - Zonemaster crashes constantly in Free BSD on using mysql
Fixes #122 - Added support for testing the backend on Travis with all 3 supported database backends
v1.0.3 2015-06-26
Fixes #116 - Add user geolocation support
Fixes #115 - Pg backend cleanup
Fixes #114 - Misc backend fixes
Fixes #113 - Api doc improvements
Fixes #112 - Remove tests for cases that are no longer supposed to fail.
Fixes #111 - Change preflight test to only check that the given name is syntactica…
Fixes #110 - Idn problem
Fixes #108 - Experimental method #9 (create_user) cant be run using API.md
Fixes #107 - Method #8 (get_test_history) cant be run using API.md
Fixes #106 - Method #7 (get_test_results) cant be run using API.md
Fixes #105 - Method #6 (test_progress) cant be run using API.md
Fixes #104 - Method #5 (start_domain_test) cant be run using API.md
Fixes #103 - Release 1.0.2 of the backend KO
v1.0.2 2015-05-11
Fixes #100 - CentOS instructions
Fixes #99 - Fixes #59
Fixes #98 - Further updates for Debian instructions 2
Fixes #97 - Debian instructions for the backend updated
Fixes #96 - Debian-compatible start script
Fixes #93 - Make test more robust
Fixes #92 - Updates backend install for Debian
Fixes #91 - API documentation needs to be improved
Fixes #90 - Cleanup of backend code
Fixes #89 - Changing the execution of tests for the backend
Fixes #88 - Installation documentation: add instructions for MySQL
Fixes #87 - fixed indentation
Fixes #86 - Mysql backend support
Fixes #85 - MySQL support for the backend
Fixes #84 - Fix install1
Fixes #83 - Fix ubuntu install doc1
Fixes #82 - Fix ubuntu install doc1
Fixes #81 - Fix ubuntu install doc
Fixes #80 - update backend install
Fixes #79 - reference to sqlite is wrong
Fixes #78 - How to stop the process, closes #70
Fixes #77 - Lots of documentation fixes.
Fixes #76 - The API examples might not be correct
Fixes #75 - Fixes links in the README, and cleanup of the API docs.
Fixes #74 - Renamed documentation files
Fixes #73 - Lots of small changes
Fixes #72 - The API documentation quotes underscores
Fixes #71 - Change the default backend to PostgreSQL,
Fixes #70 - Installation instructions: stop and restart?
Fixes #69 - Installation instructions: how do I verify the setup?
Fixes #68 - Installation instructions: errors when setting up database
Fixes #67 - Installation instructions: configuration file
Fixes #66 - Installation instructions: database setup
Fixes #65 - Issues while setting the database
Fixes #64 - make test failed for zonemaster-backend
Fixes #63 - Cleanup
Fixes #62 - Remove MySQL and CouchDB dependencies from makefile
Fixes #60 - Add the Changelog file describing the issues fixed in each release
Fixes #59 - Make the modifications described in dotse/zonemaster-gui#86
Fixes #57 - Some adjustments for Ubuntu installation
Fixes #54 - Choose which database is needed for the backend
v1.0.1 2015-02-24
Fixes #45 - Updated and moved the client.pl and Client.pm scripts to make it clear they are only ment as code snippets
Fixes #53 - Version change from 1.0.0 to 1.0.1 for release 2015.1
Fixes #48 - Disabled the "make install" target in makefiles
Fixes #17 - Remove get_data_from_parent_zone_1 method from Engine.pm
Fixes #35 - Hardcode UTF-8 as output encoding for the BackendTranslator module (disabling reading of the uncodind to use from system locale variables)
Fixes #33 - Remove reference to Net::DNS::Keyset
Fixes #27 - Solved encoding problem with #17
Fixes #25 - Fix for security related problem in the MySQL module
Fixes #20 - The Runner.pm module uses wrong version of Zonemaster to run the tests
Fixes #19 - Preflight check: Domian existance test does not work
Fixes #18 - The "Fetch data from parent zone" code does not work as it should for the NS records.
v1.0.0 2014-12-30
v1.0.0 2014-12-11 Public beta release.
Fixes #8 - Add README.md to the backend
Fixes #225 - Missing install instructions for Web UI
Fixes #6 - Impossible to start test for afnic.fr in the undelegated tab
Fixes #94 - Seperate history delegated vs undelegated
Fixes #197 - System output in the basic view
Fixes #193 - Information leakage in GUI's System-module
Fixes #235 - Internal server error on testing ".iis.se"
Fixes #193 - Information leakage in GUI's System-module
Fixes #203 - Interfaces does not take into account the winter timings
Fixes #222 - "Fetch data from Parent zone" does not work
Fixes #240 - No input validation on name server name
Fixes #245 - Zonemaster Backend doesn't handle hung processes
Fixes #184 - New logo fro the Web GUI
Fixes #170 - Translation of the results
Fixes #129 - History should differentiate from delegated, undelegated and batch
Fixes #152 - Delay in start of the test (when the same page is used for testing a second domain)
Fixes #121 - Does not support Swedish language
Fixes #132 - Does not run for all broken domains (e.g. broken.dnssec.ee)
Fixes #139 - No line-feed in output from GUI
Fixes #127 - Does not support IDN 2.0 domains
Fixes #117 - Disable both IPv4 and IPv6 possible

View File

@@ -0,0 +1,108 @@
FROM zonemaster/cli:local AS build
ARG version
USER root
RUN apk add --no-cache \
make \
curl \
gcc \
perl-dev \
musl-dev \
perl-app-cpanminus
RUN apk add --no-cache \
jq \
perl-class-method-modifiers \
perl-config-inifiles \
perl-dbd-sqlite \
perl-dbi \
perl-file-share \
perl-file-slurp \
perl-html-parser \
perl-http-parser-xs \
perl-mojolicious \
perl-io-stringy \
perl-log-any \
perl-log-dispatch \
perl-moose \
perl-parallel-forkmanager \
perl-plack \
perl-role-tiny \
perl-test-nowarnings \
perl-test-differences \
perl-test-exception \
perl-try-tiny \
perl-doc
# for METRIC
RUN cpanm --notest --no-wget --from https://cpan.metacpan.org/ \
Net::Statsd
COPY ./Zonemaster-Backend-${version}.tar.gz ./Zonemaster-Backend-${version}.tar.gz
RUN cpanm --notest --no-wget --from https://cpan.metacpan.org \
./Zonemaster-Backend-${version}.tar.gz
FROM zonemaster/cli:local
USER root
COPY --from=build /usr/local/share/perl5 /usr/local/share/perl5
COPY --from=build /usr/local/bin/ /usr/local/bin/
COPY --from=build /usr/lib/perl5 /usr/lib/perl5
RUN apk add --no-cache \
jq \
perl-config-inifiles \
perl-mojolicious \
perl-moose \
perl-dbi \
perl-dbd-sqlite \
perl-plack \
perl-parallel-forkmanager
# Create zonemaster user and group
RUN addgroup -S zonemaster
RUN adduser -S zonemaster -G zonemaster
RUN cd `perl -MFile::ShareDir=dist_dir -E 'say dist_dir("Zonemaster-Backend")'` && \
install -v -m 755 -d /etc/zonemaster && \
install -v -m 775 -g zonemaster -d /var/log/zonemaster && \
install -v -m 640 -g zonemaster ./backend_config.ini /etc/zonemaster/
# Init SQLite database
RUN install -v -m 755 -o zonemaster -g zonemaster -d /var/lib/zonemaster
USER zonemaster
RUN $(perl -MFile::ShareDir -le 'print File::ShareDir::dist_dir("Zonemaster-Backend")')/create_db.pl
USER zonemaster
COPY zonemaster_launch /usr/local/bin
USER root
ARG S6_OVERLAY_VERSION=3.2.1.0
# Install S6
ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz /tmp
RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz
ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-x86_64.tar.xz /tmp
RUN tar -C / -Jxpf /tmp/s6-overlay-x86_64.tar.xz
# RPCAPI service
RUN mkdir /etc/s6-overlay/s6-rc.d/rpcapi
RUN echo "longrun" > /etc/s6-overlay/s6-rc.d/rpcapi/type
RUN echo "#!/command/with-contenv sh" > /etc/s6-overlay/s6-rc.d/rpcapi/run
RUN echo "zonemaster_launch rpcapi" >> /etc/s6-overlay/s6-rc.d/rpcapi/run
# TESTAGENT sevice
RUN mkdir /etc/s6-overlay/s6-rc.d/testagent
RUN echo "longrun" > /etc/s6-overlay/s6-rc.d/testagent/type
RUN echo "#!/command/with-contenv sh" > /etc/s6-overlay/s6-rc.d/testagent/run
RUN echo "zonemaster_launch testagent" >> /etc/s6-overlay/s6-rc.d/testagent/run
RUN touch /etc/s6-overlay/s6-rc.d/user/contents.d/rpcapi
RUN touch /etc/s6-overlay/s6-rc.d/user/contents.d/testagent
ENTRYPOINT ["/usr/local/bin/zonemaster_launch"]

View File

@@ -0,0 +1,42 @@
### Code license
Copyright (c) The Swedish Internet Foundation (<https://internetstiftelsen.se/en/>)
Copyright (c) AFNIC (<https://www.afnic.fr/en/>)
All rights reserved.
Copyright belongs to external contributor where applicable.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
### Documentation license
Copyright (c) The Swedish Internet Foundation (<https://internetstiftelsen.se/en/>)
Copyright (c) AFNIC (<https://www.afnic.fr/en/>)
All rights reserved.
Copyright belongs to external contributor where applicable.
Creative Commons Attribution 4.0 International License
You should have received a copy of the license along with this
work. If not, see <https://creativecommons.org/licenses/by/4.0/>.

View File

@@ -0,0 +1,93 @@
Changes
CONTRIBUTING.md
docs/Architecture.md
docs/files-description.md
docs/TypographicConventions.md
inc/Module/Install.pm
inc/Module/Install/Base.pm
inc/Module/Install/Can.pm
inc/Module/Install/External.pm
inc/Module/Install/Fetch.pm
inc/Module/Install/Makefile.pm
inc/Module/Install/Metadata.pm
inc/Module/Install/Scripts.pm
inc/Module/Install/Share.pm
inc/Module/Install/Win32.pm
inc/Module/Install/WriteAll.pm
lib/Zonemaster/Backend.pm
lib/Zonemaster/Backend/Config.pm
lib/Zonemaster/Backend/Config/DCPlugin.pm
lib/Zonemaster/Backend/DB.pm
lib/Zonemaster/Backend/DB/MySQL.pm
lib/Zonemaster/Backend/DB/PostgreSQL.pm
lib/Zonemaster/Backend/DB/SQLite.pm
lib/Zonemaster/Backend/Errors.pm
lib/Zonemaster/Backend/Log.pm
lib/Zonemaster/Backend/Metrics.pm
lib/Zonemaster/Backend/RPCAPI.pm
lib/Zonemaster/Backend/TestAgent.pm
lib/Zonemaster/Backend/Translator.pm
lib/Zonemaster/Backend/Validator.pm
LICENSE
Makefile.PL
MANIFEST This list of files
META.yml
README.md
script/add-batch-job.pl
script/zmb
script/zmtest
script/zonemaster_backend_rpcapi.psgi
script/zonemaster_backend_testagent
share/backend_config.ini
share/cleanup-mysql.sql
share/cleanup-postgres.sql
share/create_db.pl
share/freebsd-pwd.conf
share/GNUmakefile
share/locale/da/LC_MESSAGES/Zonemaster-Backend.mo
share/locale/es/LC_MESSAGES/Zonemaster-Backend.mo
share/locale/fi/LC_MESSAGES/Zonemaster-Backend.mo
share/locale/fr/LC_MESSAGES/Zonemaster-Backend.mo
share/locale/nb/LC_MESSAGES/Zonemaster-Backend.mo
share/locale/sl/LC_MESSAGES/Zonemaster-Backend.mo
share/locale/sv/LC_MESSAGES/Zonemaster-Backend.mo
share/Makefile
share/patch/patch_db_zonemaster_backend_ver_11.1.0.pl
share/patch/patch_db_zonemaster_backend_ver_11.2.0.pl
share/patch/patch_db_zonemaster_backend_ver_9.0.0.pl
share/patch/patch_mysql_db_zonemaster_backend_ver_1.0.3.pl
share/patch/patch_mysql_db_zonemaster_backend_ver_5.0.0.pl
share/patch/patch_mysql_db_zonemaster_backend_ver_5.0.2.pl
share/patch/patch_mysql_db_zonemaster_backend_ver_8.0.0.pl
share/patch/patch_postgresql_db_zonemaster_backend_ver_1.0.3.pl
share/patch/patch_postgresql_db_zonemaster_backend_ver_5.0.0.pl
share/patch/patch_postgresql_db_zonemaster_backend_ver_8.0.0.pl
share/patch/patch_sqlite_db_zonemaster_backend_ver_8.0.0.pl
share/patch/README.txt
share/tmpfiles.conf
share/zm-rpcapi.lsb
share/zm-rpcapi.service
share/zm-testagent.lsb
share/zm-testagent.service
share/zm_rpcapi-bsd
share/zm_testagent-bsd
t/00-load.t
t/batches.t
t/config.t
t/db.t
t/db_ddl.t
t/idn.data
t/idn.t
t/lifecycle.t
t/parameters_validation.t
t/queue.t
t/rpc_validation.t
t/test01.data
t/test01.t
t/test_profile.json
t/test_profile_network_true.json
t/test_profile_no_network.json
t/test_validate_syntax.t
t/TestUtil.pm
t/translator.t
t/validator.t

View File

@@ -0,0 +1,84 @@
^maint/
^tags$
^\.last_cover_stats$
^t.*sessions
^.*\.log
^.*\.swp$
^MANIFEST\.SKIP$
^Dockerfile$
^zonemaster_launch$
^\.github/
^docs/internal-documentation/
\.po$
^share/[^/]*\.mo$
^share/Zonemaster-Backend.pot$
^share/backend_config\.ci_mysql\.ini$
^share/backend_config\.ci_postgresql\.ini$
^share/backend_config\.ci_sqlite\.ini$
^share/update-po$
^Zonemaster-Backend-[0-9.]*\.tar\.gz
# PO files are not present in the distribution package, tests of those are irrelevant there.
^t/po-files.t
#!start included /usr/share/perl/5.20/ExtUtils/MANIFEST.SKIP
# Avoid version control files.
\bRCS\b
\bCVS\b
\bSCCS\b
,v$
\B\.svn\b
\B\.git\b
\B\.gitignore\b
\b_darcs\b
\B\.cvsignore$
# Avoid VMS specific MakeMaker generated files
\bDescrip.MMS$
\bDESCRIP.MMS$
\bdescrip.mms$
# Avoid Makemaker generated and utility files.
\bMANIFEST\.bak
^Makefile$
\bblib/
\bMakeMaker-\d
\bpm_to_blib\.ts$
\bpm_to_blib$
\bblibdirs\.ts$ # 6.18 through 6.25 generated this
# Avoid Module::Build generated and utility files.
\bBuild$
\b_build/
\bBuild.bat$
\bBuild.COM$
\bBUILD.COM$
\bbuild.com$
# Avoid temp and backup files.
~$
\.old$
\#$
\b\.#
\.bak$
\.tmp$
\.#
\.rej$
# Avoid OS-specific files/dirs
# Mac OSX metadata
\B\.DS_Store
# Mac OSX SMB mount metadata files
\B\._
# Avoid Devel::Cover and Devel::CoverX::Covered files.
\bcover_db\b
\bcovered\b
# Avoid MYMETA files
^MYMETA\.
# Avoid MANIFEST test
t/manifest.t
#!end included /usr/share/perl/5.20/ExtUtils/MANIFEST.SKIP

View File

@@ -0,0 +1,97 @@
use inc::Module::Install;
use Module::Install::Share;
name 'Zonemaster-Backend';
all_from 'lib/Zonemaster/Backend.pm';
repository 'https://github.com/zonemaster/zonemaster-backend';
bugtracker 'https://github.com/zonemaster/zonemaster-backend/issues';
# "2.1.0" could be declared as "2.001" but not as "2.1"
# (see Zonemaster::LDNS below)
requires
'Class::Method::Modifiers' => 1.09,
'Config::IniFiles' => 0,
'DBI' => 1.635,
'Daemon::Control' => 0.001007,
'File::ShareDir' => 0,
'File::Slurp' => 0,
'HTML::Entities' => 0,
'JSON::PP' => 0,
'JSON::RPC' => 1.01,
'JSON::Validator' => 4.00,
'Locale::TextDomain' => 1.20,
'Log::Any' => 0,
'Log::Any::Adapter::Dispatch' => 0,
'Log::Dispatch' => 0,
'LWP::UserAgent' => 0,
'Mojolicious' => 7.28,
'Moose' => 2.04,
'Net::IP::XS' => 0,
'Parallel::ForkManager' => 1.12,
'Plack::Builder' => 0,
'Plack::Middleware::ReverseProxy' => 0,
'Role::Tiny' => 1.001003,
'Router::Simple::Declare' => 0,
'Starman' => 0,
'Try::Tiny' => 0.12,
'Zonemaster::LDNS' => 5.000001, # v5.0.1
'Zonemaster::Engine' => 8.001000, # v8.1.0
;
test_requires 'DBD::SQLite' => 1.66;
test_requires 'Test::Differences';
test_requires 'Test::Exception';
test_requires 'Time::Local' => 1.26;
test_requires 'Test::NoWarnings' => 0;
recommends 'DBD::mysql';
recommends 'DBD::Pg';
recommends 'DBD::SQLite' => 1.66;
install_share;
install_script 'zonemaster_backend_rpcapi.psgi';
install_script 'zonemaster_backend_testagent';
install_script 'zmtest';
install_script 'zmb';
no_index directory => 'CodeSnippets';
no_index directory => 'Doc';
# Make all platforms include inc/Module/Install/External.pm
requires_external_bin 'find';
if ($^O eq "freebsd") {
requires_external_bin 'gmake';
};
sub MY::postamble {
my $text;
if ($^O eq "freebsd") {
# Make FreeBSD use gmake for share/Makefile
$text = 'GMAKE ?= "gmake"' . "\n"
. 'pure_all :: share/Makefile' . "\n"
. "\t" . 'cd share && $(GMAKE) all' . "\n";
} else {
$text = 'pure_all :: share/Makefile' . "\n"
. "\t" . 'cd share && $(MAKE) all' . "\n";
};
my $docker = <<'END_DOCKER';
docker-build:
docker build --tag zonemaster/backend:local --build-arg version=$(VERSION) .
docker-tag-version:
docker tag zonemaster/backend:local zonemaster/backend:$(VERSION)
docker-tag-latest:
docker tag zonemaster/backend:local zonemaster/backend:latest
END_DOCKER
return $text . $docker;
};
install_share;
WriteAll;

View File

@@ -0,0 +1,68 @@
# Zonemaster Backend
### Purpose
This repository is one of the components of the Zonemaster software. For an
overview of the Zonemaster software, please see the
[Zonemaster repository].
This module is the Backend JSON/RPC weservice for the Web Interface part of
the Zonemaster project. It offers a JSON/RPC api to run tests one by one
(as the zonemaster-gui web frontend module does, or by using a batch API to
run the Zonemaster engine on many domains)
A Zonemaster user needs to install the backend only in the case where there is a
need of logging the Zonemaster test runs in one's own respective database for
analysing.
### Prerequisites
Before you install the Zonemaster Backend, you need the
Zonemaster Engine installed. Please see the
[Zonemaster Engine installation instructions][Zonemaster-Engine installation].
### Upgrade
See the [upgrade document].
### Installation
Follow the detailed [installation instructions].
### Configuration
See the [configuration documentation].
### Documentation
The Zonemaster Backend documentation is split up into several documents:
* A number of [Typographic Conventions] are used throughout this documentation.
* The [Architecture] document describes each of the Zonemaster Backend
components and how they operate. It also discusses all central concepts
needed to understand the Zonemaster backend, and contains a glossary over
domain specific technical terms.
* The [Getting Started] guide walks you through creating a *test* and following
it through its life cycle, all using JSON-RPC calls to the *RPC API daemon*.
* The [API] documentation describes the *RPC API daemon* inteface in detail.
## License
This is free software under a 2-clause BSD license. The full text of the license can
be found in the [LICENSE](LICENSE) file included in this respository.
[API]: https://github.com/zonemaster/zonemaster/blob/master/docs/public/using/backend/rpcapi-reference.md
[Architecture]: docs/Architecture.md
[Configuration documentation]: https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md
[Getting Started]: https://github.com/zonemaster/zonemaster/blob/master/docs/public/using/backend/getting-started.md
[Installation instructions]: https://github.com/zonemaster/zonemaster/blob/master/docs/public/installation/zonemaster-backend.md
[Typographic Conventions]: docs/TypographicConventions.md
[Upgrade document]: https://github.com/zonemaster/zonemaster/blob/master/docs/public/upgrading/backend.md
[Zonemaster-Engine installation]: https://github.com/zonemaster/zonemaster/blob/master/docs/public/installation/zonemaster-engine.md
[Zonemaster repository]: https://github.com/zonemaster/zonemaster

View File

@@ -0,0 +1,99 @@
# Architecture
The Zonemaster *Backend* is a system for performing domain health checks and
keeping records of performed domain health checks.
A Zonemaster *Backend* system consists of at least three components: a
single *Database*, a single *Test Agent* and one or more *RPC API daemons*.
## Components
### Database
The *Database* stores health check requests and results. The *Backend*
architecture is oriented around a single central *Database*.
All times in the database are stored in UTC.
### Test Agent
A Zonemaster *Test Agent* is a daemon that picks up *test* requests from the
*Database*, runs them using the *Zonemaster Engine* library, and records the results back
to the *Database*. A single *Test Agent* may handle several requests concurrently.
The *Backend* architecture supports a single *Test Agent* daemon interacting with a single *Database*.
>
> TODO: List all files these processes read and write.
>
> TODO: List everything these processes open network connections to.
>
> TODO: Describe in which order *test* are processed.
>
> TODO: Describe how concurrency, parallelism and synchronization works within a single *Test Agent*.
>
> TODO: Describe how synchronization works among parallel *Test Agents*.
>
### Web backend
A Zonemaster *Web backend* is a daemon providing a JSON-RPC interface for
recording *test* requests in the *Database* and fetching *test* results from the
*Database*. The *Backend* architecture supports multiple *RPC API daemons*
interacting with the same *Database*.
This only needs to be run as root in order to make sure the log file
can be opened. The `starman` process will change to the `www-data` user as
soon as it can, and all of the real work will be done as that user.
>
> TODO: List all ports these processes listen to.
>
> TODO: List all files these processes read and write.
>
> TODO: List everything these processes open network connections to.
>
## Glossary
### Test
### Batch
### Test result
### Test module
### Message
### Profile
A profile is a configuration for Zonemaster Engine; see the [profiles
overview] for context.
Zonemaster Backend allows administrators to [configure the set of
available profiles].
Each available profile has a [profile name].
A profile named `default` is always available.
Each available profile is based on the [Zonemaster Engine default profile].
Each one (with the possible exception of `default`) has a [profile file]
with profile data overriding the Zonemaster Engine default profile.
The [RPC-API] contains several methods that accept profile name arguments.
### Engine
The Zonemaster *Engine* is a library for performing *tests*. It's hosted in [its
own repository](https://github.com/zonemaster/zonemaster-engine/).
--------
[Configure the set of available profiles]: https://github.com/zonemaster/zonemaster/blob/develop/docs/public/configuration/backend.md#profiles-section
[Profile file]: https://metacpan.org/pod/Zonemaster::Engine::Config#PROFILE-DATA
[Profile name]: https://github.com/zonemaster/zonemaster/blob/master/docs/public/using/backend/rpcapi-reference.md#profile-name
[Profiles overview]: https://github.com/zonemaster/zonemaster/blob/master/docs/internal/design/Profiles.md
[RPC-API]: https://github.com/zonemaster/zonemaster/blob/master/docs/public/using/backend/rpcapi-reference.md
[Zonemaster Engine default profile]: https://metacpan.org/pod/Zonemaster::Engine::Config#DESCRIPTION

View File

@@ -0,0 +1,28 @@
# Typographic conventions
The Zonemaster Backend documentation uses the following typographic conventions:
*Italic* text is used for:
* technical terms defined in the [Architecture] document
* data types defined in the [API] document
`Monospace` text is used for:
* snippets of JSON or sh
* JSON-RPC method names
* JSON values
* single or strings of characters
* internet addresses (e.g. domain names and IP addresses)
* file names with or without paths (e.g. configuration files and command line
tools)
* config section names
>
> Block quotes are used for:
>
> * notes and commentary
>
[API]: https://github.com/zonemaster/zonemaster/blob/master/docs/public/using/backend/rpcapi-reference.md
[Architecture]: Architecture.md

View File

@@ -0,0 +1,48 @@
# Files Description
./lib/Zonemaster/Backend/RPCAPI.pm
The main module
./script/zonemaster_backend_rpcapi.psgi
The Plack/PSGI module. The main entry module for a Plack/PSGI server (like Starman)
./lib/Zonemaster/Backend/Config.pm
The Configuration file abstraction layer
./share/backend_config.ini
A sample configuration file
./CodeSnippets/Client.pm
./CodeSnippets/client.pl
A sample script and library to communicate with the backend.
./lib/Zonemaster/Backend/DB.pm
The Database abstraction layer.
./lib/Zonemaster/Backend/DB/MySQL.pm
The Database abstraction layer MySQL sample backend.
./lib/Zonemaster/Backend/DB/SQLite.pm
The Database abstraction layer SQLite sample backend.
./lib/Zonemaster/Backend/DB/PostgreSQL.pm
The Database abstraction layer PostgreSQL backend.
./lib/Zonemaster/Backend/Translator.pm
The translation module.
./lib/Zonemaster/Backend/TestAgent.pm
The TestAgent main module.
./script/execute_zonemaster_P10.pl
./script/execute_zonemaster_P5.pl
The scripts to execute tests with differents priorities (application level priorities).
./script/execute_tests.pl
The main Test Agent entry point to execute from crontab.
./t/test01.t
./t/test_mysql_backend.pl
./t/test_postgresql_backend.pl
./t/test_validate_syntax.t
Test files.

View File

@@ -0,0 +1,59 @@
# Testing instructions for the Garbage Collection feature of the Zonemaster Backend module
## Introduction
The purpose of this instruction is to serve as a notice for manual testing of the new garbage collection feature at release time.
## Testing the unfinished tests garbage collection feature
1. Start a test and wait for it to be finished
```
SELECT hash_id, progress FROM test_results LIMIT 1;
```
Should return:
```
hash_id | progress
------------------+----------
3f7a604683efaf93 | 100
(1 row)
```
2. Simulate a crashed test
```
UPDATE test_results SET progress = 50, test_start_time = '2020-01-01' WHERE hash_id = '3f7a604683efaf93';
```
3. Check that the backend finishes the test with a result stating it was unfinished
```
SELECT hash_id, progress FROM test_results WHERE hash_id = '3f7a604683efaf93';
```
Should return a finished result:
```
hash_id | progress
------------------+----------
3f7a604683efaf93 | 100
(1 row)
```
4. Ensure the test result contains the backend generated critical message:
```
{"tag":"UNABLE_TO_FINISH_TEST","level":"CRITICAL","timestamp":"300","module":"BACKEND_TEST_AGENT"}
```
```
SELECT hash_id, progress FROM test_results WHERE hash_id = '3f7a604683efaf93' AND results::text like '%UNABLE_TO_FINISH_TEST%';
```
_Remark: for MySQL queries remove the `::text` from all queries_
Should return:
```
hash_id | progress
------------------+----------
3f7a604683efaf93 | 100
(1 row)
```

View File

@@ -0,0 +1,24 @@
package Zonemaster::Backend;
our $VERSION = '12.0.0';
use strict;
use warnings;
use 5.14.2;
=head1 NAME
Zonemaster::Backend - A system for running Zonemaster tests asynchronously through an RPC-API
=head1 AUTHOR
Michal Toma <toma@nic.fr>
=head1 LICENSE
This is free software under a 2-clause BSD license. The full text of the license can
be found in the F<LICENSE> file included with this distribution.
=cut
1;

View File

@@ -0,0 +1,928 @@
package Zonemaster::Backend::Config;
use strict;
use warnings;
use 5.14.2;
our $VERSION = '1.1.0';
use Carp qw( confess croak );
use Config::IniFiles;
use Config;
use File::ShareDir qw[dist_file];
use File::Slurp qw( read_file );
use Log::Any qw( $log );
use Readonly;
use Zonemaster::Backend::Validator qw( :untaint );
use Zonemaster::Backend::DB;
Readonly my @SIG_NAME => split ' ', $Config{sig_name};
=head1 CLASS METHODS
=head2 get_default_path
Determine the path for the default backend_config.ini file.
A list of values and locations are checked and the first match is returned.
If all places are checked and no file is found, an exception is thrown.
This procedure is idempotent - i.e. if you call this procedure multiple times
the same value is returned no matter if environment variables or the file system
have changed.
The following checks are made in order:
=over 4
=item $ZONEMASTER_BACKEND_CONFIG_FILE
If this environment variable is set ot a truthy value, that path is returned.
=item /etc/zonemaster/backend_config.ini
If a file exists at this path, it is returned.
=item /usr/local/etc/zonemaster/backend_config.ini
If a file exists at such a path, it is returned.
=item DIST_DIR/backend_config.ini
If a file exists at this path, it is returned.
DIST_DIR is wherever File::ShareDir installs the Zonemaster-Backend dist.
=back
=cut
sub get_default_path {
state $path =
$ENV{ZONEMASTER_BACKEND_CONFIG_FILE} ? $ENV{ZONEMASTER_BACKEND_CONFIG_FILE}
: -e '/etc/zonemaster/backend_config.ini' ? '/etc/zonemaster/backend_config.ini'
: -e '/usr/local/etc/zonemaster/backend_config.ini' ? '/usr/local/etc/zonemaster/backend_config.ini'
: eval { dist_file( 'Zonemaster-Backend', 'backend_config.ini' ) };
return $path // croak "File not found: backend_config.ini\n";
}
=head2 load_profiles
Loads and returns a set of named profiles.
my %all_profiles = (
$config->PUBLIC_PROFILES,
$config->PRIVATE_PROFILES,
);
my %profiles = %{ Zonemaster::Backend::Config->load_profiles( %all_profiles ) };
Takes a hash mapping profile names to profile paths.
An `undef` path value means the default profile.
Returns a hashref mapping profile names to profile objects.
The returned profiles have omitted values filled in with defaults from the
default profile.
Dies if any of the given paths cannot be read or their contents cannot be parsed
as JSON.
=cut
sub load_profiles {
my ( $class, %profile_paths ) = @_;
my %profiles;
foreach my $name ( keys %profile_paths ) {
my $path = $profile_paths{$name};
my $full_profile = Zonemaster::Engine::Profile->default;
if ( defined $path ) {
my $json = eval { read_file( $path, err_mode => 'croak' ) } #
// die "Error loading profile '$name': $@";
my $named_profile = eval { Zonemaster::Engine::Profile->from_json( $json ) } #
// die "Error loading profile '$name' at '$path': $@";
$full_profile->merge( $named_profile );
}
$profiles{$name} = $full_profile;
}
return \%profiles;
}
=head1 CONSTRUCTORS
=head2 load_config
A wrapper around L<parse> that also determines where the config file is located
in the file system and reads it.
Throws an exception if the determined configuration file cannot be read.
See L<parse> for details on additional parsing-related error modes.
=cut
sub load_config {
my ( $class ) = @_;
my $path = get_default_path();
$log->notice( "Loading config: $path" );
my $text = read_file $path;
my $obj = eval { $class->parse( $text ) };
if ( $@ ) {
die "File $path: $@";
}
return $obj;
}
=head2 parse
Construct a new Zonemaster::Backend::Config based on a given configuration.
my $config = Zonemaster::Backend::Config->parse(
q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
}
);
The configuration is interpreted according to the
L<configuration format specification|https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md>.
Returns a new Zonemaster::Backend::Config instance with its properties set to
normalized and untainted values according to the given configuration with
defaults according to the configuration format.
Emits a log warning with a deprecation message for each deprecated property that
is present.
Throws an exception if the given configuration contains errors.
In a valid config file:
=over 4
=item
all required properties are present, and
=item
all sections and properties are recognized.
=back
=cut
sub parse {
my ( $class, $text ) = @_;
my $obj = bless( {}, $class );
$obj->{_public_profiles} = {};
$obj->{_private_profiles} = {};
my $ini = Config::IniFiles->new( -file => \$text )
or die "Failed to parse config: " . join( '; ', @Config::IniFiles::errors ) . "\n";
my $get_and_clear = sub { # Read and clear a property from a Config::IniFiles object.
my ( $section, $param ) = @_;
my ( $value, @extra ) = $ini->val( $section, $param );
if ( @extra ) {
die "Property not unique: $section.$param\n";
}
$ini->delval( $section, $param );
return $value;
};
# Validate section names
{
my %sections = map { $_ => 1 } ( 'DB', 'MYSQL', 'POSTGRESQL', 'SQLITE', 'LANGUAGE', 'PUBLIC PROFILES', 'PRIVATE PROFILES', 'ZONEMASTER', 'METRICS', 'RPCAPI' );
for my $section ( $ini->Sections ) {
if ( !exists $sections{$section} ) {
die "config: unrecognized section: $section\n";
}
}
}
# Assign default values
$obj->_set_DB_polling_interval( '0.5' );
$obj->_set_MYSQL_port( '3306' );
$obj->_set_POSTGRESQL_port( '5432' );
$obj->_set_ZONEMASTER_max_zonemaster_execution_time( '600' );
$obj->_set_ZONEMASTER_number_of_processes_for_frontend_testing( '20' );
$obj->_set_ZONEMASTER_number_of_processes_for_batch_testing( '20' );
$obj->_set_ZONEMASTER_lock_on_queue( '0' );
$obj->_set_ZONEMASTER_age_reuse_previous_test( '600' );
$obj->_set_RPCAPI_enable_user_create( 'no' ); # experimental
$obj->_set_RPCAPI_enable_batch_create( 'yes' ); # experimental
$obj->_set_RPCAPI_enable_add_api_user( 'no' );
$obj->_set_RPCAPI_enable_add_batch_job( 'yes' );
$obj->_set_locales( 'en_US' );
$obj->_add_public_profile( 'default', undef );
$obj->_set_METRICS_statsd_port( '8125' );
# Assign property values (part 1/2)
if ( defined( my $value = $get_and_clear->( 'DB', 'engine' ) ) ) {
$obj->_set_DB_engine( $value );
}
# Check required properties (part 1/2)
if ( !defined $obj->DB_engine ) {
die "config: missing required property DB.engine\n";
}
# Check deprecated properties and assign fallback values
my @warnings;
#currently no deprecation warnings
# Assign property values (part 2/2)
if ( defined( my $value = $get_and_clear->( 'DB', 'polling_interval' ) ) ) {
$obj->_set_DB_polling_interval( $value );
}
if ( defined( my $value = $get_and_clear->( 'MYSQL', 'host' ) ) ) {
$obj->_set_MYSQL_host( $value );
}
if ( defined( my $value = $get_and_clear->( 'MYSQL', 'port' ) ) ) {
if ( $obj->MYSQL_host eq 'localhost' ) {
push @warnings, "MYSQL.port is disregarded if MYSQL.host is set to 'localhost'";
}
$obj->{_MYSQL_port} = $value;
}
if ( defined( my $value = $get_and_clear->( 'MYSQL', 'user' ) ) ) {
$obj->_set_MYSQL_user( $value );
}
if ( defined( my $value = $get_and_clear->( 'MYSQL', 'password' ) ) ) {
$obj->_set_MYSQL_password( $value );
}
if ( defined( my $value = $get_and_clear->( 'MYSQL', 'database' ) ) ) {
$obj->_set_MYSQL_database( $value );
}
if ( defined( my $value = $get_and_clear->( 'POSTGRESQL', 'host' ) ) ) {
$obj->_set_POSTGRESQL_host( $value );
}
if ( defined( my $value = $get_and_clear->( 'POSTGRESQL', 'port' ) ) ) {
$obj->{_POSTGRESQL_port} = $value;
}
if ( defined( my $value = $get_and_clear->( 'POSTGRESQL', 'user' ) ) ) {
$obj->_set_POSTGRESQL_user( $value );
}
if ( defined( my $value = $get_and_clear->( 'POSTGRESQL', 'password' ) ) ) {
$obj->_set_POSTGRESQL_password( $value );
}
if ( defined( my $value = $get_and_clear->( 'POSTGRESQL', 'database' ) ) ) {
$obj->_set_POSTGRESQL_database( $value );
}
if ( defined( my $value = $get_and_clear->( 'SQLITE', 'database_file' ) ) ) {
$obj->_set_SQLITE_database_file( $value );
}
if ( defined( my $value = $get_and_clear->( 'ZONEMASTER', 'max_zonemaster_execution_time' ) ) ) {
$obj->_set_ZONEMASTER_max_zonemaster_execution_time( $value );
}
if ( defined( my $value = $get_and_clear->( 'ZONEMASTER', 'number_of_processes_for_frontend_testing' ) ) ) {
$obj->_set_ZONEMASTER_number_of_processes_for_frontend_testing( $value );
}
if ( defined( my $value = $get_and_clear->( 'ZONEMASTER', 'number_of_processes_for_batch_testing' ) ) ) {
$obj->_set_ZONEMASTER_number_of_processes_for_batch_testing( $value );
}
if ( defined( my $value = $get_and_clear->( 'ZONEMASTER', 'lock_on_queue' ) ) ) {
$obj->_set_ZONEMASTER_lock_on_queue( $value );
}
if ( defined( my $value = $get_and_clear->( 'ZONEMASTER', 'age_reuse_previous_test' ) ) ) {
$obj->_set_ZONEMASTER_age_reuse_previous_test( $value );
}
if ( defined( my $value = $get_and_clear->( 'METRICS', 'statsd_host' ) ) ) {
$obj->_set_METRICS_statsd_host( $value );
}
if ( defined( my $value = $get_and_clear->( 'METRICS', 'statsd_port' ) ) ) {
$obj->_set_METRICS_statsd_port( $value );
}
if ( defined( my $value = $get_and_clear->( 'RPCAPI', 'enable_user_create' ) ) ) {
if ( defined( $get_and_clear->( 'RPCAPI', 'enable_add_api_user' ) ) ) {
die "Error: cannot specify both RPCAPI.enable_add_api_user and RPCAPI.enable_user_create\n";
}
$obj->_set_RPCAPI_enable_add_api_user( $value );
$obj->_set_RPCAPI_enable_user_create( $value );
} else {
if ( defined( my $value = $get_and_clear->( 'RPCAPI', 'enable_add_api_user' ) ) ) {
$obj->_set_RPCAPI_enable_add_api_user( $value );
$obj->_set_RPCAPI_enable_user_create( $value );
}
}
if ( defined( my $value = $get_and_clear->( 'RPCAPI', 'enable_batch_create' ) ) ) {
if ( defined( $get_and_clear->( 'RPCAPI', 'enable_add_batch_job' ) ) ) {
die "Error: cannot specify both RPCAPI.enable_add_batch_job and RPCAPI.enable_batch_create\n";
}
$obj->_set_RPCAPI_enable_add_batch_job( $value );
$obj->_set_RPCAPI_enable_batch_create( $value );
} else {
if ( defined( my $value = $get_and_clear->( 'RPCAPI', 'enable_add_batch_job' ) ) ) {
$obj->_set_RPCAPI_enable_add_batch_job( $value );
$obj->_set_RPCAPI_enable_batch_create( $value );
}
}
if ( defined( my $value = $get_and_clear->( 'LANGUAGE', 'locale' ) ) ) {
$obj->_set_locales( $value );
}
for my $name ( $ini->Parameters( 'PUBLIC PROFILES' ) ) {
my $path = $get_and_clear->( 'PUBLIC PROFILES', $name );
$obj->_add_public_profile( $name, $path );
}
for my $name ( $ini->Parameters( 'PRIVATE PROFILES' ) ) {
my $path = $get_and_clear->( 'PRIVATE PROFILES', $name );
$obj->_add_private_profile( $name, $path );
}
# Check required propertys (part 2/2)
if ( $obj->DB_engine eq 'MySQL' ) {
die "config: missing required property MYSQL.host (required when DB.engine = MySQL)\n"
if !defined $obj->MYSQL_host;
die "config: missing required property MYSQL.user (required when DB.engine = MySQL)\n"
if !defined $obj->MYSQL_user;
die "config: missing required property MYSQL.password (required when DB.engine = MySQL)\n"
if !defined $obj->MYSQL_password;
die "config: missing required property MYSQL.database (required when DB.engine = MySQL)\n"
if !defined $obj->MYSQL_database;
}
elsif ( $obj->DB_engine eq 'PostgreSQL' ) {
die "config: missing required property POSTGRESQL.host (required when DB.engine = PostgreSQL)\n"
if !defined $obj->POSTGRESQL_host;
die "config: missing required property POSTGRESQL.user (required when DB.engine = PostgreSQL)\n"
if !defined $obj->POSTGRESQL_user;
die "config: missing required property POSTGRESQL.password (required when DB.engine = PostgreSQL)\n"
if !defined $obj->POSTGRESQL_password;
die "config: missing required property POSTGRESQL.database (required when DB.engine = PostgreSQL)\n"
if !defined $obj->POSTGRESQL_database;
}
elsif ( $obj->DB_engine eq 'SQLite' ) {
die "config: missing required property SQLITE.database_file (required when DB.engine = SQLite)\n"
if !defined $obj->SQLITE_database_file;
}
# Check unknown property names
{
my @unrecognized;
for my $section ( $ini->Sections ) {
for my $param ( $ini->Parameters( $section ) ) {
push @unrecognized, "$section.$param";
}
}
if ( @unrecognized ) {
die "config: unrecognized property(s): " . join( ", ", sort @unrecognized ) . "\n";
}
}
# Emit deprecation warnings
for my $message ( @warnings ) {
$log->warning( $message );
}
return $obj;
}
=head1 METHODS
=head2 check_db
Returns a normalized string based on the supported databases.
=head3 EXCEPTION
Dies if the value is not one of SQLite, PostgreSQL or MySQL.
=cut
sub check_db {
my ( $self, $db ) = @_;
$db = untaint_engine_type( $db ) #
// die "Unknown database '$db', should be one of SQLite, MySQL or PostgreSQL\n";
return _normalize_engine_type( $db );
}
=head2 DB_engine
Get the value of L<DB.engine|https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md#engine>.
Returns one of C<"SQLite">, C<"PostgreSQL"> or C<"MySQL">.
=cut
sub DB_engine {
my ( $self ) = @_;
return $self->{_DB_engine};
}
sub _set_DB_engine {
my ( $self, $value ) = @_;
$value = untaint_engine_type( $value ) #
// die "Invalid value for DB.engine: $value\n";
$self->{_DB_engine} = _normalize_engine_type( $value );
return;
}
=head2 DB_polling_interval
Get the value of L<DB.polling_interval|https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md#polling_interval>.
Returns a number.
=head2 MYSQL_database
Get the value of L<MYSQL.database|https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md#database>.
Returns a string.
=head2 MYSQL_host
Get the value of L<MYSQL.host|https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md#host>.
Returns a string.
=head2 MYSQL_port
Returns the L<MYSQL.port|https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md#port>
property from the loaded config.
Returns a number.
=head2 MYSQL_password
Get the value of L<MYSQL.password|https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md#password>.
Returns a string.
=head2 MYSQL_user
Get the value of L<MYSQL.user|https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md#user>.
Returns a string.
=head2 POSTGRESQL_database
Get the value of L<POSTGRESQL.database|https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md#database-1>.
Returns a string.
=head2 POSTGRESQL_host
Get the value of L<POSTGRESQL.host|https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md#host-1>.
Returns a string.
=head2 POSTGRESQL_port
Returns the L<POSTGRESQL.port|https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md#port-1>
property from the loaded config.
Returns a number.
=head2 POSTGRESQL_password
Get the value of L<POSTGRESQL.password|https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md#password-1>.
Returns a string.
=head2 POSTGRESQL_user
Get the value of L<POSTGRESQL.user|https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md#user-1>.
Returns a string.
=head2 SQLITE_database_file
Get the value of L<SQLITE.database_file|https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md#database_file>.
Returns a string.
=head2 LANGUAGE_locale
Get the value of L<LANGUAGE.locale|https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md#locale>.
Returns a mapping from two-letter locale tag prefixes to full locale tags.
This is represented by a hash mapping prefix to full locale tag.
E.g.:
(
en => "en_US",
sv => "sv_SE",
)
=head2 PUBLIC_PROFILES
Get the set of L<PUBLIC PROFILES|https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md#public-profiles-and-private-profiles-sections>.
Returns a hash mapping profile names to profile paths.
The profile names are normalized to lowercase.
Profile paths are either strings or C<undef>.
C<undef> means that the Zonemaster Engine default profile should be used.
=head2 PRIVATE_PROFILES
Get the set of L<PRIVATE PROFILES|https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md#public-profiles-and-private-profiles-sections>.
Returns a hash mapping profile names to profile paths.
The profile names are normalized to lowercase.
Profile paths are always strings (contrast with L<PUBLIC_PROFILES>).
=head2 ZONEMASTER_max_zonemaster_execution_time
Get the value of L<ZONEMASTER.max_zonemaster_execution_time|https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md#max_zonemaster_execution_time>.
Returns a number.
=head2 ZONEMASTER_number_of_processes_for_frontend_testing
Get the value of
L<ZONEMASTER.number_of_processes_for_frontend_testing|https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md#number_of_processes_for_frontend_testing>.
Returns a number.
=head2 ZONEMASTER_number_of_processes_for_batch_testing
Get the value of
L<ZONEMASTER.number_of_processes_for_batch_testing|https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md#number_of_processes_for_batch_testing>.
Returns a number.
=head2 ZONEMASTER_lock_on_queue
Get the value of
L<ZONEMASTER.lock_on_queue|https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md#lock_on_queue>.
Returns a number.
=head2 ZONEMASTER_age_reuse_previous_test
Get the value of
L<ZONEMASTER.age_reuse_previous_test|https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md#age_reuse_previous_test>.
Returns a number.
=head2 METRICS_statsd_host
Get the value of
L<METRICS.statsd_host|https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md#statsd_host>.
Returns a string.
=head2 METRICS_statsd_port
Get the value of
L<METRICS.statsd_host|https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md#statsd_port>.
Returns a number.
=head2 RPCAPI_enable_user_create
Experimental.
Get the value of
L<RPCAPI.enable_user_create|https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md#enable_user_create>.
Return 0 or 1
=head2 RPCAPI_enable_batch_create
Experimental.
Get the value of
L<RPCAPI.enable_batch_create|https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md#enable_batch_create>.
Return 0 or 1
=head2 RPCAPI_enable_add_api_user
Get the value of
L<RPCAPI.enable_add_api_user|https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md#enable_add_api_user>.
Return 0 or 1
=head2 RPCAPI_enable_add_batch_job
Get the value of
L<RPCAPI.enable_add_batch_job|https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md#enable_add_batch_job>.
Return 0 or 1
=cut
# Getters for the properties documented above
sub DB_polling_interval { return $_[0]->{_DB_polling_interval}; }
sub MYSQL_host { return $_[0]->{_MYSQL_host}; }
sub MYSQL_port { return $_[0]->{_MYSQL_port}; }
sub MYSQL_user { return $_[0]->{_MYSQL_user}; }
sub MYSQL_password { return $_[0]->{_MYSQL_password}; }
sub MYSQL_database { return $_[0]->{_MYSQL_database}; }
sub POSTGRESQL_host { return $_[0]->{_POSTGRESQL_host}; }
sub POSTGRESQL_port { return $_[0]->{_POSTGRESQL_port}; }
sub POSTGRESQL_user { return $_[0]->{_POSTGRESQL_user}; }
sub POSTGRESQL_password { return $_[0]->{_POSTGRESQL_password}; }
sub POSTGRESQL_database { return $_[0]->{_POSTGRESQL_database}; }
sub SQLITE_database_file { return $_[0]->{_SQLITE_database_file}; }
sub LANGUAGE_locale { return %{ $_[0]->{_LANGUAGE_locale} }; }
sub PUBLIC_PROFILES { return %{ $_[0]->{_public_profiles} }; }
sub PRIVATE_PROFILES { return %{ $_[0]->{_private_profiles} }; }
sub ZONEMASTER_max_zonemaster_execution_time { return $_[0]->{_ZONEMASTER_max_zonemaster_execution_time}; }
sub ZONEMASTER_lock_on_queue { return $_[0]->{_ZONEMASTER_lock_on_queue}; }
sub ZONEMASTER_number_of_processes_for_frontend_testing { return $_[0]->{_ZONEMASTER_number_of_processes_for_frontend_testing}; }
sub ZONEMASTER_number_of_processes_for_batch_testing { return $_[0]->{_ZONEMASTER_number_of_processes_for_batch_testing}; }
sub ZONEMASTER_age_reuse_previous_test { return $_[0]->{_ZONEMASTER_age_reuse_previous_test}; }
sub METRICS_statsd_host { return $_[0]->{_METRICS_statsd_host}; }
sub METRICS_statsd_port { return $_[0]->{_METRICS_statsd_port}; }
sub RPCAPI_enable_user_create { return $_[0]->{_RPCAPI_enable_user_create}; } # experimental
sub RPCAPI_enable_batch_create { return $_[0]->{_RPCAPI_enable_batch_create}; } # experimental
sub RPCAPI_enable_add_api_user { return $_[0]->{_RPCAPI_enable_add_api_user}; }
sub RPCAPI_enable_add_batch_job { return $_[0]->{_RPCAPI_enable_add_batch_job}; }
# Compile time generation of setters for the properties documented above
UNITCHECK {
_create_setter( '_set_DB_polling_interval', '_DB_polling_interval', \&untaint_strictly_positive_millis );
_create_setter( '_set_MYSQL_host', '_MYSQL_host', \&untaint_host );
_create_setter( '_set_MYSQL_port', '_MYSQL_port', \&untaint_strictly_positive_int );
_create_setter( '_set_MYSQL_user', '_MYSQL_user', \&untaint_mariadb_user );
_create_setter( '_set_MYSQL_password', '_MYSQL_password', \&untaint_password );
_create_setter( '_set_MYSQL_database', '_MYSQL_database', \&untaint_mariadb_database );
_create_setter( '_set_POSTGRESQL_host', '_POSTGRESQL_host', \&untaint_host );
_create_setter( '_set_POSTGRESQL_port', '_POSTGRESQL_port', \&untaint_strictly_positive_int );
_create_setter( '_set_POSTGRESQL_user', '_POSTGRESQL_user', \&untaint_postgresql_ident );
_create_setter( '_set_POSTGRESQL_password', '_POSTGRESQL_password', \&untaint_password );
_create_setter( '_set_POSTGRESQL_database', '_POSTGRESQL_database', \&untaint_postgresql_ident );
_create_setter( '_set_SQLITE_database_file', '_SQLITE_database_file', \&untaint_abs_path );
_create_setter( '_set_ZONEMASTER_max_zonemaster_execution_time', '_ZONEMASTER_max_zonemaster_execution_time', \&untaint_strictly_positive_int );
_create_setter( '_set_ZONEMASTER_lock_on_queue', '_ZONEMASTER_lock_on_queue', \&untaint_non_negative_int );
_create_setter( '_set_ZONEMASTER_number_of_processes_for_frontend_testing', '_ZONEMASTER_number_of_processes_for_frontend_testing', \&untaint_strictly_positive_int );
_create_setter( '_set_ZONEMASTER_number_of_processes_for_batch_testing', '_ZONEMASTER_number_of_processes_for_batch_testing', \&untaint_non_negative_int );
_create_setter( '_set_ZONEMASTER_age_reuse_previous_test', '_ZONEMASTER_age_reuse_previous_test', \&untaint_strictly_positive_int );
_create_setter( '_set_METRICS_statsd_host', '_METRICS_statsd_host', \&untaint_host );
_create_setter( '_set_METRICS_statsd_port', '_METRICS_statsd_port', \&untaint_strictly_positive_int );
_create_setter( '_set_RPCAPI_enable_user_create', '_RPCAPI_enable_user_create', \&untaint_bool ); # experimental
_create_setter( '_set_RPCAPI_enable_batch_create', '_RPCAPI_enable_batch_create', \&untaint_bool ); # experimental
_create_setter( '_set_RPCAPI_enable_add_api_user', '_RPCAPI_enable_add_api_user', \&untaint_bool );
_create_setter( '_set_RPCAPI_enable_add_batch_job', '_RPCAPI_enable_add_batch_job', \&untaint_bool );
}
=head2 new_DB
Create a new database adapter object according to configuration.
The adapter connects to the database before it is returned.
=head3 INPUT
The database adapter class is selected based on the return value of
L<DB_engine>.
The database adapter class constructor is called without arguments and is
expected to configure itself according to available global configuration.
=head3 RETURNS
A configured L<Zonemaster::Backend::DB> object.
=head3 EXCEPTIONS
=over 4
=item Dies if no adapter for the configured database engine can be loaded.
=item Dies if the adapter is unable to connect to the database.
=back
=cut
sub new_DB {
my ( $self ) = @_;
my $dbtype = $self->DB_engine;
my $dbclass = Zonemaster::Backend::DB->get_db_class( $dbtype );
my $db = $dbclass->from_config( $self );
return $db;
}
=head2 new_PM
Create a new processing manager object according to configuration.
=head3 INPUT
The values of the following attributes affect the construction of the returned object:
=over
=item ZONEMASTER_max_zonemaster_execution_time
=item ZONEMASTER_number_of_processes_for_batch_testing
=item ZONEMASTER_number_of_processes_for_frontend_testing
=back
=head3 RETURNS
A configured L<Parallel::ForkManager> object.
=cut
sub new_PM {
my $self = shift;
my $maximum_processes = $self->ZONEMASTER_number_of_processes_for_frontend_testing + $self->ZONEMASTER_number_of_processes_for_batch_testing;
my $timeout = $self->ZONEMASTER_max_zonemaster_execution_time;
my %times;
my $pm = Parallel::ForkManager->new( $maximum_processes );
$pm->set_waitpid_blocking_sleep( 0 ) if $pm->can( 'set_waitpid_blocking_sleep' );
$pm->run_on_wait(
sub {
foreach my $pid ( $pm->running_procs ) {
my $diff = time() - $times{$pid}[0];
my $id = $times{$pid}[1];
if ( $diff > $timeout ) {
$log->warning( "Worker process (pid $pid, testid $id): Timeout, sending SIGKILL" );
kill 9, $pid;
}
}
},
1
);
$pm->run_on_start(
sub {
my ( $pid, $id ) = @_;
$times{$pid} = [ time(), $id ];
}
);
$pm->run_on_finish(
sub {
my ( $pid, $exitcode, $id, $signal ) = @_;
delete $times{$pid};
my $message =
( $signal )
? "Terminated by signal $signal (SIG$SIG_NAME[$signal])"
: "Terminated with exit code $exitcode";
$log->notice( "Worker process (pid $pid, testid $id): $message" );
}
);
return $pm;
}
sub _set_locales {
my ( $self, $value ) = @_;
my @locale_tags = split / +/, $value;
if ( !@locale_tags ) {
die "config: Use of empty LANGUAGE.locale property is not permitted. Remove the LANGUAGE.locale entry or specify LANGUAGE.locale = en_US instead.";
}
my %locales;
for my $locale_tag ( @locale_tags ) {
$locale_tag = untaint_locale_tag( $locale_tag ) #
// die "Illegal locale tag in LANGUAGE.locale: $locale_tag\n";
my $lang_code = $locale_tag =~ s/_..$//r;
if ( exists $locales{$lang_code} ) {
die "Repeated language code in LANGUAGE.locale: $lang_code\n";
}
$locales{$lang_code} = $locale_tag;
}
$self->{_LANGUAGE_locale} = \%locales;
return;
}
sub _add_public_profile {
my ( $self, $name, $path ) = @_;
$name = untaint_profile_name( $name ) #
// die "Invalid profile name in PUBLIC PROFILES section: $name\n";
$name = lc $name;
if ( defined $self->{_public_profiles}{$name} || exists $self->{_private_profiles}{$name} ) {
die "Profile name not unique: $name\n";
}
if ( defined $path ) {
$path = untaint_abs_path( $path ) #
// die "Path must be absolute for profile: $name\n";
}
$self->{_public_profiles}{$name} = $path;
return;
}
sub _add_private_profile {
my ( $self, $name, $path ) = @_;
$name = untaint_profile_name( $name ) #
// die "Invalid profile name in PRIVATE PROFILES section: $name\n";
$name = lc $name;
if ( $name eq 'default' ) {
die "Profile name must not be present in PRIVATE PROFILES section: $name\n";
}
if ( exists $self->{_public_profiles}{$name} || exists $self->{_private_profiles}{$name} ) {
die "Profile name not unique: $name\n";
}
$path = untaint_abs_path( $path ) #
// die "Path must be absolute for profile: $name\n";
$self->{_private_profiles}{$name} = $path;
return;
}
# Create a setter method with a given name using the given field and validator
sub _create_setter {
my ( $setter, $field, $validate ) = @_;
$setter =~ /^_set_([A-Z_]*)_([a-z_]*)$/
or confess "Invalid setter name";
my $section = $1;
my $property = $2;
my $setter_impl = sub {
my ( $self, $value ) = @_;
$self->{$field} = $validate->( $value ) #
// die "Invalid value for $section.$property: $value\n";
return;
};
no strict 'refs';
*$setter = $setter_impl;
return;
}
sub _normalize_engine_type {
my ( $value ) = @_;
# Normalized to camel case to match the database engine Perl module name, e.g. "SQLite.pm".
state $db_module_names = {
mysql => 'MySQL',
postgresql => 'PostgreSQL',
sqlite => 'SQLite',
};
return $db_module_names->{ lc $value };
}
1;

View File

@@ -0,0 +1,155 @@
package Zonemaster::Backend::Config::DCPlugin;
use strict;
use warnings;
=head1 NAME
Zonemaster::Backend::Config::DCPlugin - Daemon::Control plugin that
loads the backend configuration.
=head1 SYNOPSIS
Provides validated and sanity-checked backend configuration through the
L<config>, L<db> and L<pm> properties.
my $daemon = Daemon::Control
->with_plugins('+Zonemaster::Backend::Config::DCPlugin')
->new({
program => sub {
my $self = shift;
$self->init_backend_config();
my $config = $self->config;
my $db = $self->db;
my $pm = $self->pm;
...
},
});
No configuration is loaded automatically.
Instead a successful call to init_backend_config() is required.
On restart the reload_config() method is called automatically.
=head1 AUTHOR
Mattias P, C<< <mattias.paivarinta@iis.se> >>
=cut
use parent 'Daemon::Control';
use Role::Tiny; # Must be loaded before Class::Method::Modifiers or it will warn
use Carp;
use Class::Method::Modifiers;
use Hash::Util::FieldHash qw( fieldhash );
use Log::Any qw( $log );
use Zonemaster::Backend::Config;
before do_restart => \&init_backend_config;
# Using the inside-out technique to avoid collisions with other instance
# variables.
fieldhash my %config;
fieldhash my %db;
fieldhash my %pm;
=head1 INSTANCE METHODS
=head2 init_backend_config
Initializes or reinitializes the L<config>, L<db> and L<pm> properties.
A candidate for the L<config> property is either accepted as an argument,
or L<Zonemaster::Backend::Config::load_config> is invoked to provide one.
Candidates for the L<db> and L<pm> properties are constructed according to the
L<config> candidate.
Returns 1 if all candidates are successfully constructed.
In this case all properties are assigned their respective candidate values.
Returns 0 if the construction of any one of the candidates fails.
Details about the construction failure are logged.
None of the properties are updated.
=cut
sub init_backend_config {
my ( $self, $config_candidate ) = @_;
eval {
$config_candidate //= Zonemaster::Backend::Config->load_config();
my $db_candidate = $config_candidate->new_DB();
my $pm_candidate = $config_candidate->new_PM();
$config{$self} = $config_candidate;
$db{$self} = $db_candidate;
$pm{$self} = $pm_candidate;
};
if ( $@ ) {
$log->warn( "Failed to load the configuration: $@" );
return 0;
}
return 1;
}
=head1 PROPERTIES
=head2 config
Getter for the currently loaded configuration.
Throws an exception if no successful call to init_backend_config() has been
made prior to this call.
=cut
sub config {
my ( $self ) = @_;
exists $config{$self} or croak "Not initialized";
return $config{$self};
}
=head2 db
Getter for a database adapter constructed according to the current
configuration.
Throws an exception if no successful call to init_backend_config() has been
made prior to this call.
=cut
sub db {
my ( $self ) = @_;
exists $db{$self} or croak "Not initialized";
return $db{$self};
}
=head2 pm
Getter for a processing manager constructed according to the current
configuration.
Throws an exception if no successful call to init_backend_config() has been
made prior to this call.
=cut
sub pm {
my ( $self ) = @_;
exists $pm{$self} or croak "Not initialized";
return $pm{$self};
}
1;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,344 @@
package Zonemaster::Backend::DB::MySQL;
our $VERSION = '1.1.0';
use Moose;
use 5.14.2;
use DBI qw(:utils);
use Digest::MD5 qw(md5_hex);
use JSON::PP;
use Zonemaster::Backend::Validator qw( untaint_ipv6_address );
use Zonemaster::Backend::Errors;
with 'Zonemaster::Backend::DB';
=head1 CLASS METHODS
=head2 from_config
Construct a new instance from a Zonemaster::Backend::Config.
my $db = Zonemaster::Backend::DB::MySQL->from_config( $config );
=cut
sub from_config {
my ( $class, $config ) = @_;
my $database = $config->MYSQL_database;
my $host = $config->MYSQL_host;
my $port = $config->MYSQL_port;
my $user = $config->MYSQL_user;
my $password = $config->MYSQL_password;
if ( untaint_ipv6_address( $host ) ) {
$host = "[$host]";
}
my $data_source_name = "DBI:mysql:database=$database;host=$host;port=$port";
return $class->new(
{
data_source_name => $data_source_name,
user => $user,
password => $password,
dbhandle => undef,
}
);
}
sub get_dbh_specific_attributes {
return {};
}
sub create_schema {
my ( $self ) = @_;
my $dbh = $self->dbh;
####################################################################
# TEST RESULTS
####################################################################
$dbh->do(
'CREATE TABLE IF NOT EXISTS test_results (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
hash_id VARCHAR(16) NOT NULL,
domain varchar(255) NOT NULL,
batch_id integer NULL,
created_at DATETIME NOT NULL,
started_at DATETIME DEFAULT NULL,
ended_at DATETIME DEFAULT NULL,
priority integer DEFAULT 10,
queue integer DEFAULT 0,
progress integer DEFAULT 0,
fingerprint character varying(32),
params blob NOT NULL,
results mediumblob DEFAULT NULL,
undelegated integer NOT NULL DEFAULT 0,
UNIQUE (hash_id)
) ENGINE=InnoDB
'
) or die Zonemaster::Backend::Error::Internal->new( reason => "MySQL error, could not create 'test_results' table", data => $dbh->errstr() );
# Manually create the index if it does not exist
# the clause IF NOT EXISTS is not available for MySQL (used with FreeBSD)
# retrieve all indexes by key name
my $indexes = $dbh->selectall_hashref( 'SHOW INDEXES FROM test_results', 'Key_name' );
if ( not exists($indexes->{test_results__hash_id}) ) {
$dbh->do(
'CREATE INDEX test_results__hash_id ON test_results (hash_id)'
);
}
if ( not exists($indexes->{test_results__fingerprint}) ) {
$dbh->do(
'CREATE INDEX test_results__fingerprint ON test_results (fingerprint)'
);
}
if ( not exists($indexes->{test_results__batch_id_progress}) ) {
$dbh->do(
'CREATE INDEX test_results__batch_id_progress ON test_results (batch_id, progress)'
);
}
if ( not exists($indexes->{test_results__progress}) ) {
$dbh->do(
'CREATE INDEX test_results__progress ON test_results (progress)'
);
}
if ( not exists($indexes->{test_results__domain_undelegated}) ) {
$dbh->do(
'CREATE INDEX test_results__domain_undelegated ON test_results (domain, undelegated)'
);
}
####################################################################
# LOG LEVEL
####################################################################
$dbh->do(
"CREATE TABLE IF NOT EXISTS log_level (
value INT,
level VARCHAR(15),
UNIQUE (value)
) ENGINE=InnoDB
"
) or die Zonemaster::Backend::Error::Internal->new( reason => "MySQL error, could not create 'log_level' table", data => $dbh->errstr() );
my ( $c ) = $dbh->selectrow_array( "SELECT count(*) FROM log_level" );
if ( $c == 0 ) {
$dbh->do(
"INSERT INTO log_level (value, level)
VALUES
(-2, 'DEBUG3'),
(-1, 'DEBUG2'),
( 0, 'DEBUG'),
( 1, 'INFO'),
( 2, 'NOTICE'),
( 3, 'WARNING'),
( 4, 'ERROR'),
( 5, 'CRITICAL')
"
);
}
####################################################################
# RESULT ENTRIES
####################################################################
$dbh->do(
"CREATE TABLE IF NOT EXISTS result_entries (
hash_id VARCHAR(16) NOT NULL,
level INT NOT NULL,
module VARCHAR(255) NOT NULL,
testcase VARCHAR(255) NOT NULL,
tag VARCHAR(255) NOT NULL,
timestamp REAL NOT NULL,
args BLOB NOT NULL,
CONSTRAINT fk_hash_id FOREIGN KEY (hash_id) REFERENCES test_results(hash_id),
CONSTRAINT fk_level FOREIGN KEY (level) REFERENCES log_level(value)
) ENGINE=InnoDB
"
) or die Zonemaster::Backend::Error::Internal->new( reason => "MySQL error, could not create 'result_entries' table", data => $dbh->errstr() );
$indexes = $dbh->selectall_hashref( 'SHOW INDEXES FROM result_entries', 'Key_name' );
if ( not exists($indexes->{result_entries__hash_id}) ) {
$dbh->do(
'CREATE INDEX result_entries__hash_id ON result_entries (hash_id)'
);
}
if ( not exists($indexes->{result_entries__level}) ) {
$dbh->do(
'CREATE INDEX result_entries__level ON result_entries (level)'
);
}
####################################################################
# BATCH JOBS
####################################################################
$dbh->do(
'CREATE TABLE IF NOT EXISTS batch_jobs (
id integer AUTO_INCREMENT PRIMARY KEY,
username character varying(50) NOT NULL,
created_at DATETIME NOT NULL
) ENGINE=InnoDB;
'
) or die Zonemaster::Backend::Error::Internal->new( reason => "MySQL error, could not create 'batch_jobs' table", data => $dbh->errstr() );
####################################################################
# USERS
####################################################################
$dbh->do(
'CREATE TABLE IF NOT EXISTS users (
id integer AUTO_INCREMENT primary key,
username varchar(128),
api_key varchar(512),
UNIQUE (username)
) ENGINE=InnoDB;
'
) or die Zonemaster::Backend::Error::Internal->new( reason => "MySQL error, could not create 'users' table", data => $dbh->errstr() );
return;
}
=head2 drop_tables
Drop all the tables if they exist.
=cut
sub drop_tables {
my ( $self ) = @_;
# remove any FOREIGN KEY before droping the table
# MariaDB <10.4 and MySQL do not support the IF EXISTS syntax
# on ALTER TABLE and DROP FOREIGN KEY
# MariaDB 10.3 is used on Ubuntu 20.04 LTS (eol 2023-04)
# MySQL is used on FreeBSD
my $tables = $self->dbh->selectall_hashref( 'SHOW TABLE STATUS', 'Name' );
if ( exists $tables->{result_entries} ) {
my @fk = $self->dbh->selectall_array( 'SELECT constraint_name FROM information_schema.referential_constraints' );
@fk = map { ref eq 'ARRAY' ? @$_ : $_ } @fk;
if ( grep( /^fk_hash_id$/, @fk ) ) {
$self->dbh->do( "ALTER TABLE result_entries DROP FOREIGN KEY fk_hash_id" );
}
if ( grep( /^fk_level$/, @fk ) ) {
$self->dbh->do( "ALTER TABLE result_entries DROP FOREIGN KEY fk_level" );
}
}
$self->dbh->do( "DROP TABLE IF EXISTS test_results" );
$self->dbh->do( "DROP TABLE IF EXISTS result_entries" );
$self->dbh->do( "DROP TABLE IF EXISTS log_level" );
$self->dbh->do( "DROP TABLE IF EXISTS users" );
$self->dbh->do( "DROP TABLE IF EXISTS batch_jobs" );
return;
}
sub add_batch_job {
my ( $self, $params ) = @_;
my $batch_id;
my $dbh = $self->dbh;
if ( $self->user_authorized( $params->{username}, $params->{api_key} ) ) {
$batch_id = $self->create_new_batch_job( $params->{username} );
my $test_params = $params->{test_params};
my $priority = $test_params->{priority};
my $queue_label = $test_params->{queue};
$dbh->{AutoCommit} = 0;
eval {$dbh->do( "DROP INDEX test_results__hash_id ON test_results" );};
eval {$dbh->do( "DROP INDEX test_results__fingerprint ON test_results" );};
eval {$dbh->do( "DROP INDEX test_results__batch_id_progress ON test_results" );};
eval {$dbh->do( "DROP INDEX test_results__progress ON test_results" );};
eval {$dbh->do( "DROP INDEX test_results__domain_undelegated ON test_results" );};
my $sth = $dbh->prepare(
q[
INSERT INTO test_results (
hash_id,
domain,
batch_id,
created_at,
priority,
queue,
fingerprint,
params,
undelegated
) VALUES (?,?,?,?,?,?,?,?,?)
],
);
foreach my $domain ( @{$params->{domains}} ) {
$test_params->{domain} = _normalize_domain( $domain );
my $fingerprint = $self->generate_fingerprint( $test_params );
my $encoded_params = $self->encode_params( $test_params );
my $undelegated = $self->undelegated ( $test_params );
my $hash_id = substr(md5_hex(time().rand()), 0, 16);
$sth->execute(
$hash_id,
$test_params->{domain},
$batch_id,
$self->format_time( time() ),
$priority,
$queue_label,
$fingerprint,
$encoded_params,
$undelegated,
);
}
$dbh->do( "CREATE INDEX test_results__hash_id ON test_results (hash_id, created_at)" );
$dbh->do( "CREATE INDEX test_results__fingerprint ON test_results (fingerprint)" );
$dbh->do( "CREATE INDEX test_results__batch_id_progress ON test_results (batch_id, progress)" );
$dbh->do( "CREATE INDEX test_results__progress ON test_results (progress)" );
$dbh->do( "CREATE INDEX test_results__domain_undelegated ON test_results (domain, undelegated)" );
$dbh->commit();
$dbh->{AutoCommit} = 1;
}
else {
die Zonemaster::Backend::Error::PermissionDenied->new( message => 'User not authorized to use batch mode', data => { username => $params->{username}} );
}
return $batch_id;
}
sub get_relative_start_time {
my ( $self, $hash_id ) = @_;
return $self->dbh->selectrow_array(
q[
SELECT ? - started_at
FROM test_results
WHERE hash_id = ?
],
undef,
$self->format_time( time() ),
$hash_id,
);
}
sub is_duplicate {
my ( $self ) = @_;
# for the list of codes see:
# https://mariadb.com/kb/en/mariadb-error-codes/
# https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
return ( $self->dbh->err == 1062 );
}
no Moose;
__PACKAGE__->meta()->make_immutable();
1;

View File

@@ -0,0 +1,315 @@
package Zonemaster::Backend::DB::PostgreSQL;
our $VERSION = '1.1.0';
use Moose;
use 5.14.2;
use DBI qw(:utils);
use Digest::MD5 qw(md5_hex);
use JSON::PP;
use Try::Tiny;
use Zonemaster::Backend::DB;
use Zonemaster::Backend::Errors;
with 'Zonemaster::Backend::DB';
=head1 CLASS METHODS
=head2 from_config
Construct a new instance from a Zonemaster::Backend::Config.
my $db = Zonemaster::Backend::DB::PostgreSQL->from_config( $config );
=cut
sub from_config {
my ( $class, $config ) = @_;
my $database = $config->POSTGRESQL_database;
my $host = $config->POSTGRESQL_host;
my $port = $config->POSTGRESQL_port;
my $user = $config->POSTGRESQL_user;
my $password = $config->POSTGRESQL_password;
my $data_source_name = "DBI:Pg:dbname=$database;host=$host;port=$port";
return $class->new(
{
data_source_name => $data_source_name,
user => $user,
password => $password,
dbhandle => undef,
}
);
}
sub get_dbh_specific_attributes {
return { pg_enable_utf8 => 0 };
}
sub create_schema {
my ( $self ) = @_;
my $dbh = $self->dbh;
####################################################################
# TEST RESULTS
####################################################################
$dbh->do(
'CREATE TABLE IF NOT EXISTS test_results (
id BIGSERIAL PRIMARY KEY,
hash_id VARCHAR(16) NOT NULL,
domain VARCHAR(255) NOT NULL,
batch_id integer,
created_at TIMESTAMP NOT NULL,
started_at TIMESTAMP DEFAULT NULL,
ended_at TIMESTAMP DEFAULT NULL,
priority integer DEFAULT 10,
queue integer DEFAULT 0,
progress integer DEFAULT 0,
fingerprint varchar(32),
params json NOT NULL,
undelegated integer NOT NULL DEFAULT 0,
results json,
UNIQUE (hash_id)
)
'
) or die Zonemaster::Backend::Error::Internal->new( reason => "PostgreSQL error, could not create 'test_results' table", data => $dbh->errstr() );
$dbh->do(
'CREATE INDEX IF NOT EXISTS test_results__hash_id ON test_results (hash_id)'
);
$dbh->do(
'CREATE INDEX IF NOT EXISTS test_results__fingerprint ON test_results (fingerprint)'
);
$dbh->do(
'CREATE INDEX IF NOT EXISTS test_results__batch_id_progress ON test_results (batch_id, progress)'
);
$dbh->do(
'CREATE INDEX IF NOT EXISTS test_results__progress ON test_results (progress)'
);
$dbh->do(
'CREATE INDEX IF NOT EXISTS test_results__domain_undelegated ON test_results (domain, undelegated)'
);
# this index helps speed up query time to retrieve the next test to
# perform when using batches
$dbh->do(
'CREATE INDEX IF NOT EXISTS test_results__progress_priority_id ON test_results (progress, priority DESC, id) WHERE (progress = 0)'
);
####################################################################
# LOG LEVEL
####################################################################
$dbh->do(
"CREATE TABLE IF NOT EXISTS log_level (
value INT,
level VARCHAR(15),
UNIQUE (value)
)
"
) or die Zonemaster::Backend::Error::Internal->new( reason => "PostgreSQL error, could not create 'log_level' table", data => $dbh->errstr() );
my ( $c ) = $dbh->selectrow_array( "SELECT count(*) FROM log_level" );
if ( $c == 0 ) {
$dbh->do(
"INSERT INTO log_level (value, level)
VALUES
(-2, 'DEBUG3'),
(-1, 'DEBUG2'),
( 0, 'DEBUG'),
( 1, 'INFO'),
( 2, 'NOTICE'),
( 3, 'WARNING'),
( 4, 'ERROR'),
( 5, 'CRITICAL')
"
);
}
####################################################################
# RESULT ENTRIES
####################################################################
$dbh->do(
'CREATE TABLE IF NOT EXISTS result_entries (
hash_id VARCHAR(16) NOT NULL,
level INT NOT NULL,
module VARCHAR(255) NOT NULL,
testcase VARCHAR(255) NOT NULL,
tag VARCHAR(255) NOT NULL,
timestamp REAL NOT NULL,
args JSONb NOT NULL,
CONSTRAINT fk_hash_id FOREIGN KEY (hash_id) REFERENCES test_results(hash_id),
CONSTRAINT fk_level FOREIGN KEY(level) REFERENCES log_level(value)
)
'
) or die Zonemaster::Backend::Error::Internal->new( reason => "PostgreSQL error, could not create 'result_entries' table", data => $dbh->errstr() );
$dbh->do(
'CREATE INDEX IF NOT EXISTS result_entries__hash_id ON result_entries (hash_id)'
);
$dbh->do(
'CREATE INDEX IF NOT EXISTS result_entries__level ON result_entries (level)'
);
####################################################################
# BATCH JOBS
####################################################################
$dbh->do(
'CREATE TABLE IF NOT EXISTS batch_jobs (
id serial PRIMARY KEY,
username varchar(50) NOT NULL,
created_at TIMESTAMP NOT NULL
)
'
) or die Zonemaster::Backend::Error::Internal->new( reason => "PostgreSQL error, could not create 'batch_jobs' table", data => $dbh->errstr() );
####################################################################
# USERS
####################################################################
$dbh->do(
'CREATE TABLE IF NOT EXISTS users (
id serial PRIMARY KEY,
username VARCHAR(128),
api_key VARCHAR(512),
UNIQUE (username)
)
'
) or die Zonemaster::Backend::Error::Internal->new( reason => "PostgreSQL error, could not create 'users' table", data => $dbh->errstr() );
return;
}
=head2 drop_tables
Drop all the tables if they exist.
=cut
sub drop_tables {
my ( $self ) = @_;
# Temporarily set the message level just above "notice" to mute messages when the tables don't
# exist.
# Without setting this level we run the risk of tripping up Test::NoWarnings in unit tests.
my ( $old_client_min_messages ) = $self->dbh->selectrow_array( "SHOW client_min_messages" );
$self->dbh->do( "SET client_min_messages = warning" );
try {
$self->dbh->do( "DROP TABLE IF EXISTS test_results CASCADE" );
$self->dbh->do( "DROP TABLE IF EXISTS result_entries CASCADE" );
$self->dbh->do( "DROP TABLE IF EXISTS log_level" );
$self->dbh->do( "DROP TABLE IF EXISTS users" );
$self->dbh->do( "DROP TABLE IF EXISTS batch_jobs" );
}
finally {
$self->dbh->do( "SET client_min_messages = ?", undef, $old_client_min_messages );
};
return;
}
sub add_batch_job {
my ( $self, $params ) = @_;
my $batch_id;
my $dbh = $self->dbh;
if ( $self->user_authorized( $params->{username}, $params->{api_key} ) ) {
$batch_id = $self->create_new_batch_job( $params->{username} );
my $test_params = $params->{test_params};
my $priority = $test_params->{priority};
my $queue_label = $test_params->{queue};
my $created_at = $self->format_time( time() );
$dbh->begin_work();
$dbh->do( "ALTER TABLE test_results DROP CONSTRAINT IF EXISTS test_results_pkey" );
$dbh->do( "DROP INDEX IF EXISTS test_results__hash_id" );
$dbh->do( "DROP INDEX IF EXISTS test_results__fingerprint" );
$dbh->do( "DROP INDEX IF EXISTS test_results__batch_id_progress" );
$dbh->do( "DROP INDEX IF EXISTS test_results__progress" );
$dbh->do( "DROP INDEX IF EXISTS test_results__domain_undelegated" );
$dbh->do(
q[
COPY test_results (
hash_id,
domain,
batch_id,
created_at,
priority,
queue,
fingerprint,
params,
undelegated
)
FROM STDIN
]
);
foreach my $domain ( @{$params->{domains}} ) {
$test_params->{domain} = _normalize_domain( $domain );
my $fingerprint = $self->generate_fingerprint( $test_params );
my $encoded_params = $self->encode_params( $test_params );
my $undelegated = $self->undelegated ( $test_params );
my $hash_id = substr(md5_hex(time().rand()), 0, 16);
$dbh->pg_putcopydata(
"$hash_id\t$test_params->{domain}\t$batch_id\t$created_at\t$priority\t$queue_label\t$fingerprint\t$encoded_params\t$undelegated\n"
);
}
$dbh->pg_putcopyend();
$dbh->do( "ALTER TABLE test_results ADD PRIMARY KEY (id)" );
$dbh->do( "CREATE INDEX test_results__hash_id ON test_results (hash_id, created_at)" );
$dbh->do( "CREATE INDEX test_results__fingerprint ON test_results (fingerprint)" );
$dbh->do( "CREATE INDEX test_results__batch_id_progress ON test_results (batch_id, progress)" );
$dbh->do( "CREATE INDEX test_results__progress ON test_results (progress)" );
$dbh->do( "CREATE INDEX test_results__domain_undelegated ON test_results (domain, undelegated)" );
$dbh->commit();
}
else {
die Zonemaster::Backend::Error::PermissionDenied->new( message => 'User not authorized to use batch mode', data => { username => $params->{username}} );
}
return $batch_id;
}
sub get_relative_start_time {
my ( $self, $hash_id ) = @_;
return $self->dbh->selectrow_array(
q[
SELECT EXTRACT(EPOCH FROM ? - started_at)
FROM test_results
WHERE hash_id=?
],
undef,
$self->format_time( time() ),
$hash_id,
);
}
sub is_duplicate {
my ( $self ) = @_;
# for the list of codes see:
# https://www.postgresql.org/docs/current/errcodes-appendix.html
return ( $self->dbh->state == 23505 );
}
no Moose;
__PACKAGE__->meta()->make_immutable();
1;

View File

@@ -0,0 +1,301 @@
package Zonemaster::Backend::DB::SQLite;
our $VERSION = '1.1.0';
use Moose;
use 5.14.2;
use DBI qw(:utils);
use Digest::MD5 qw(md5_hex);
use JSON::PP;
use Zonemaster::Backend::Errors;
with 'Zonemaster::Backend::DB';
=head1 CLASS METHODS
=head2 from_config
Construct a new instance from a Zonemaster::Backend::Config.
my $db = Zonemaster::Backend::DB::SQLite->from_config( $config );
=cut
sub from_config {
my ( $class, $config ) = @_;
my $file = $config->SQLITE_database_file;
my $data_source_name = "DBI:SQLite:dbname=$file";
return $class->new(
{
data_source_name => $data_source_name,
user => '',
password => '',
dbhandle => undef,
}
);
}
sub DEMOLISH {
my ( $self ) = @_;
$self->dbh->disconnect() if defined $self->dbhandle && $self->dbhandle->ping;
}
sub get_dbh_specific_attributes {
return { sqlite_extended_result_codes => 1 };
}
sub create_schema {
my ( $self ) = @_;
my $dbh = $self->dbh;
# enable FOREIGN KEY support
$dbh->do( 'PRAGMA foreign_keys = ON;' );
####################################################################
# TEST RESULTS
####################################################################
$dbh->do(
'CREATE TABLE IF NOT EXISTS test_results (
id integer PRIMARY KEY AUTOINCREMENT,
hash_id VARCHAR(16) NOT NULL,
domain VARCHAR(255) NOT NULL,
batch_id integer NULL,
created_at DATETIME NOT NULL,
started_at DATETIME DEFAULT NULL,
ended_at DATETIME DEFAULT NULL,
priority integer DEFAULT 10,
queue integer DEFAULT 0,
progress integer DEFAULT 0,
fingerprint character varying(32),
params text NOT NULL,
results text DEFAULT NULL,
undelegated boolean NOT NULL DEFAULT false,
UNIQUE (hash_id)
)
'
) or die Zonemaster::Backend::Error::Internal->new( reason => "SQLite error, could not create 'test_results' table", data => $dbh->errstr() );
$dbh->do(
'CREATE INDEX IF NOT EXISTS test_results__hash_id ON test_results (hash_id)'
);
$self->dbh->do(
'CREATE INDEX IF NOT EXISTS test_results__fingerprint ON test_results (fingerprint)'
);
$dbh->do(
'CREATE INDEX IF NOT EXISTS test_results__batch_id_progress ON test_results (batch_id, progress)'
);
$dbh->do(
'CREATE INDEX IF NOT EXISTS test_results__progress ON test_results (progress)'
);
$dbh->do(
'CREATE INDEX IF NOT EXISTS test_results__domain_undelegated ON test_results (domain, undelegated)'
);
####################################################################
# LOG LEVEL
####################################################################
$dbh->do(
"CREATE TABLE IF NOT EXISTS log_level (
value INTEGER,
level VARCHAR(15),
UNIQUE (value)
)
"
) or die Zonemaster::Backend::Error::Internal->new( reason => "SQLite error, could not create 'log_level' table", data => $dbh->errstr() );
my ( $c ) = $dbh->selectrow_array( "SELECT count(*) FROM log_level" );
if ( $c == 0 ) {
$dbh->do(
"INSERT INTO log_level (value, level)
VALUES
(-2, 'DEBUG3'),
(-1, 'DEBUG2'),
( 0, 'DEBUG'),
( 1, 'INFO'),
( 2, 'NOTICE'),
( 3, 'WARNING'),
( 4, 'ERROR'),
( 5, 'CRITICAL')
"
);
}
####################################################################
# RESULT ENTRIES
####################################################################
$dbh->do(
'CREATE TABLE IF NOT EXISTS result_entries (
hash_id VARCHAR(16) NOT NULL,
level INT NOT NULL,
module VARCHAR(255) NOT NULL,
testcase VARCHAR(255) NOT NULL,
tag VARCHAR(255) NOT NULL,
timestamp REAL NOT NULL,
args BLOB NOT NULL,
FOREIGN KEY(hash_id) REFERENCES test_results(hash_id),
FOREIGN KEY(level) REFERENCES log_level(value)
)
'
) or die Zonemaster::Backend::Error::Internal->new( reason => "SQLite error, could not create 'result_entries' table", data => $dbh->errstr() );
$dbh->do(
'CREATE INDEX IF NOT EXISTS result_entries__hash_id ON result_entries (hash_id)'
);
$dbh->do(
'CREATE INDEX IF NOT EXISTS result_entries__level ON result_entries (level)'
);
####################################################################
# BATCH JOBS
####################################################################
$dbh->do(
'CREATE TABLE IF NOT EXISTS batch_jobs (
id integer PRIMARY KEY,
username character varying(50) NOT NULL,
created_at DATETIME NOT NULL
)
'
) or die Zonemaster::Backend::Error::Internal->new( reason => "SQLite error, could not create 'batch_jobs' table", data => $dbh->errstr() );
####################################################################
# USERS
####################################################################
$dbh->do(
'CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username varchar(128),
api_key varchar(512),
UNIQUE (username)
)
'
) or die Zonemaster::Backend::Error::Internal->new( reason => "SQLite error, could not create 'users' table", data => $dbh->errstr() );
return;
}
=head2 drop_tables
Drop all the tables if they exist.
=cut
sub drop_tables {
my ( $self ) = @_;
$self->dbh->do( "DROP TABLE IF EXISTS test_results" );
$self->dbh->do( "DROP TABLE IF EXISTS result_entries" );
$self->dbh->do( "DROP TABLE IF EXISTS log_level" );
$self->dbh->do( "DROP TABLE IF EXISTS users" );
$self->dbh->do( "DROP TABLE IF EXISTS batch_jobs" );
return;
}
sub add_batch_job {
my ( $self, $params ) = @_;
my $batch_id;
my $dbh = $self->dbh;
if ( $self->user_authorized( $params->{username}, $params->{api_key} ) ) {
$batch_id = $self->create_new_batch_job( $params->{username} );
my $test_params = $params->{test_params};
my $priority = $test_params->{priority};
my $queue_label = $test_params->{queue};
$dbh->{AutoCommit} = 0;
eval {$dbh->do( "DROP INDEX IF EXISTS test_results__hash_id " );};
eval {$dbh->do( "DROP INDEX IF EXISTS test_results__fingerprint " );};
eval {$dbh->do( "DROP INDEX IF EXISTS test_results__batch_id_progress " );};
eval {$dbh->do( "DROP INDEX IF EXISTS test_results__progress " );};
eval {$dbh->do( "DROP INDEX IF EXISTS test_results__domain_undelegated " );};
my $sth = $dbh->prepare( '
INSERT INTO test_results (
hash_id,
domain,
batch_id,
created_at,
priority,
queue,
fingerprint,
params,
undelegated
) VALUES (?,?,?,?,?,?,?,?,?)'
);
foreach my $domain ( @{$params->{domains}} ) {
$test_params->{domain} = _normalize_domain( $domain );
my $fingerprint = $self->generate_fingerprint( $test_params );
my $encoded_params = $self->encode_params( $test_params );
my $undelegated = $self->undelegated ( $test_params );
my $hash_id = substr(md5_hex(time().rand()), 0, 16);
$sth->execute(
$hash_id,
$test_params->{domain},
$batch_id,
$self->format_time( time() ),
$priority,
$queue_label,
$fingerprint,
$encoded_params,
$undelegated,
);
}
$dbh->do( "CREATE INDEX test_results__hash_id ON test_results (hash_id, created_at)" );
$dbh->do( "CREATE INDEX test_results__fingerprint ON test_results (fingerprint)" );
$dbh->do( "CREATE INDEX test_results__batch_id_progress ON test_results (batch_id, progress)" );
$dbh->do( "CREATE INDEX test_results__progress ON test_results (progress)" );
$dbh->do( "CREATE INDEX test_results__domain_undelegated ON test_results (domain, undelegated)" );
$dbh->commit();
$dbh->{AutoCommit} = 1;
}
else {
die Zonemaster::Backend::Error::PermissionDenied->new( message => 'User not authorized to use batch mode', data => { username => $params->{username}} );
}
return $batch_id;
}
sub get_relative_start_time {
my ( $self, $hash_id ) = @_;
return $self->dbh->selectrow_array(
q[
SELECT (julianday(?) - julianday(started_at)) * 3600 * 24
FROM test_results
WHERE hash_id = ?
],
undef,
$self->format_time( time() ),
$hash_id,
);
}
sub is_duplicate {
my ( $self ) = @_;
# for the list of codes see: https://sqlite.org/rescode.html
return ( $self->dbh->err == 2067 );
}
no Moose;
__PACKAGE__->meta()->make_immutable();
1;

View File

@@ -0,0 +1,162 @@
package Zonemaster::Backend::Error;
use Moose;
use Data::Dumper;
use overload '""' => \&as_string;
has 'message' => (
is => 'ro',
isa => 'Str',
required => 1,
);
has 'code' => (
is => 'ro',
isa => 'Int',
required => 1,
);
has 'data' => (
is => 'ro',
isa => 'Any',
default => undef,
);
sub as_hash {
my $self = shift;
my $error = {
code => $self->code,
message => $self->message,
error => ref($self),
};
$error->{data} = $self->data if defined $self->data;
return $error;
}
sub as_string {
my $self = shift;
my $str = sprintf "%s (code %d).", $self->message, $self->code;
if (defined $self->data) {
$str .= sprintf " Context: %s", $self->_data_dump;
}
return $str;
}
sub _data_dump {
my $self = shift;
local $Data::Dumper::Indent = 0;
local $Data::Dumper::Terse = 1;
my $data = Dumper($self->data);
$data =~ s/[\n\r]/ /g;
return $data ;
}
package Zonemaster::Backend::Error::Internal;
use Moose;
use overload '""' => \&as_string;
extends 'Zonemaster::Backend::Error';
has '+message' => (
default => 'Internal server error'
);
has '+code' => (
default => -32603
);
has 'reason' => (
isa => 'Str',
is => 'ro'
);
has 'method' => (
is => 'ro',
isa => 'Str',
builder => '_build_method'
);
sub _build_method {
my $s = 0;
while (my @c = caller($s)) {
$s ++;
last if $c[3] eq 'Moose::Object::new';
}
my @c = caller($s);
if ($c[3] =~ /^(.*)::handle_exception$/ ) {
@c = caller(++$s);
}
return $c[3];
}
sub as_string {
my $self = shift;
my $reason = $self->reason;
$reason =~ s/\s+/ /g;
$reason =~ s/^\s+|\s+$//g;
my $str = sprintf "Caught %s in the `%s` method: %s", ref($self), $self->method, $reason;
if (defined $self->data) {
$str .= sprintf " Context: %s", $self->_data_dump;
}
return $str;
}
around as_hash => sub {
my ($orig, $self) = @_;
my $hash = $self->$orig;
$hash->{reason} = $self->reason;
$hash->{method} = $self->method;
return $hash;
};
package Zonemaster::Backend::Error::ResourceNotFound;
use Moose;
extends 'Zonemaster::Backend::Error';
has '+message' => (
default => 'Resource not found'
);
has '+code' => (
default => -32000
);
package Zonemaster::Backend::Error::PermissionDenied;
use Moose;
extends 'Zonemaster::Backend::Error';
has '+message' => (
default => 'Permission denied'
);
has '+code' => (
default => -32001
);
package Zonemaster::Backend::Error::Conflict;
use Moose;
extends 'Zonemaster::Backend::Error';
has '+message' => (
default => 'Conflicting resource'
);
has '+code' => (
default => -32002
);
package Zonemaster::Backend::Error::JsonError;
use Moose;
extends 'Zonemaster::Backend::Error::Internal';
1;

View File

@@ -0,0 +1,127 @@
use strict;
use warnings;
package Zonemaster::Backend::Log;
use English qw( $PID );
use POSIX;
use JSON::PP;
use IO::Handle;
use Log::Any::Adapter::Util ();
use Carp;
use Data::Dumper;
use base qw(Log::Any::Adapter::Base);
my $default_level = Log::Any::Adapter::Util::numeric_level('info');
sub init {
my ($self) = @_;
if ( defined $self->{log_level} && $self->{log_level} =~ /\D/ ) {
$self->{log_level} = lc $self->{log_level};
my $numeric_level = Log::Any::Adapter::Util::numeric_level( $self->{log_level} );
if ( !defined($numeric_level) ) {
croak "Error: Unrecognized log level " . $self->{log_level} . "\n";
}
$self->{log_level} = $numeric_level;
}
$self->{log_level} //= $default_level;
my $fd;
if ( !exists $self->{file} || $self->{file} eq '-') {
if ( $self->{stderr} ) {
$fd = fileno(STDERR);
} else {
$fd = fileno(STDOUT);
}
} else {
open( $fd, '>>', $self->{file} ) or croak "Can't open log file: $!";
}
$self->{handle} = IO::Handle->new_from_fd( $fd, "w" ) or croak "Can't fdopen file: $!";
$self->{handle}->autoflush(1);
if ( !exists $self->{formatter} ) {
if ( $self->{json} ) {
$self->{formatter} = \&format_json;
} else {
$self->{formatter} = \&format_text;
}
}
}
sub format_text {
my ($self, $log_params) = @_;
my $msg;
$msg .= sprintf "%s ", $log_params->{timestamp};
delete $log_params->{timestamp};
$msg .= sprintf(
"[%d] [%s] [%s] %s",
delete $log_params->{pid},
uc delete $log_params->{level},
delete $log_params->{category},
delete $log_params->{message}
);
if ( %$log_params ) {
local $Data::Dumper::Indent = 0;
local $Data::Dumper::Terse = 1;
my $data = Dumper($log_params);
$msg .= " Extra parameters: $data";
}
return $msg
}
sub format_json {
my ($self, $log_params) = @_;
my $js = JSON::PP->new;
$js->canonical( 1 );
return $js->encode( $log_params );
}
sub structured {
my ($self, $level, $category, $string, @items) = @_;
my $log_level = Log::Any::Adapter::Util::numeric_level($level);
return if $log_level > $self->{log_level};
my %log_params = (
timestamp => strftime( "%FT%TZ", gmtime ),
level => $level,
category => $category,
message => $string,
pid => $PID,
);
for my $item ( @items ) {
if (ref($item) eq 'HASH') {
for my $key (keys %$item) {
$log_params{$key} = $item->{$key};
}
}
}
my $msg = $self->{formatter}->($self, \%log_params);
$self->{handle}->print($msg . "\n");
}
# From Log::Any::Adapter::File
foreach my $method ( Log::Any::Adapter::Util::detection_methods() ) {
no strict 'refs';
my $base = substr($method,3);
my $method_level = Log::Any::Adapter::Util::numeric_level( $base );
*{$method} = sub {
return !!( $method_level <= $_[0]->{log_level} );
};
}
1;

View File

@@ -0,0 +1,60 @@
package Zonemaster::Backend::Metrics;
use strict;
use warnings;
use Log::Any qw($log);
eval("use Net::Statsd");
my $enable_metrics = 0;
if (!$@) {
$enable_metrics = 1;
}
my %CODE_STATUS_HASH = (
-32700 => 'RPC_PARSE_ERROR',
-32600 => 'RPC_INVALID_REQUEST',
-32601 => 'RPC_METHOD_NOT_FOUND',
-32602 => 'RPC_INVALID_PARAMS',
-32603 => 'RPC_INTERNAL_ERROR'
);
sub setup {
my ( $cls, $host, $port ) = @_;
if (!defined $host) {
$enable_metrics = 0;
} elsif ( $enable_metrics ) {
$log->info('Enabling metrics module', { host => $host, port => $port });
$Net::Statsd::HOST = $host;
$Net::Statsd::PORT = $port;
}
}
sub code_to_status {
my ($cls, $code) = @_;
if (defined $code) {
return $CODE_STATUS_HASH{$code};
} else {
return 'RPC_SUCCESS';
}
}
sub increment {
if ( $enable_metrics ) {
Net::Statsd::increment(@_);
}
}
sub gauge {
if ( $enable_metrics ) {
Net::Statsd::gauge(@_);
}
}
sub timing {
if ( $enable_metrics ) {
Net::Statsd::timing(@_);
}
}

View File

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

View File

@@ -0,0 +1,218 @@
package Zonemaster::Backend::TestAgent;
our $VERSION = '1.1.0';
use strict;
use warnings;
use 5.14.2;
use DBI qw(:utils);
use JSON::PP;
use Scalar::Util qw( blessed );
use File::Slurp;
use Locale::TextDomain qw[Zonemaster-Backend];
use Time::HiRes qw[time sleep gettimeofday tv_interval];
use Zonemaster::LDNS;
use Zonemaster::Engine;
use Zonemaster::Engine::Translator;
use Zonemaster::Engine::Profile;
use Zonemaster::Engine::Util;
use Zonemaster::Engine::Logger::Entry;
use Zonemaster::Backend::Config;
use Zonemaster::Backend::Metrics;
sub new {
my ( $class, $params ) = @_;
my $self = {};
if ( !$params || !$params->{config} ) {
die "missing 'config' parameter";
}
my $config = $params->{config};
my $dbtype;
if ( $params->{dbtype} ) {
$dbtype = $config->check_db( $params->{dbtype} );
}
else {
$dbtype = $config->DB_engine;
}
my $dbclass = Zonemaster::Backend::DB->get_db_class( $dbtype );
$self->{_db} = $dbclass->from_config( $config );
$self->{_profiles} = Zonemaster::Backend::Config->load_profiles( #
$config->PUBLIC_PROFILES,
$config->PRIVATE_PROFILES,
);
bless( $self, $class );
return $self;
}
sub run {
my ( $self, $test_id, $show_progress ) = @_;
my @accumulator;
my $params;
$params = $self->{_db}->get_test_params( $test_id );
my ( $domain ) = $params->{domain};
if ( !$domain ) {
die "Must give the name of a domain to test.\n";
}
$domain = $self->to_idn( $domain );
my %numeric = Zonemaster::Engine::Logger::Entry->levels();
if ( $params->{nameservers} && @{ $params->{nameservers} } > 0 ) {
$self->add_fake_delegation( $domain, $params->{nameservers} );
}
if ( $params->{ds_info} && @{ $params->{ds_info} } > 0 ) {
$self->add_fake_ds( $domain, $params->{ds_info} );
}
# If the profile parameter has been set in the API, then load a profile
if ( $params->{profile} ) {
$params->{profile} = lc($params->{profile});
if ( defined $self->{_profiles}{ $params->{profile} } ) {
Zonemaster::Engine::Profile->effective->merge( $self->{_profiles}{ $params->{profile} } );
}
else {
die "The profile [$params->{profile}] is not defined in the backend_config ini file";
}
}
# If IPv4 or IPv6 transport has been explicitly disabled or enabled, then load it after
# any explicitly set profile has been loaded.
if (defined $params->{ipv4}) {
Zonemaster::Engine::Profile->effective->set( q{net.ipv4}, ( $params->{ipv4} ) ? ( 1 ) : ( 0 ) );
}
if (defined $params->{ipv6}) {
Zonemaster::Engine::Profile->effective->set( q{net.ipv6}, ( $params->{ipv6} ) ? ( 1 ) : ( 0 ) );
}
if ( $show_progress ) {
my %methods = Zonemaster::Engine->all_methods;
# BASIC methods are always run: Basic0{0..4}
my $nbr_testcases_planned = 5;
my $nbr_testcases_finished = 0;
foreach my $module ( keys %methods ) {
foreach my $method ( @{ $methods{$module} } ) {
if ( Zonemaster::Engine::Util::should_run_test( $method ) ) {
$nbr_testcases_planned++;
}
}
}
Zonemaster::Engine->logger->callback(
sub {
my ( $entry ) = @_;
if ( $entry->{tag} and $entry->{tag} eq 'TEST_CASE_END' ) {
$nbr_testcases_finished++;
my $progress_percent = int( 100 * $nbr_testcases_finished / $nbr_testcases_planned );
$self->{_db}->test_progress( $test_id, $progress_percent );
}
}
);
}
# Actually run tests!
eval { Zonemaster::Engine->test_zone( $domain ); };
if ( $@ ) {
my $err = $@;
if ( blessed $err and $err->isa( "NormalExit" ) ) {
say STDERR "Exited early: " . $err->message;
}
else {
die "$err\n"; # Don't know what it is, rethrow
}
}
my $insert_result_start_time = [ gettimeofday ];
# TODO: Make minimum level configurable
my @entries = grep { $_->numeric_level >= $numeric{INFO} } @{ Zonemaster::Engine->logger->entries };
Zonemaster::Backend::Metrics::timing("zonemaster.testagent.log_callback_add_result_entry_filter_duration", tv_interval($insert_result_start_time) * 1000);
$self->{_db}->add_result_entries( $test_id, @entries);
my $callback_add_result_entry_duration = tv_interval($insert_result_start_time);
Zonemaster::Backend::Metrics::timing("zonemaster.testagent.log_callback_add_result_entry_duration", $callback_add_result_entry_duration * 1000);
$self->{_db}->set_test_completed( $test_id );
return;
} ## end sub run
sub reset {
my ( $self ) = @_;
Zonemaster::Engine->reset();
}
sub add_fake_delegation {
my ( $self, $domain, $nameservers ) = @_;
my @ns_with_no_ip;
my %data;
foreach my $ns_ip_pair ( @$nameservers ) {
if ( $ns_ip_pair->{ns} && $ns_ip_pair->{ip} ) {
push( @{ $data{ $self->to_idn( $ns_ip_pair->{ns} ) } }, $ns_ip_pair->{ip} );
}
elsif ($ns_ip_pair->{ns}) {
push(@ns_with_no_ip, $self->to_idn( $ns_ip_pair->{ns} ) );
}
else {
die "Invalid ns_ip_pair";
}
}
foreach my $ns ( @ns_with_no_ip ) {
if ( not exists $data{ $ns } ) {
$data{ $self->to_idn( $ns ) } = undef;
}
}
Zonemaster::Engine->add_fake_delegation( $domain => \%data );
return;
}
sub add_fake_ds {
my ( $self, $domain, $ds_info ) = @_;
my @data;
foreach my $ds ( @{ $ds_info } ) {
push @data, { keytag => $ds->{keytag}, algorithm => $ds->{algorithm}, type => $ds->{digtype}, digest => $ds->{digest} };
}
Zonemaster::Engine->add_fake_ds( $domain => \@data );
return;
}
sub to_idn {
my ( $self, $str ) = @_;
if ( $str =~ m/^[[:ascii:]]+$/ ) {
return $str;
}
if ( Zonemaster::LDNS::has_idn() ) {
return Zonemaster::LDNS::to_idn( $str );
}
else {
warn __( "Warning: Zonemaster::LDNS not compiled with IDN support, cannot handle non-ASCII names correctly." );
return $str;
}
}
1;

View File

@@ -0,0 +1,54 @@
package Zonemaster::Backend::Translator;
our $VERSION = '1.1.0';
use 5.14.2;
use Moose;
use Encode;
use Readonly;
use POSIX qw[setlocale LC_MESSAGES LC_CTYPE];
use Locale::TextDomain qw[Zonemaster-Backend];
use Zonemaster::Backend::Config;
# Zonemaster Modules
require Zonemaster::Engine::Translator;
require Zonemaster::Engine::Logger::Entry;
extends 'Zonemaster::Engine::Translator';
Readonly my %TAG_DESCRIPTIONS => (
TEST_DIED => sub {
__x # BACKEND_TEST_AGENT:TEST_DIED
'An error occured and Zonemaster could not start or finish the test.', @_;
},
UNABLE_TO_FINISH_TEST => sub {
__x # BACKEND_TEST_AGENT:UNABLE_TO_FINISH_TEST
'The test took too long to run (the current limit is {max_execution_time} seconds). '
. 'Maybe there are too many name servers or the name servers are either unreachable or not responsive enough.', @_;
},
);
sub _build_all_tag_descriptions {
my ( $class ) = @_;
my $all_tag_descriptions = Zonemaster::Engine::Translator::_build_all_tag_descriptions();
$all_tag_descriptions->{Backend} = \%TAG_DESCRIPTIONS;
return $all_tag_descriptions;
}
sub translate_tag {
my ( $self, $hashref ) = @_;
my $entry = Zonemaster::Engine::Logger::Entry->new( { %{ $hashref } } );
return decode_utf8( $self->SUPER::translate_tag( $entry ) );
}
sub test_case_description {
my ( $self, $test_name ) = @_;
return decode_utf8( $self->SUPER::test_case_description( $test_name ) );
}
1;

View File

@@ -0,0 +1,554 @@
package Zonemaster::Backend::Validator;
our $VERSION = '0.1.0';
use strict;
use warnings;
use 5.14.2;
use Exporter qw( import );
use File::Spec::Functions qw( file_name_is_absolute );
use JSON::Validator::Joi;
use Readonly;
use Locale::TextDomain qw[Zonemaster-Backend];
use Net::IP::XS;
use Zonemaster::Engine::Logger::Entry;
use Zonemaster::Engine::Normalization qw( normalize_name trim_space );
use Zonemaster::LDNS;
our @EXPORT_OK = qw(
untaint_abs_path
untaint_bool
untaint_engine_type
untaint_ip_address
untaint_ipv4_address
untaint_ipv6_address
untaint_host
untaint_ldh_domain
untaint_locale_tag
untaint_mariadb_database
untaint_mariadb_user
untaint_non_negative_int
untaint_password
untaint_postgresql_ident
untaint_profile_name
untaint_strictly_positive_int
untaint_strictly_positive_millis
check_domain
check_ip
check_profile
check_language_tag
);
our %EXPORT_TAGS = (
untaint => [
qw(
untaint_abs_path
untaint_bool
untaint_engine_type
untaint_ip_address
untaint_ipv4_address
untaint_ipv6_address
untaint_host
untaint_ldh_domain
untaint_locale_tag
untaint_mariadb_database
untaint_mariadb_user
untaint_non_negative_int
untaint_password
untaint_postgresql_ident
untaint_profile_name
untaint_strictly_positive_int
untaint_strictly_positive_millis
)
],
format => [
qw(
check_domain
check_ip
check_profile
check_language_tag
)
]
);
# Does not check value ranges within the groups
Readonly my $IPV4_RE => qr/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/;
# Does not check the length and number of the hex groups, nor the value ranges in the IPv4 groups
Readonly my $IPV6_RE => qr/^[0-9a-f:]*:[0-9a-f:]+(:[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})?$/i;
Readonly my $API_KEY_RE => qr/^[a-z0-9-_]{1,512}$/i;
Readonly my $CLIENT_ID_RE => qr/^[a-z0-9-+~_.: ]{1,50}$/i;
Readonly my $CLIENT_VERSION_RE => qr/^[a-z0-9-+~_.: ]{1,50}$/i;
Readonly my $DIGEST_RE => qr/^[a-f0-9]{40}$|^[a-f0-9]{64}$|^[a-f0-9]{96}$/i;
Readonly my $ENGINE_TYPE_RE => qr/^(?:mysql|postgresql|sqlite)$/i;
Readonly my $IPADDR_RE => qr/^$|$IPV4_RE|$IPV6_RE/;
Readonly my $JSONRPC_METHOD_RE => qr/^[a-z0-9_-]*$/i;
Readonly my $LANGUAGE_RE => qr/^[a-z]{2}$/;
Readonly my $LDH_DOMAIN_RE1 => qr{^[a-z0-9_./-]{1,253}[.]?$}i;
Readonly my $LDH_DOMAIN_RE2 => qr{^(?:[.]|[^.]{1,63}(?:[.][^.]{1,63})*[.]?)$};
Readonly my $LOCALE_TAG_RE => qr/^[a-z]{2}_[A-Z]{2}$/;
Readonly my $MARIADB_DATABASE_LENGTH_RE => qr/^.{1,64}$/;
# See: https://mariadb.com/kb/en/identifier-names/#unquoted
Readonly my $MARIADB_IDENT_RE => qr/^[0-9a-z\$_]+$/i;
Readonly my $MARIADB_USER_LENGTH_RE => qr/^.{1,80}$/u;
# Up to 5 and 3 digits in the integer and fraction components respectively
Readonly my $MILLIS_RE => qr/^(?:0|[1-9][0-9]{0,4})(?:[.][0-9]{1,3})?$/;
# Up to 5 digits
Readonly my $NON_NEGATIVE_INT_RE => qr/^(?:0|[1-9][0-9]{0,4})$/;
# At least one non-zero digit
Readonly my $NON_ZERO_NUM_RE => qr/[1-9]/;
# Printable ASCII but first character must not be space or '<'
Readonly my $PASSWORD_RE => qr/^(?:[\x21-\x3b\x3d-\x7e][\x20-\x7e]{0,99})?$/;
# See: https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
Readonly my $POSTGRESQL_IDENT_RE => qr/^[a-z_][a-z0-9_\$]{0,62}$/i;
Readonly my $PROFILE_NAME_RE => qr/^[a-z0-9]$|^[a-z0-9][a-z0-9_-]{0,30}[a-z0-9]$/i;
Readonly my $RELAXED_DOMAIN_NAME_RE => qr/^[.]$|^.{2,254}$/;
Readonly my $TEST_ID_RE => qr/^[0-9a-f]{16}$/;
Readonly my $USERNAME_RE => qr/^[a-z0-9-.@]{1,50}$/i;
# Boolean
Readonly my $BOOL_TRUE_RE => qr/^(true|yes)$/i;
Readonly my $BOOL_FALSE_RE => qr/^(false|no)$/i;
Readonly my $BOOL_RE => qr/^$BOOL_TRUE_RE|$BOOL_FALSE_RE$/i;
sub joi {
return JSON::Validator::Joi->new;
}
sub new {
my ( $type ) = @_;
my $self = {};
bless( $self, $type );
return ( $self );
}
sub api_key {
return joi->string->regex( $API_KEY_RE );
}
sub batch_id {
return joi->integer->positive;
}
sub client_id {
return joi->string->regex( $CLIENT_ID_RE );
}
sub client_version {
return joi->string->regex( $CLIENT_VERSION_RE );
}
sub domain_name {
return {
type => 'string',
format => 'domain',
};
}
sub ds_info {
return {
type => 'object',
additionalProperties => 0,
required => [ 'digest', 'algorithm', 'digtype', 'keytag' ],
properties => {
digest => {
type => 'string',
pattern => $DIGEST_RE,
'x-error-message' => N__ 'Invalid digest format'
},
algorithm => {
type => 'number',
minimum => 0,
'x-error-message' => N__ 'Algorithm must be a positive integer'
},
digtype => {
type => 'number',
minimum => 0,
'x-error-message' => N__ 'Digest type must be a positive integer'
},
keytag => {
type => 'number',
minimum => 0,
'x-error-message' => N__ 'Keytag must be a positive integer'
}
}
};
}
sub ip_address {
return {
type => 'string',
format => 'ip',
};
}
sub nameserver {
return {
type => 'object',
required => [ 'ns' ],
additionalProperties => 0,
properties => {
ns => domain_name,
ip => ip_address
}
};
}
sub priority {
return joi->integer;
}
sub profile_name {
return {
type => 'string',
format => 'profile',
};
}
sub queue {
return joi->integer;
}
sub test_id {
return joi->string->regex( $TEST_ID_RE );
}
sub language_tag {
return {
type => 'string',
format => 'language_tag',
};
}
sub username {
return joi->string->regex( $USERNAME_RE );
}
sub jsonrpc_method {
return joi->string->regex( $JSONRPC_METHOD_RE );
}
=head1 FORMAT INTERFACE
This module contains a set of procedures for validating data types.
The C<check_*> procedures take the value to validate and potential extra
arguments and return either undef if the validation succeeded or the reason of
the failure.
use Zonemaster::Backend::Validator qw( :format );
# prints "invalid value: The domain name character(s) are not supported"
if ( defined ( my $error = check_domain( 'not a domain' ) ) ) {
print "invalid value: $error\n";
} else {
print "value is valid\n";
}
# prints "value is valid"
if ( defined ( my $error = check_domain( 'zonemaster.net' ) ) ) {
print "invalid value: $error\n";
} else {
print "value is valid\n";
}
=cut
=head2 formats($config)
Returns a hashref to be used with the L<"format" method in JSON::Validator|JSON::Validator::Schema/formats>.
The keys are the names of the custom formats, supports: C<domain>,
C<language_tag>, C<ip> and C<profile>.
The method takes a L<Config|Zonemaster::Backend::Config> object as argument.
=cut
sub formats {
my ( $config ) = @_;
return {
domain => \&check_domain,
language_tag => sub { check_language_tag( @_, $config->LANGUAGE_locale ) },
ip => \&check_ip,
profile => sub { check_profile( @_, ( $config->PUBLIC_PROFILES, $config->PRIVATE_PROFILES ) ) },
};
}
=head2 check_domain(%value)
Validates a L<domain name|https://github.com/zonemaster/zonemaster/blob/master/docs/public/using/backend/rpcapi-reference.md#domain-name>.
=cut
sub check_domain {
my ( $domain ) = @_;
if ( !defined( $domain ) ) {
return N__ 'Domain name required';
}
my ( $errors, $_domain ) = normalize_name( trim_space( $domain ) );
if ( @{$errors} ) {
return $errors->[0]->message;
}
return undef
}
=head2 check_language_tag($value, %locales)
Validates a L<https://github.com/zonemaster/zonemaster/blob/master/docs/public/using/backend/rpcapi-reference.md#language-tag>.
=over
=item %locales
A hash of configured locales, as returned by L<Zonemaster::Backend::Config::LANGUAGE_locale>.
=back
=cut
sub check_language_tag {
my ( $language, %locales ) = @_;
my @error;
if ( $language !~ $LANGUAGE_RE ) {
return N__ 'Invalid language tag format';
}
elsif ( !exists $locales{$language} ) {
return N__ "Unkown language string";
}
return undef;
}
=head2 check_ip($value)
Validates an L<IP address|https://github.com/zonemaster/zonemaster/blob/master/docs/public/using/backend/rpcapi-reference.md#ip-address>.
=cut
sub check_ip {
my ( $ip ) = @_;
return N__ 'Invalid IP address' unless untaint_ip_address($ip) ;
return undef
}
=head2 check_profile($value, %profiles)
Validates a L<profile name|https://github.com/zonemaster/zonemaster/blob/master/docs/public/using/backend/rpcapi-reference.md#profile-name>.
=over
=item %profiles
A hash of configured profiles, as returned by L<Zonemaster::Backend::Config::PUBLIC_PROFILES>.
=back
=cut
sub check_profile {
my ( $profile, %profiles ) = @_;
if ( $profile !~ $PROFILE_NAME_RE ) {
return N__ "Invalid profile format";
}
if ( !exists $profiles{ lc($profile) } ) {
return N__ "Unknown profile";
}
}
=head1 UNTAINT INTERFACE
This module contains a set of procedures for validating and untainting strings.
use Zonemaster::Backend::Validator qw( :untaint );
# prints "untainted: sqlite"
if ( defined ( my $value = untaint_engine_type( 'sqlite' ) ) ) {
print "untainted: $value\n";
}
# does not print anything
if ( defined ( my $value = untaint_engine_type( 'Excel' ) ) ) {
print "untainted: $value\n";
}
These procedures all take a possibly tainted single string argument.
If the string is accepted an untainted copy of the string is returned.
=cut
sub untaint_abs_path {
my ( $value ) = @_;
return _untaint_pred( $value, \&file_name_is_absolute );
}
=head2 untaint_engine_type
Accepts the strings C<"MySQL">, C<"PostgreSQL"> and C<"SQLite">,
case-insensitively.
=cut
sub untaint_engine_type {
my ( $value ) = @_;
return _untaint_pat( $value , $ENGINE_TYPE_RE );
}
=head2 untaint_ip_address
Accepts an IPv4 or IPv6 address.
=cut
sub untaint_ip_address {
my ( $value ) = @_;
return untaint_ipv4_address( $value ) // untaint_ipv6_address( $value );
}
=head2 untaint_ipv4_address
Accepts an IPv4 address.
=cut
sub untaint_ipv4_address {
my ( $value ) = @_;
if ( $value =~ /($IPV4_RE)/
&& Net::IP::XS::ip_is_ipv4( $value ) )
{
return $1;
}
return;
}
=head2 untaint_ipv6_address
Accepts an IPv6 address.
=cut
sub untaint_ipv6_address {
my ( $value ) = @_;
if ( $value =~ /($IPV6_RE)/
&& Net::IP::XS::ip_is_ipv6( $value ) )
{
return $1;
}
return;
}
=head2 untaint_host
Accepts an LDH domain name or an IPv4 or IPv6 address.
=cut
sub untaint_host {
my ( $value ) = @_;
return untaint_ldh_domain( $value ) // untaint_ip_address( $value );
}
=head2 untaint_ldh_domain
Accepts an LDH domain name.
=cut
sub untaint_ldh_domain {
my ( $value ) = @_;
return _untaint_pat( $value, $LDH_DOMAIN_RE1, $LDH_DOMAIN_RE2 );
}
=head2 untaint_locale_tag
Accepts a locale tag.
=cut
sub untaint_locale_tag {
my ( $value ) = @_;
return _untaint_pat( $value, $LOCALE_TAG_RE );
}
sub untaint_mariadb_database {
my ( $value ) = @_;
return _untaint_pat( $value, $MARIADB_IDENT_RE, $MARIADB_DATABASE_LENGTH_RE );
}
sub untaint_mariadb_user {
my ( $value ) = @_;
return _untaint_pat( $value, $MARIADB_IDENT_RE, $MARIADB_USER_LENGTH_RE );
}
sub untaint_password {
my ( $value ) = @_;
return _untaint_pat( $value, $PASSWORD_RE );
}
sub untaint_strictly_positive_int {
my ( $value ) = @_;
return _untaint_pat( $value, $NON_NEGATIVE_INT_RE, $NON_ZERO_NUM_RE );
}
sub untaint_strictly_positive_millis {
my ( $value ) = @_;
return _untaint_pat( $value, $MILLIS_RE, $NON_ZERO_NUM_RE );
}
sub untaint_postgresql_ident {
my ( $value ) = @_;
return _untaint_pat( $value, $POSTGRESQL_IDENT_RE );
}
sub untaint_non_negative_int {
my ( $value ) = @_;
return _untaint_pat( $value, $NON_NEGATIVE_INT_RE );
}
sub untaint_profile_name {
my ( $value ) = @_;
return _untaint_pat( $value, $PROFILE_NAME_RE );
}
sub untaint_bool {
my ( $value ) = @_;
my $ret;
$ret = 1 if defined _untaint_pat( $value, $BOOL_TRUE_RE );
$ret = 0 if defined _untaint_pat( $value, $BOOL_FALSE_RE );
return $ret;
}
sub _untaint_pat {
my ( $value, @patterns ) = @_;
for my $pattern ( @patterns ) {
if ( $value !~ /($pattern)/ ) {
return;
}
}
$value =~ qr/(.*)/;
return $1;
}
sub _untaint_pred {
my ( $value, $predicate ) = @_;
if ( $predicate->( $value ) ) {
$value =~ qr/(.*)/;
return $1;
}
else {
return;
}
}
1;

View File

@@ -0,0 +1,42 @@
#!/usr/bin/env perl
# This script is for testing purpose only.
use 5.14.2;
use warnings;
use Data::Dumper;
use Encode qw[decode_utf8];
use Zonemaster::Backend::RPCAPI;
use Digest::MD5 qw(md5_hex);
binmode STDOUT, ':utf8';
my $e = Zonemaster::Backend::RPCAPI->new;
say "Starting add_batch_job";
my @domains;
for (my $i = 0; $i < 100; $i++) {
push(@domains, substr(md5_hex(rand(10000)), 0, 5).".fr");
}
#die Dumper(\@domains);
$e->add_api_user({ username => 'test_user', api_key => 'API_KEY_01'});
$e->add_batch_job(
{
client_id => 'Add Script',
client_version => '1.0',
username => 'test_user',
api_key => 'API_KEY_01',
test_params => {
client_id => 'Add Script',
client_version => '1.0',
ipv4 => 1, # 0 or 1, is the ipv4 checkbox checked
ipv6 => 1, # 0 or 1, is the ipv6 checkbox checked
profile => 'default', # the id if the Test profile listbox (unused)
},
domains => \@domains,
}
);

750
zonemaster-backend/script/zmb Executable file
View File

@@ -0,0 +1,750 @@
#!/usr/bin/env perl
use strict;
use warnings;
use feature 'say';
use Encode qw( decode_utf8 FB_CROAK );
use Getopt::Long qw( GetOptionsFromArray :config require_order );
use JSON::PP qw( encode_json );
use LWP::UserAgent;
use Pod::Usage;
=head1 NAME
B<zmb> - Shell bindings for the Zonemaster::Backend RPC API
Zmb is meant to be pronounced I<Zimba>.
=head1 SYNOPSIS
zmb [GLOBAL OPTIONS] COMMAND [OPTIONS]
=head1 GLOBAL OPTIONS
--help Show usage
--verbose Show RPC query
--server URL The server to connect to. Default is http://localhost:5000/.
=cut
sub main {
my @argv = @_;
@argv = map { decode_utf8( $_, FB_CROAK ) } @argv;
my $opt_help;
my $opt_verbose;
my $opt_server = 'http://localhost:5000/';
GetOptionsFromArray(
\@argv,
'help' => \$opt_help,
'verbose' => \$opt_verbose,
'server=s' => \$opt_server,
) or pod2usage( 2 );
if ( !@argv ) {
pod2usage( -verbose => 99, -sections => ['SYNOPSIS', 'GLOBAL OPTIONS'], -exitval => 'NOEXIT' );
show_commands();
exit 1;
}
my $cmd = shift @argv;
pod2usage( 1 ) if !defined $cmd;
my $cmd_sub = \&{ "cmd_" . $cmd };
pod2usage( "'$cmd' is not a command" ) if !defined &$cmd_sub;
pod2usage( -verbose => 99, -sections => ["COMMANDS/$cmd"] ) if $opt_help;
my $json = &$cmd_sub( @argv );
if ( $json ) {
say $json if $opt_verbose;
my $request = to_request( $opt_server, $json );
my $response = submit( $request );
say $response;
}
}
=head1 COMMANDS
=head2 man
Show the full manual page.
zmb [GLOBAL OPTIONS] man
=cut
sub cmd_man {
pod2usage( -verbose => 2 );
}
=head2 non_existing_method
Call a non-existing RPC method.
zmb [GLOBAL OPTIONS] non_existing_method
=cut
sub cmd_non_existing_method {
return to_jsonrpc(
id => 1,
method => 'non_existing_method',
);
}
=head2 version_info
zmb [GLOBAL OPTIONS] version_info
=cut
sub cmd_version_info {
return to_jsonrpc(
id => 1,
method => 'version_info',
);
}
=head2 profile_names
zmb [GLOBAL OPTIONS] profile_names
=cut
sub cmd_profile_names {
return to_jsonrpc(
id => 1,
method => 'profile_names',
);
}
=head2 get_language_tags
zmb [GLOBAL OPTIONS] get_language_tags
=cut
sub cmd_get_language_tags {
return to_jsonrpc(
id => 1,
method => 'get_language_tags',
);
}
=head2 start_domain_test
zmb [GLOBAL OPTIONS] start_domain_test [OPTIONS]
Options:
--domain DOMAIN_NAME
--ipv4 true|false|null
--ipv6 true|false|null
--nameserver DOMAIN_NAME:IP_ADDRESS
--nameserver DOMAIN_NAME # Trailing colon is optional when not specifing IP_ADDRESS
--ds-info DS_INFO
--client-id CLIENT_ID
--client-version CLIENT_VERSION
--profile PROFILE_NAME
--queue QUEUE
--language LANGUAGE
DS_INFO is a comma separated list of key-value pairs. The expected pairs are:
keytag=NON_NEGATIVE_INTEGER
algorithm=NON_NEGATIVE_INTEGER
digtype=NON_NEGATIVE_INTEGER
digest=HEX_STRING
=cut
sub cmd_start_domain_test {
my @opts = @_;
my @opt_nameserver;
my $opt_domain;
my $opt_client_id;
my $opt_client_version;
my @opt_ds_info;
my $opt_ipv4;
my $opt_ipv6;
my $opt_profile;
my $opt_queue;
my $opt_language;
GetOptionsFromArray(
\@opts,
'domain|d=s' => \$opt_domain,
'nameserver|n=s' => \@opt_nameserver,
'client-id=s' => \$opt_client_id,
'client-version=s' => \$opt_client_version,
'ds-info=s' => \@opt_ds_info,
'ipv4=s' => \$opt_ipv4,
'ipv6=s' => \$opt_ipv6,
'profile=s' => \$opt_profile,
'queue=s' => \$opt_queue,
'language=s' => \$opt_language,
) or pod2usage( 2 );
my %params = ( domain => $opt_domain, );
if ( $opt_client_id ) {
$params{client_id} = $opt_client_id;
}
if ( $opt_client_version ) {
$params{client_version} = $opt_client_version;
}
if ( @opt_ds_info ) {
my @info_objects;
for my $property_value_pairs ( @opt_ds_info ) {
my %info_object;
for my $pair ( split /,/, $property_value_pairs ) {
my ( $property, $value ) = split /=/, $pair;
if ( $property =~ /^(?:keytag|algorithm|digtype)$/ ) {
$value = 0 + $value;
}
$info_object{$property} = $value;
}
push @info_objects, \%info_object;
}
$params{ds_info} = \@info_objects;
}
if ( @opt_nameserver ) {
my @nameserver_objects;
for my $domain_ip_pair ( @opt_nameserver ) {
my ( $domain, $ip ) = split /:/, $domain_ip_pair, 2;
if ($ip) {
push @nameserver_objects, { ns => $domain, ip => $ip };
} else {
push @nameserver_objects, { ns => $domain };
}
}
$params{nameservers} = \@nameserver_objects;
}
if ( $opt_ipv4 ) {
$params{ipv4} = json_tern( $opt_ipv4 );
}
if ( $opt_ipv6 ) {
$params{ipv6} = json_tern( $opt_ipv6 );
}
if ( $opt_profile ) {
$params{profile} = $opt_profile;
}
if ( $opt_queue ) {
$params{queue} = $opt_queue;
}
if ( $opt_language ) {
$params{language} = $opt_language;
}
return to_jsonrpc(
id => 1,
method => 'start_domain_test',
params => \%params,
);
}
=head2 test_progress
zmb [GLOBAL OPTIONS] test_progress [OPTIONS]
Options:
--test-id TEST_ID
=cut
sub cmd_test_progress {
my @opts = @_;
my $opt_test_id;
GetOptionsFromArray(
\@opts,
'test-id|t=s' => \$opt_test_id,
) or pod2usage( 2 );
return to_jsonrpc(
id => 1,
method => 'test_progress',
params => {
test_id => $opt_test_id,
},
);
}
=head2 get_test_params
zmb [GLOBAL OPTIONS] get_test_params [OPTIONS]
Options:
--test-id TEST_ID
=cut
sub cmd_get_test_params {
my @opts = @_;
my $opt_test_id;
GetOptionsFromArray( #
\@opts,
'test-id|t=s' => \$opt_test_id,
) or pod2usage( 2 );
return to_jsonrpc(
id => 1,
method => 'get_test_params',
params => {
test_id => $opt_test_id,
},
);
}
=head2 get_test_results
zmb [GLOBAL OPTIONS] get_test_results [OPTIONS]
Options:
--test-id TEST_ID
--lang LANGUAGE
=cut
sub cmd_get_test_results {
my @opts = @_;
my $opt_lang;
my $opt_test_id;
GetOptionsFromArray(
\@opts,
'test-id|t=s' => \$opt_test_id,
'lang|l=s' => \$opt_lang,
) or pod2usage( 2 );
return to_jsonrpc(
id => 1,
method => 'get_test_results',
params => {
id => $opt_test_id,
language => $opt_lang,
},
);
}
=head2 get_test_history
zmb [GLOBAL OPTIONS] get_test_history [OPTIONS]
Options:
--domain DOMAIN_NAME
--filter all|delegated|undelegated
--offset COUNT
--limit COUNT
=cut
sub cmd_get_test_history {
my @opts = @_;
my $opt_filter;
my $opt_domain;
my $opt_offset;
my $opt_limit;
GetOptionsFromArray(
\@opts,
'domain|d=s' => \$opt_domain,
'filter|n=s' => \$opt_filter,
'offset|o=i' => \$opt_offset,
'limit|l=i' => \$opt_limit,
) or pod2usage( 2 );
my %params = (
frontend_params => {
domain => $opt_domain,
},
);
if ( $opt_filter ) {
unless ( $opt_filter =~ /^(?:all|delegated|undelegated)$/ ) {
die 'Illegal filter value. Expects "all", "delegated" or "undelegated" ';
}
$params{filter} = $opt_filter;
}
if ( defined $opt_offset ) {
$params{offset} = $opt_offset;
}
if ( defined $opt_limit ) {
$params{limit} = $opt_limit;
}
return to_jsonrpc(
id => 1,
method => 'get_test_history',
params => \%params,
);
}
=head2 add_api_user
zmb [GLOBAL OPTIONS] add_api_user [OPTIONS]
Options:
--username USERNAME
--api-key API_KEY
=cut
sub cmd_add_api_user {
my @opts = @_;
my $opt_username;
my $opt_api_key;
GetOptionsFromArray(
\@opts,
'username|u=s' => \$opt_username,
'api-key|a=s' => \$opt_api_key,
) or pod2usage( 2 );
return to_jsonrpc(
id => 1,
method => 'add_api_user',
params => {
username => $opt_username,
api_key => $opt_api_key,
},
);
}
=head2 add_batch_job
zmb [GLOBAL OPTIONS] add_batch_job [OPTIONS]
Options:
--username USERNAME
--api-key API_KEY
--domain DOMAIN_NAME
--ipv4 true|false|null
--ipv6 true|false|null
--nameserver DOMAIN_NAME:IP_ADDRESS
--nameserver DOMAIN_NAME # Trailing colon is optional when not specifing IP_ADDRESS
--ds-info DS_INFO
--client-id CLIENT_ID
--client-version CLIENT_VERSION
--profile PROFILE_NAME
--queue QUEUE
--file FILENAME
"--domain" is repeated for each domain to be tested.
"--nameserver" can be repeated for each name server.
"--ds-info" can be repeated for each DS record.
"--file" points at a file with a list of domain names
to test, one name per line. Lines starting with "#",
empty lines and lines with white space only are
ignored. Trailing white space is ignored.
"--file" and "--domain" can be combined. Domains specified
by any "--domain" are added before those specified in the
file, if any.
DS_INFO is a comma separated list of key-value pairs. The expected pairs are:
keytag=NON_NEGATIVE_INTEGER
algorithm=NON_NEGATIVE_INTEGER
digtype=NON_NEGATIVE_INTEGER
digest=HEX_STRING
=cut
sub cmd_add_batch_job {
my @opts = @_;
my $opt_username;
my $opt_api_key;
my @opt_nameserver;
my @opt_domains;
my $opt_file;
my $opt_client_id;
my $opt_client_version;
my @opt_ds_info;
my $opt_ipv4;
my $opt_ipv6;
my $opt_profile;
my $opt_queue;
GetOptionsFromArray(
\@opts,
'username|u=s' => \$opt_username,
'api-key|a=s' => \$opt_api_key,
'domain|d=s' => \@opt_domains,
'nameserver|n=s' => \@opt_nameserver,
'client-id=s' => \$opt_client_id,
'client-version=s' => \$opt_client_version,
'ds-info=s' => \@opt_ds_info,
'ipv4=s' => \$opt_ipv4,
'ipv6=s' => \$opt_ipv6,
'profile=s' => \$opt_profile,
'queue=s' => \$opt_queue,
'file=s' => \$opt_file,
) or pod2usage( 2 );
if ($opt_file) {
open( my $fh, "<", $opt_file ) or die "Can't open < $opt_file: $!";
while( <$fh> ) {
chomp;
s/\s+$//;
s/^\s+//;
next if /^#/ or /^$/;
push( @opt_domains, decode_utf8( $_ ) );
};
};
my %params = ( domains => \@opt_domains );
$params{username} = $opt_username;
$params{api_key} = $opt_api_key;
if ( $opt_client_id ) {
$params{test_params}{client_id} = $opt_client_id;
}
if ( $opt_client_version ) {
$params{test_params}{client_version} = $opt_client_version;
}
if ( @opt_ds_info ) {
my @info_objects;
for my $property_value_pairs ( @opt_ds_info ) {
my %info_object;
for my $pair ( split /,/, $property_value_pairs ) {
my ( $property, $value ) = split /=/, $pair;
if ( $property =~ /^(?:keytag|algorithm|digtype)$/ ) {
$value = 0 + $value;
}
$info_object{$property} = $value;
}
push @info_objects, \%info_object;
}
$params{test_params}{ds_info} = \@info_objects;
}
if ( @opt_nameserver ) {
my @nameserver_objects;
for my $domain_ip_pair ( @opt_nameserver ) {
my ( $domain, $ip ) = split /:/, $domain_ip_pair, 2;
$ip //= "";
push @nameserver_objects,
{
ns => $domain,
ip => $ip,
};
}
$params{test_params}{nameservers} = \@nameserver_objects;
}
if ( $opt_ipv4 ) {
$params{test_params}{ipv4} = json_tern( $opt_ipv4 );
}
if ( $opt_ipv6 ) {
$params{test_params}{ipv6} = json_tern( $opt_ipv6 );
}
if ( $opt_profile ) {
$params{test_params}{profile} = $opt_profile;
}
if ( $opt_queue ) {
$params{test_params}{queue} = $opt_queue;
}
return to_jsonrpc(
id => 1,
method => 'add_batch_job',
params => \%params,
);
}
=head2 batch_status
zmb [GLOBAL OPTIONS] batch_status [OPTIONS]
Options:
--batch-id BATCH-ID|--bi BATCH-ID
--list-waiting-tests true|false|null
--list-running-tests true|false|null
--list-finished-tests true|false|null
--lw # Same as "--list-waiting-tests true"
--lr # Same as "--list-running-tests true"
--lf # Same as "--list-finished-tests true"
"--batch-id" is mandatory.
The command provides the number of tests waiting to be run, tests running and
test finished, respectively, for the batch.
"--list-waiting-tests", "--list-running-tests" and "--list-finished-tests" are
optional. If given the test IDs of tests waiting to be run, tests running
and test finished, respectively, are listed.
"--lw", "--lr" and "--lf" are option.
"--lw" must not be combined with "--list-waiting-tests". "--lr" must not be
combined with "--list-running-tests". "--lf" must not be combined with
"--list-finished-tests".
=cut
sub cmd_batch_status {
my @opts = @_;
my $opt_batch_id;
my $opt_list_waiting_tests;
my $opt_lw;
my $opt_list_running_tests;
my $opt_lr;
my $opt_list_finished_tests;
my $opt_lf;
GetOptionsFromArray(
\@opts,
'batch-id|bi=s' => \$opt_batch_id,
'list-waiting-tests=s' => \$opt_list_waiting_tests,
'lw' => \$opt_lw,
'list-running-tests=s' => \$opt_list_running_tests,
'lr' => \$opt_lr,
'list-finished-tests=s' => \$opt_list_finished_tests,
'lf' => \$opt_lf,
) or pod2usage( 2 );
pod2usage( "'--lw' and '--list-waiting-test' must not be combined" ) if defined $opt_list_waiting_tests and $opt_lw;
pod2usage( "'--lr' and '--list-running-test' must not be combined" ) if defined $opt_list_running_tests and $opt_lr;
pod2usage( "'--lf' and '--list-finished-test' must not be combined" ) if defined $opt_list_finished_tests and $opt_lf;
my %params;
$params{batch_id} = $opt_batch_id;
$params{list_waiting_tests} = json_tern( $opt_list_waiting_tests ) if $opt_list_waiting_tests and json_tern( $opt_list_waiting_tests );
$params{list_running_tests} = json_tern( $opt_list_running_tests ) if $opt_list_running_tests and json_tern( $opt_list_running_tests );
$params{list_finished_tests} = json_tern( $opt_list_finished_tests ) if $opt_list_finished_tests and json_tern( $opt_list_finished_tests );
$params{list_waiting_tests} = JSON::PP::true if $opt_lw;
$params{list_running_tests} = JSON::PP::true if $opt_lr;
$params{list_finished_tests} = JSON::PP::true if $opt_lf;
return to_jsonrpc(
id => 1,
method => 'batch_status',
params => \%params,
);
}
sub show_commands {
my %specials = (
man => 'Show the full manual page.',
non_existing_method => 'Call a non-existing RPC method.',
);
my @commands = get_commands();
my $max_width = 0;
for my $command ( @commands ) {
$max_width = length $command if length $command > $max_width;
}
say "Commands:";
for my $command ( @commands ) {
if ( exists $specials{$command} ) {
printf " %-*s %s\n", $max_width, $command, $specials{$command};
}
else {
say " ", $command;
}
}
}
sub get_commands {
no strict 'refs';
return sort
map { $_ =~ s/^cmd_//r }
grep { $_ =~ /^cmd_/ } grep { defined &{"main\::$_"} } keys %{"main\::"};
}
sub json_tern {
my $value = shift;
if ( $value eq 'true' ) {
return JSON::PP::true;
}
elsif ( $value eq 'false' ) {
return JSON::PP::false;
}
elsif ( $value eq 'null' ) {
return undef;
}
else {
die 'Illegal value. Expects "true", "false" or "null" ';
}
}
sub to_jsonrpc {
my %args = @_;
my $id = $args{id};
my $method = $args{method};
my $request = {
jsonrpc => "2.0",
method => $method,
id => $id,
};
if ( exists $args{params} ) {
$request->{params} = $args{params};
}
return encode_json( $request );
}
sub to_request {
my $server = shift;
my $json = shift;
my $req = HTTP::Request->new( POST => $server );
$req->content_type( 'application/json' );
$req->content( $json );
return $req;
}
sub submit {
my $req = shift;
my $ua = LWP::UserAgent->new;
my $res = $ua->request( $req );
if ( $res->is_success ) {
return $res->decoded_content;
}
else {
die $res->status_line;
}
}
main( @ARGV );

View File

@@ -0,0 +1,87 @@
#!/bin/sh
bindir="$(dirname "$0")"
ZMB="${bindir}/zmb"
JQ="$(which jq)"
usage () {
status="$1"
message="$2"
[ -n "$message" ] && printf "%s\n" "${message}" >&2
echo "Usage: zmtest [OPTIONS] DOMAIN" >&2
echo >&2
echo "Options:" >&2
echo " -h --help Show usage (this documentation)." >&2
echo " -s URL --server URL Zonemaster Backend to query. Default is http://localhost:5000/" >&2
echo " --noipv4 Run the test with IPv4 disabled." >&2
echo " --noipv6 Run the test with IPv6 disabled." >&2
echo " IPv4 and IPv6 follow the profile setting unless disabled by option." >&2
echo " --lang LANG A language tag. Default is \"en\"." >&2
echo " Valid values are determined by backend_config.ini." >&2
echo " --profile PROFILE The name of a profile. Default is \"default\"." >&2
echo " Valid values are determined by backend_config.ini except that" >&2
echo " \"default\" is always a valid value." >&2
exit "${status}"
}
error () {
status="$1"
message="$2"
printf "error: %s\n" "${message}" >&2
exit "${status}"
}
zmb () {
server_url="$1"; shift
output="$("${ZMB}" --server="${server_url}" "$@" 2>&1)" || error 1 "method $1: ${output}"
json="$(printf "%s" "${output}" | "${JQ}" -S . 2>/dev/null)" || error 1 "method $1 did not return valid JSON output: ${output}"
error="$(printf "%s" "${json}" | "${JQ}" -e .error 2>/dev/null)" && error 1 "method $1: ${error}"
printf "%s" "${json}"
}
[ -n "${JQ}" ] || error 2 "Dependency not found: jq"
domain=""
server_url="http://localhost:5000/"
ipv4=""
ipv6=""
lang="en"
profile="default"
while [ $# -gt 0 ] ; do
case "$1" in
-h|--help) usage 2; shift 1;;
-s|--server) server_url="$2"; shift 2;;
--noipv4) ipv4='--ipv4 false'; shift 1;;
--noipv6) ipv6='--ipv6 false'; shift 1;;
--lang) lang="$2"; shift 2;;
--profile) profile="$2"; shift 2;;
*) domain="$1" ; shift 1;;
esac
done
[ -n "${domain}" ] || usage 2 "No domain specified"
# Start test
output="$(zmb "${server_url}" start_domain_test --domain "${domain}" ${ipv4} ${ipv6} --profile "${profile}")" || exit $?
testid="$(printf "%s" "${output}" | "${JQ}" -r .result)" || exit $?
printf "testid: %s\n" "${testid}" >&2
if echo "${testid}" | grep -qE '[^0-9a-fA-F]' ; then
error 1 "start_domain_test did not return a testid: ${testid}"
fi
# Wait for test to finish
while true
do
output="$(zmb "${server_url}" test_progress --test-id "${testid}")" || exit $?
progress="$(printf "%s" "${output}" | "${JQ}" -r .result)" || exit $?
printf "\r${progress}%% done" >&2
if [ "${progress}" -eq 100 ] ; then
echo >&2
break
fi
sleep 1
done
# Get test results
zmb "${server_url}" get_test_results --test-id "${testid}" --lang "${lang}"

View File

@@ -0,0 +1,252 @@
#!/usr/bin/env perl
use strict;
use warnings;
our $VERSION = '1.1.0';
use 5.14.2;
use English qw( $PID );
use JSON::PP;
use JSON::RPC::Dispatch;
use Log::Any qw( $log );
use Log::Any::Adapter;
use POSIX;
use Plack::Builder;
use Plack::Response;
use Router::Simple::Declare;
use Try::Tiny;
BEGIN {
$ENV{PERL_JSON_BACKEND} = 'JSON::PP';
undef $ENV{LANGUAGE};
};
use Zonemaster::Backend::RPCAPI;
use Zonemaster::Backend::Config;
use Zonemaster::Backend::Metrics;
local $| = 1;
Log::Any::Adapter->set(
'+Zonemaster::Backend::Log',
log_level => $ENV{ZM_BACKEND_RPCAPI_LOGLEVEL},
json => $ENV{ZM_BACKEND_RPCAPI_LOGJSON},
stderr => 1
);
$SIG{__WARN__} = sub {
$log->warning(map s/^\s+|\s+$//gr, map s/\n/ /gr, @_);
};
my $config = Zonemaster::Backend::Config->load_config();
Zonemaster::Backend::Metrics->setup($config->METRICS_statsd_host, $config->METRICS_statsd_port);
Zonemaster::Engine::init_engine();
builder {
enable sub {
my $app = shift;
# Make sure we can connect to the database
$config->new_DB();
return $app;
};
};
my $handler = Zonemaster::Backend::RPCAPI->new( { config => $config } );
my $router = router {
############## FRONTEND ####################
connect "version_info" => {
handler => $handler,
action => "version_info"
};
# Experimental
connect "system_versions" => {
handler => $handler,
action => "system_versions"
};
connect "profile_names" => {
handler => $handler,
action => "profile_names"
};
# Experimental
connect "conf_profiles" => {
handler => $handler,
action => "conf_profiles"
};
connect "get_language_tags" => {
handler => $handler,
action => "get_language_tags"
};
# Experimental
connect "conf_languages" => {
handler => $handler,
action => "conf_languages"
};
connect "get_host_by_name" => {
handler => $handler,
action => "get_host_by_name"
};
# Experimental
connect "lookup_address_records" => {
handler => $handler,
action => "lookup_address_records"
};
connect "get_data_from_parent_zone" => {
handler => $handler,
action => "get_data_from_parent_zone"
};
# Experimental
connect "lookup_delegation_data" => {
handler => $handler,
action => "lookup_delegation_data"
};
connect "start_domain_test" => {
handler => $handler,
action => "start_domain_test"
};
# Experimental
connect "job_create" => {
handler => $handler,
action => "job_create"
};
connect "test_progress" => {
handler => $handler,
action => "test_progress"
};
# Experimental
connect "job_status" => {
handler => $handler,
action => "job_status"
};
connect "get_test_params" => {
handler => $handler,
action => "get_test_params"
};
# Experimental
connect "job_params" => {
handler => $handler,
action => "job_params"
};
connect "get_test_results" => {
handler => $handler,
action => "get_test_results"
};
# Experimental
connect "job_results" => {
handler => $handler,
action => "job_results"
};
connect "get_test_history" => {
handler => $handler,
action => "get_test_history"
};
# Experimental
connect "domain_history" => {
handler => $handler,
action => "domain_history"
};
connect "batch_status" => {
handler => $handler,
action => "batch_status"
};
};
if ( $config->RPCAPI_enable_user_create or $config->RPCAPI_enable_add_api_user ) {
$log->info('Enabling add_api_user method');
$router->connect("add_api_user", {
handler => $handler,
action => "add_api_user"
});
$router->connect("user_create", {
handler => $handler,
action => "user_create"
});
}
if ( $config->RPCAPI_enable_batch_create or $config->RPCAPI_enable_add_batch_job ) {
$log->info('Enabling add_batch_job method');
$router->connect("add_batch_job", {
handler => $handler,
action => "add_batch_job"
});
$router->connect("batch_create", {
handler => $handler,
action => "batch_create"
});
}
my $dispatch = JSON::RPC::Dispatch->new(
router => $router,
);
my $rpcapi_app = sub {
my $env = shift;
my $req = Plack::Request->new($env);
my $res = {};
my $content = {};
my $json_error = '';
try {
my $json = $req->content;
$content = decode_json($json);
} catch {
$json_error = (split /at \//, $_)[0];
};
if ($json_error eq '') {
my $errors = $handler->jsonrpc_validate($content);
if ($errors ne '') {
$res = Plack::Response->new(200);
$res->content_type('application/json');
$res->body( encode_json($errors) );
$res->finalize;
} else {
local $log->context->{rpc_method} = $content->{method};
$res = $dispatch->handle_psgi($env, $env->{REMOTE_ADDR});
my $status = Zonemaster::Backend::Metrics->code_to_status(decode_json(@{@$res[2]}[0])->{error}->{code});
Zonemaster::Backend::Metrics::increment("zonemaster.rpcapi.requests.$content->{method}.$status");
$res;
}
} else {
$res = Plack::Response->new(200);
$res->content_type('application/json');
$res->body( encode_json({
jsonrpc => '2.0',
id => undef,
error => {
code => '-32700',
message => 'Invalid JSON was received by the server.',
data => "$json_error"
}}) );
$res->finalize;
}
};
builder {
enable "Plack::Middleware::ReverseProxy";
mount "/" => $rpcapi_app;
};

View File

@@ -0,0 +1,293 @@
#!/usr/bin/env perl
use 5.14.2;
use warnings;
use Zonemaster::Backend::TestAgent;
use Zonemaster::Backend::Config;
use Zonemaster::Backend::Metrics;
use Parallel::ForkManager;
use Daemon::Control;
use Log::Any qw( $log );
use Log::Any::Adapter;
use English;
use Pod::Usage;
use Getopt::Long;
use POSIX;
use Time::HiRes qw[time sleep gettimeofday tv_interval];
use sigtrap qw(die normal-signals);
###
### Compile-time stuff.
###
BEGIN {
$ENV{PERL_JSON_BACKEND} = 'JSON::PP';
undef $ENV{LANGUAGE};
}
# Enable immediate flush to stdout and stderr
$|++;
###
### More global variables, and initialization.
###
my $pidfile;
my $user;
my $group;
my $logfile;
my $loglevel;
my $logjson;
my $opt_outfile;
my $opt_help;
GetOptions(
'help!' => \$opt_help,
'pidfile=s' => \$pidfile,
'user=s' => \$user,
'group=s' => \$group,
'logfile=s' => \$logfile,
'loglevel=s' => \$loglevel,
'logjson!' => \$logjson,
'outfile=s' => \$opt_outfile,
) or pod2usage( "Try '$0 --help' for more information." );
pod2usage( -verbose => 1 ) if $opt_help;
$pidfile //= '/tmp/zonemaster_backend_testagent.pid';
$logfile //= '/var/log/zonemaster/zonemaster_backend_testagent.log';
$opt_outfile //= '/var/log/zonemaster/zonemaster_backend_testagent.out';
$loglevel //= 'info';
$loglevel = lc $loglevel;
Log::Any::Adapter->set(
'+Zonemaster::Backend::Log',
log_level => $loglevel,
json => $logjson,
file => $logfile,
);
$SIG{__WARN__} = sub {
$log->warning(map s/^\s+|\s+$//gr, map s/\n/ /gr, @_);
};
###
### Actual functionality
###
sub main {
my $self = shift;
my $caught_sigterm = 0;
my $catch_sigterm;
$catch_sigterm = sub {
$SIG{TERM} = $catch_sigterm;
$caught_sigterm = 1;
$log->notice( "Daemon caught SIGTERM" );
return;
};
local $SIG{TERM} = $catch_sigterm;
my $agent = Zonemaster::Backend::TestAgent->new( { config => $self->config } );
while ( !$caught_sigterm ) {
my $cleanup_timer = [ gettimeofday ];
$self->pm->reap_finished_children(); # Reaps terminated child processes
$self->pm->on_wait(); # Sends SIGKILL to overdue child processes
Zonemaster::Backend::Metrics::gauge("zonemaster.testagent.maximum_processes", $self->pm->max_procs);
Zonemaster::Backend::Metrics::gauge("zonemaster.testagent.running_processes", scalar($self->pm->running_procs));
Zonemaster::Backend::Metrics::timing("zonemaster.testagent.cleanup_duration_seconds", tv_interval($cleanup_timer) * 1000);
my $fetch_test_timer = [ gettimeofday ];
my ( $test_id, $batch_id );
eval {
$self->db->process_unfinished_tests(
$self->config->ZONEMASTER_lock_on_queue,
$self->config->ZONEMASTER_max_zonemaster_execution_time,
);
( $test_id, $batch_id ) = $self->db->get_test_request( $self->config->ZONEMASTER_lock_on_queue );
Zonemaster::Backend::Metrics::timing("zonemaster.testagent.fetchtests_duration_seconds", tv_interval($fetch_test_timer) * 1000);
};
if ( $@ ) {
$log->error( $@ );
}
my $show_progress = defined $batch_id ? 0 : 1;
if ( $test_id ) {
$log->infof( "Test found: %s", $test_id );
if ( $self->pm->start( $test_id ) == 0 ) { # Forks off child process
$log->infof( "Test starting: %s", $test_id );
Zonemaster::Backend::Metrics::increment("zonemaster.testagent.tests_started");
my $start_time = [ gettimeofday ];
eval { $agent->run( $test_id, $show_progress ) };
if ( $@ ) {
chomp $@;
Zonemaster::Backend::Metrics::increment("zonemaster.testagent.tests_died");
$log->errorf( "Test died: %s: %s", $test_id, $@ );
$self->db->process_dead_test( $test_id )
}
else {
Zonemaster::Backend::Metrics::increment("zonemaster.testagent.tests_completed");
$log->infof( "Test completed: %s", $test_id );
}
Zonemaster::Backend::Metrics::timing("zonemaster.testagent.tests_duration_seconds", tv_interval($start_time) * 1000);
$agent->reset();
$self->pm->finish; # Terminates child process
}
}
else {
sleep $self->config->DB_polling_interval;
}
}
$log->notice( "Daemon entered graceful shutdown" );
$self->pm->wait_all_children(); # Includes SIGKILLing overdue child processes
return;
}
sub preflight_checks {
# Make sure we can load the configuration file
$log->debug("Starting pre-flight check");
my $initial_config = Zonemaster::Backend::Config->load_config();
Zonemaster::Backend::Metrics->setup($initial_config->METRICS_statsd_host, $initial_config->METRICS_statsd_port);
# Validate the Zonemaster-Engine profile
Zonemaster::Backend::TestAgent->new( { config => $initial_config } );
# Connect to the database
$initial_config->new_DB();
$log->debug("Completed pre-flight check");
return $initial_config;
}
my $initial_config;
# Make sure the environment is alright before forking (only on startup)
if ( grep /^foreground$|^restart$|^start$/, @ARGV ) {
eval {
$initial_config = preflight_checks();
};
if ( $@ ) {
$log->critical( "Aborting startup: $@" );
print STDERR "Aborting startup: $@";
exit 1;
}
}
###
### Daemon Control stuff.
###
my $daemon = Daemon::Control->with_plugins( qw( +Zonemaster::Backend::Config::DCPlugin ) )->new(
{
name => 'zonemaster-testagent',
program => sub {
my $self = shift;
$log->notice( "Daemon spawned" );
$self->init_backend_config( $initial_config );
undef $initial_config;
eval { main( $self ) };
if ( $@ ) {
chomp $@;
$log->critical( $@ );
}
$log->notice( "Daemon terminating" );
},
pid_file => $pidfile,
stderr_file => $opt_outfile,
stdout_file => $opt_outfile,
}
);
$daemon->init_config( $ENV{PERLBREW_ROOT} . '/etc/bashrc' ) if ( $ENV{PERLBREW_ROOT} );
$daemon->user($user) if $user;
$daemon->group($group) if $group;
exit $daemon->run;
=head1 NAME
zonemaster_backend_testagent - Init script for Zonemaster Test Agent.
=head1 SYNOPSIS
zonemaster_backend_testagent [OPTIONS] [COMMAND]
=head1 OPTIONS
=over 4
=item B<--help>
Print a brief help message and exits.
=item B<--user=USER>
When specified the daemon will drop to the user with this username when forked.
=item B<--group=GROUP>
When specified the daemon will drop to the group with this groupname when forked.
=item B<--pidfile=FILE>
The location of the PID file to use.
=item B<--logfile=FILE>
The location of the log file to use.
When FILE is -, the log is written to standard output.
=item B<--loglevel=LEVEL>
The location of the log level to use.
The allowed values are specified at L<Log::Any/LOG-LEVELS>.
=item B<--logjson>
Enable JSON logging when specified.
=item B<COMMAND>
One of the following:
=over 4
=item start
=item foreground
=item stop
=item restart
=item reload
=item status
=item get_init_file
=back
=back
=cut

View File

@@ -0,0 +1,56 @@
.POSIX:
.SUFFIXES: .po .mo
.PHONY: all check-msg-args dist extract-pot tidy-po show-fuzzy update-po new-po check-po
POFILES := $(shell find . -maxdepth 1 -type f -name '*.po' -exec basename {} \;)
MOFILES := $(POFILES:%.po=%.mo)
POTFILE = Zonemaster-Backend.pot
PMFILES := $(shell find ../lib -type f -name '*.pm' | sort)
all: $(MOFILES)
@echo
@echo Remember to make sure all of the above names are in the
@echo MANIFEST file, or they will not be installed.
@echo
# Tidy the formatting of all PO files
tidy-po:
@tmpdir="`mktemp -d tidy-po.XXXXXXXX`" ;\
trap 'rm -rf "$$tmpdir"' EXIT ;\
for f in $(POFILES) ; do msgcat $$f -o $$tmpdir/$$f && mv -f $$tmpdir/$$f $$f ; done
update-po: extract-pot
@for f in $(POFILES) ; do msgmerge --update --backup=none --quiet --no-location $(MSGMERGE_OPTS) $$f $(POTFILE) ; done
# Create a new empty PO file with basename provided with the POLANG variable
# Update the Language field in the header
new-po: extract-pot
@[ -n "$(POLANG)" ] || ( echo "Usage: make POLANG=xx new-po" && exit 1 )
@cp $(POTFILE) $(POLANG).po
@perl -pi -e 's/^("Project-Id-Version:) .+(\\n)/$$1 1.0.0$$2/;' \
-e 's/^("Language-Team:) .+(\\n)/$$1 Zonemaster Team$$2/;' \
-e 's/^"Language: /$$&$(POLANG)/;' \
-e 's/^("Content-Type:.+charset=)CHARSET/$${1}UTF-8/;' $(POLANG).po
@perl -ni -e 'print unless /^#( |$$)/' $(POLANG).po
# Check the msgid/msgstr pair for some inconsistencies between them in the
# selected PO file and report on standard error any errors found. The PO file
# is not updated.
check-po:
@for f in $(POFILES) ; do msgfmt -c $$f ; done
extract-pot:
@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)
$(POTFILE): extract-pot
.po.mo:
@msgfmt -o $@ $<
@mkdir -p locale/`basename $@ .mo`/LC_MESSAGES
@ln -vf $@ locale/`basename $@ .mo`/LC_MESSAGES/Zonemaster-Backend.mo
show-fuzzy:
@for f in $(POFILES) ; do msgattrib --only-fuzzy $$f ; done
check-msg-args:
@for f in $(POFILES) ; do ../util/check-msg-args $$f ; done

View File

@@ -0,0 +1,14 @@
# This is a wrapper for BSD Make (FreeBSD) to execute
# GNU Make (gmake) and the primary makefile GNUmakefile.
GNUMAKE ?= gmake
FILES != ls *
# File targets should be evaluated by gmake.
.PHONY: all $(FILES)
all:
@${GNUMAKE} $@
.DEFAULT:
@${GNUMAKE} $@

View File

@@ -0,0 +1,20 @@
[DB]
engine=MySQL
polling_interval=0.5
#seconds
[MYSQL]
host=localhost
database=zonemaster
user=ci
password=password
[ZONEMASTER]
max_zonemaster_execution_time=300
number_of_processes_for_frontend_testing=20
number_of_processes_for_batch_testing=20
#seconds
[LANGUAGE]
locale = da_DK en_US es_ES fi_FI fr_FR nb_NO sv_SE

View File

@@ -0,0 +1,20 @@
[DB]
engine=PostgreSQL
polling_interval=0.5
#seconds
[POSTGRESQL]
host=localhost
database=zonemaster
user=ci
password=password
[ZONEMASTER]
max_zonemaster_execution_time=300
number_of_processes_for_frontend_testing=20
number_of_processes_for_batch_testing=20
#seconds
[LANGUAGE]
locale = da_DK en_US es_ES fi_FI fr_FR nb_NO sv_SE

View File

@@ -0,0 +1,17 @@
[DB]
engine=SQLite
polling_interval=0.5
#seconds
[SQLITE]
database_file=/tmp/zonemaster.sqlite
[ZONEMASTER]
max_zonemaster_execution_time=300
number_of_processes_for_frontend_testing=20
number_of_processes_for_batch_testing=20
#seconds
[LANGUAGE]
locale = da_DK en_US es_ES fi_FI fr_FR nb_NO sv_SE

View File

@@ -0,0 +1,50 @@
# For documentation of the backend_config.ini file see
# https://github.com/zonemaster/zonemaster/blob/master/docs/public/configuration/backend.md
[DB]
engine = SQLite
polling_interval = 0.5
[MYSQL]
host = localhost
user = zonemaster
password = zonemaster
database = zonemaster
[POSTGRESQL]
host = localhost
user = zonemaster
password = zonemaster
database = zonemaster
[SQLITE]
database_file = /var/lib/zonemaster/db.sqlite
[ZONEMASTER]
#max_zonemaster_execution_time = 600
#number_of_processes_for_frontend_testing = 20
#number_of_processes_for_batch_testing = 20
#lock_on_queue = 0
#age_reuse_previous_test = 600
[RPCAPI]
# Uncomment to enable API method "add_api_user"
#enable_add_api_user = yes
# Uncomment to disable API method "add_batch_job"
#enable_add_batch_job = no
[LANGUAGE]
locale = da_DK en_US es_ES fi_FI fr_FR nb_NO sl_SI sv_SE
[PUBLIC PROFILES]
#example_profile_1=/example/directory/test1_profile.json
#default=/example/directory/default_profile.json
[PRIVATE PROFILES]
#example_profile_2=/example/directory/test2_profile.json
[METRICS]
# Uncoment the following option to enable the metrics feature
#statsd_host = localhost
#statsd_port = 8125

View File

@@ -0,0 +1,3 @@
-- Remove Zonemaster data from database
DROP DATABASE zonemaster;
DROP USER 'zonemaster'@'localhost';

View File

@@ -0,0 +1,3 @@
-- Remove Zonemaster data from database
DROP DATABASE zonemaster;
DROP USER zonemaster;

View File

@@ -0,0 +1,15 @@
#!/usr/bin/env perl
use strict;
use warnings;
use Zonemaster::Backend::Config;
use Zonemaster::Backend::DB;
my $config = Zonemaster::Backend::Config->load_config();
my $db_engine = $config->DB_engine;
my $db_class = Zonemaster::Backend::DB->get_db_class( $db_engine );
my $db = $db_class->from_config( $config );
$db->create_schema();

View File

@@ -0,0 +1,89 @@
msgid ""
msgstr ""
"Project-Id-Version: 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-05-26 14:36+0000\n"
"PO-Revision-Date: 2023-05-26 14:33+0000\n"
"Last-Translator: haarbo@dk-hostmaster.dk\n"
"Language-Team: Zonemaster project\n"
"Language: da\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "Invalid method parameter(s)."
msgstr "Invalid metodeparametre"
msgid "Missing property"
msgstr "Manglende egenskab"
msgid ""
"Warning: Zonemaster::LDNS not compiled with IDN support, cannot handle non-"
"ASCII names correctly."
msgstr ""
"Advarsel: Zonemaster::LDNS ikke kompileret med IDN-support, og kan derfor "
"ikke håndtere ikke-ascii-navne korrekt"
#. BACKEND_TEST_AGENT:TEST_DIED
msgid "An error occured and Zonemaster could not start or finish the test."
msgstr ""
"Der opstod en fejl, og Zonemaster kunne ikke starte eller afslutte testen."
#. BACKEND_TEST_AGENT:UNABLE_TO_FINISH_TEST
#, perl-brace-format
msgid ""
"The test took too long to run (the current limit is {max_execution_time} "
"seconds). Maybe there are too many name servers or the name servers are "
"either unreachable or not responsive enough."
msgstr ""
"Testen tog for lang tid at afvikle (den aktuelle grænse er "
"{max_execution_time} sekunder). Måske er der for mange navneservere, eller "
"også er navneserverne enten ikke tilgængelige eller ikke responsive nok."
msgid "Invalid digest format"
msgstr "Invalid digest-format"
msgid "Algorithm must be a positive integer"
msgstr "Algoritmen skal være et positivt heltal"
msgid "Digest type must be a positive integer"
msgstr "Digest type skal være et positivt heltal"
msgid "Keytag must be a positive integer"
msgstr "Keytag skal være et positivt heltal"
msgid "Domain name required"
msgstr "Domænenavn påkrævet"
msgid "The domain name is IDNA invalid"
msgstr "Domænenavnet er IDNA ugyldigt"
msgid ""
"The domain name contains non-ascii characters and IDNA support is not "
"installed"
msgstr ""
"Domænenavnet indeholder ikke-ascii-tegn og IDNA-support er ikke installeret"
msgid "The domain name character(s) are not supported"
msgstr "Domænenavnets tegn er ikke understøttet"
msgid "The domain name contains consecutive dots"
msgstr "Domænenavnet indeholder flere dots (.) efter hinanden"
msgid "The domain name or label is too long"
msgstr "Domænenavnet eller labelen er for lang"
msgid "Invalid language tag format"
msgstr "Invalidt sprogkode-format"
msgid "Unkown language string"
msgstr "Ukendt sprogstreng"
msgid "Invalid IP address"
msgstr "Invalid IP-adresse"
msgid "Invalid profile format"
msgstr "Invalidt profil-format"
msgid "Unknown profile"
msgstr "Ukendt profil"

View File

@@ -0,0 +1,91 @@
msgid ""
msgstr ""
"Project-Id-Version: 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-05-21 21:33+0000\n"
"PO-Revision-Date: 2023-05-21 21:32+0000\n"
"Last-Translator: hsalgado@vulcano.cl\n"
"Language-Team: Zonemaster project\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.0\n"
msgid "Invalid method parameter(s)."
msgstr "Parámetro(s) de método inválido."
msgid "Missing property"
msgstr "Propiedad no incluída"
msgid ""
"Warning: Zonemaster::LDNS not compiled with IDN support, cannot handle non-"
"ASCII names correctly."
msgstr ""
"Advertencia: Zonemaster::LDNS no fue compilado con soporte IDN, no se puede "
"manejar correctamente los nombres no-ASCII."
#. BACKEND_TEST_AGENT:TEST_DIED
msgid "An error occured and Zonemaster could not start or finish the test."
msgstr ""
"Ha ocurrido un error y Zonemaster no pudo comenzar o finalizar la prueba."
#. BACKEND_TEST_AGENT:UNABLE_TO_FINISH_TEST
#, perl-brace-format
msgid ""
"The test took too long to run (the current limit is {max_execution_time} "
"seconds). Maybe there are too many name servers or the name servers are "
"either unreachable or not responsive enough."
msgstr ""
"La prueba tomó demasiado tiempo en terminar (el límite máximo actual es "
"{max_execution_time} segundos). Quizás hay demasiados servidores de nombres, "
"o son inalcanzables, o no lo suficientemente rápidos para responder."
msgid "Invalid digest format"
msgstr "Formato de resumen (digest) inválido"
msgid "Algorithm must be a positive integer"
msgstr "El algoritmo debe ser un entero positivo"
msgid "Digest type must be a positive integer"
msgstr "El tipo de resumen (digest) debe ser un entero positivo"
msgid "Keytag must be a positive integer"
msgstr "El tag de llave debe ser un entero positivo"
msgid "Domain name required"
msgstr "Se necesita el nombre de dominio"
msgid "The domain name is IDNA invalid"
msgstr "El nombre de dominio es inválido según IDNA"
msgid ""
"The domain name contains non-ascii characters and IDNA support is not "
"installed"
msgstr ""
"El nombre de dominio contiene caracteres no-ascii, y el soporte IDNA no está "
"instalado"
msgid "The domain name character(s) are not supported"
msgstr "Los caracteres del nombre de dominio no están soportados"
msgid "The domain name contains consecutive dots"
msgstr "El nombre de dominio contiene puntos consecutivos"
msgid "The domain name or label is too long"
msgstr "El nombre de dominio o la etiqueta es muy larga"
msgid "Invalid language tag format"
msgstr "Formato de descriptor de idioma inválido"
msgid "Unkown language string"
msgstr "Descriptor de idioma desconocido"
msgid "Invalid IP address"
msgstr "Dirección IP inválida"
msgid "Invalid profile format"
msgstr "Formato de perfil (profile) inválido"
msgid "Unknown profile"
msgstr "Perfil desconocido"

View File

@@ -0,0 +1,91 @@
msgid ""
msgstr ""
"Project-Id-Version: 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-05-23 14:04+0000\n"
"PO-Revision-Date: 2023-05-23 14:03+0000\n"
"Last-Translator: mats.dufberg@iis.se\n"
"Language-Team: Zonemaster project\n"
"Language: fi\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.0\n"
msgid "Invalid method parameter(s)."
msgstr "Virheelliset asetukset"
msgid "Missing property"
msgstr "Kenttä puuttuu"
msgid ""
"Warning: Zonemaster::LDNS not compiled with IDN support, cannot handle non-"
"ASCII names correctly."
msgstr ""
"Varoitus: Zonemaster::LDNS ei ole käännetty IDN tuella, joten se ei pysty "
"käsittelemään ei-ASCII-nimiä oikein."
#. BACKEND_TEST_AGENT:TEST_DIED
msgid "An error occured and Zonemaster could not start or finish the test."
msgstr ""
"Tapahtui virhe, eikä Zonemaster voinut aloittaa tai suorittaa testiä loppuun."
#. BACKEND_TEST_AGENT:UNABLE_TO_FINISH_TEST
#, perl-brace-format
msgid ""
"The test took too long to run (the current limit is {max_execution_time} "
"seconds). Maybe there are too many name servers or the name servers are "
"either unreachable or not responsive enough."
msgstr ""
"Testin suorittaminen kesti liian kauan (nykyinen raja on "
"{max_execution_time} sekuntia). Ehkä nimipalvelimia on liian monta, "
"nimipalvelimiin ei saada yhteyttä, tai ne eivät vastaa tarpeeksi nopeasti."
msgid "Invalid digest format"
msgstr "Virheellinen tiivisteen muoto"
msgid "Algorithm must be a positive integer"
msgstr "Algoritmin on oltava positiivinen kokonaisluku"
msgid "Digest type must be a positive integer"
msgstr "Tiivistetyypin (Digest type) on oltava positiivinen kokonaisluku"
msgid "Keytag must be a positive integer"
msgstr "Tunnisteen (Keytag) on oltava positiivinen kokonaisluku"
msgid "Domain name required"
msgstr "Vaaditaan verkkotunnus"
msgid "The domain name is IDNA invalid"
msgstr "Verkkotunnus on IDNA virheellinen"
msgid ""
"The domain name contains non-ascii characters and IDNA support is not "
"installed"
msgstr ""
"Verkkotunnuksen nimi sisältää muita kuin ascii-merkkejä, eikä IDNA tukea ole "
"asennettu"
msgid "The domain name character(s) are not supported"
msgstr "Verkkotunnuksen sisältämiä merkkejä ei tueta"
msgid "The domain name contains consecutive dots"
msgstr "Verkkotunnus sisältää peräkkäisiä pisteitä"
msgid "The domain name or label is too long"
msgstr "Verkkotunnus tai sen tunnisteet ovat liian pitkiä"
msgid "Invalid language tag format"
msgstr "Virheellinen kielitunnisteen muoto"
msgid "Unkown language string"
msgstr "Tuntematon kielitunniste"
msgid "Invalid IP address"
msgstr "Virheellinen IP-osoite"
msgid "Invalid profile format"
msgstr "Virheellinen profiilin muoto"
msgid "Unknown profile"
msgstr "Tuntematon profiili"

View File

@@ -0,0 +1,71 @@
msgid ""
msgstr ""
"Project-Id-Version: 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-02-18 11:20+0100\n"
"PO-Revision-Date: 2023-05-22 07:17+0200\n"
"Last-Translator: thomas.green@afnic.fr\n"
"Language-Team: Zonemaster project\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "Invalid method parameter(s)."
msgstr "Paramètre(s) incorrect(s)."
msgid "Missing property"
msgstr "Propriété manquante"
msgid ""
"Warning: Zonemaster::LDNS not compiled with IDN support, cannot handle non-"
"ASCII names correctly."
msgstr ""
"Attention : Zonemaster::LDNS n'est pas compilé avec le support IDN, "
"impossible de traiter correctement les noms non ASCII."
#. BACKEND_TEST_AGENT:TEST_DIED
msgid "An error occured and Zonemaster could not start or finish the test."
msgstr ""
"Une erreur est survenue et Zonemaster na pas pu commencer ou finir le test."
#. BACKEND_TEST_AGENT:UNABLE_TO_FINISH_TEST
#, perl-brace-format
msgid ""
"The test took too long to run (the current limit is {max_execution_time} "
"seconds). Maybe there are too many name servers or the name servers are "
"either unreachable or not responsive enough."
msgstr ""
"Le test a mis trop de temps à sexécuter (la limite actuelle est de "
"{max_execution_time} secondes). Il y a peut-être trop de serveurs de noms, "
"ou les serveurs de noms sont injoignables ou trop peu réactifs."
msgid "Invalid digest format"
msgstr "Format du condensat non valide"
msgid "Algorithm must be a positive integer"
msgstr "L'algorithme doit être un entier positif"
msgid "Digest type must be a positive integer"
msgstr "Le type d'empreinte doit être un entier positif"
msgid "Keytag must be a positive integer"
msgstr "L'identifiant de clef doit être un entier positif"
msgid "Domain name required"
msgstr "Nom de domaine requis"
msgid "Invalid language tag format"
msgstr "Format de l'étiquette d'identification de langue incorrect"
msgid "Unkown language string"
msgstr "Étiquette d'identification de langue inconnue"
msgid "Invalid IP address"
msgstr "Adresse IP non valide"
msgid "Invalid profile format"
msgstr "Format du profil non valide"
msgid "Unknown profile"
msgstr "Profil inconnu"

View File

@@ -0,0 +1,7 @@
# Range of free system UIDs that can be used for Zonemaster user
minuid = 736
maxuid = 769
# Range of free system GIDs that can be used for Zonemaster group
mingid = 736
maxgid = 769

View File

@@ -0,0 +1,71 @@
msgid ""
msgstr ""
"Project-Id-Version: 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-05-24 09:36+0000\n"
"PO-Revision-Date: 2025-04-25 08:30+0200\n"
"Last-Translator: richard.persson@norid.no\n"
"Language-Team: Zonemaster project\n"
"Language: nb\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "Invalid method parameter(s)."
msgstr "Ugyldig metodeparameter."
msgid "Missing property"
msgstr "Mangler verdi"
msgid ""
"Warning: Zonemaster::LDNS not compiled with IDN support, cannot handle non-"
"ASCII names correctly."
msgstr ""
"Advarsel: Zonemaster::LDNS er ikke kompilert med IDN-støtte. Kan bare "
"håndtere ASCII-navn."
#. BACKEND_TEST_AGENT:TEST_DIED
msgid "An error occured and Zonemaster could not start or finish the test."
msgstr ""
"Det oppstod en feil og Zonemaster kunne ikke starte eller fullføre testen."
#. BACKEND_TEST_AGENT:UNABLE_TO_FINISH_TEST
#, perl-brace-format
msgid ""
"The test took too long to run (the current limit is {max_execution_time} "
"seconds). Maybe there are too many name servers or the name servers are "
"either unreachable or not responsive enough."
msgstr ""
"Testen tok for lang tid å kjøre (gjeldende grense er {max_execution_time} "
"sekunder). Kanskje er det for mange navnetjenere eller navnetjenerne er "
"enten utilgjengelige eller ikke responsive nok."
msgid "Invalid digest format"
msgstr "Ugyldig format på digest"
msgid "Algorithm must be a positive integer"
msgstr "Algoritme må være et positivt tall"
msgid "Digest type must be a positive integer"
msgstr "Algoritme-type må være et positivt tall"
msgid "Keytag must be a positive integer"
msgstr "Keytag må være et positivt tall"
msgid "Domain name required"
msgstr "Domenenavn påkrevd"
msgid "Invalid language tag format"
msgstr "Ugyldig format på språk-tag"
msgid "Unkown language string"
msgstr "Ukjent språk-tag"
msgid "Invalid IP address"
msgstr "Ugylding IP-adresse"
msgid "Invalid profile format"
msgstr "Ugyldig format på profil"
msgid "Unknown profile"
msgstr "Ukjent profil"

View File

@@ -0,0 +1,2 @@
Find instructions on patching (upgrading) the Zonemaster database
on https://github.com/zonemaster/zonemaster/blob/master/docs/public/upgrading/backend.md

View File

@@ -0,0 +1,330 @@
use strict;
use warnings;
use List::MoreUtils qw(zip_unflatten);
use JSON::PP;
use Try::Tiny;
use File::Temp qw(tempfile);
use Encode qw(find_encoding);
use Zonemaster::Backend::Config;
use Zonemaster::Engine;
my $config = Zonemaster::Backend::Config->load_config();
my %module_mapping;
for my $module ( Zonemaster::Engine->modules ) {
$module_mapping{lc $module} = $module;
}
my %patch = (
mysql => \&patch_db_mysql,
postgresql => \&patch_db_postgresql,
sqlite => \&patch_db_sqlite,
);
my $db_engine = $config->DB_engine;
print "Configured database engine: $db_engine\n";
if ( $db_engine =~ /^(MySQL|PostgreSQL|SQLite)$/ ) {
print( "Starting database migration\n" );
$patch{ lc $db_engine }();
print( "\nMigration done\n" );
}
else {
die "Unknown database engine configured: $db_engine\n";
}
# depending on the resources available to select all data in database
# update $row_count to your needs
sub _update_data_result_entries {
my ( $dbh, $row_count ) = @_;
my $json = JSON::PP->new->allow_blessed->convert_blessed->canonical;
# update only jobs with results
my ( $row_total ) = $dbh->selectrow_array( 'SELECT count(*) FROM test_results WHERE results IS NOT NULL' );
print "Will update $row_total rows\n";
my %levels = Zonemaster::Engine::Logger::Entry->levels();
my $row_done = 0;
while ( $row_done < $row_total ) {
print "Progress update: $row_done / $row_total\n";
my $row_updated = 0;
my $sth1 = $dbh->prepare( 'SELECT hash_id, results FROM test_results WHERE results IS NOT NULL ORDER BY id ASC LIMIT ? OFFSET ?' );
$sth1->execute( $row_count, $row_done );
while ( my $row = $sth1->fetchrow_arrayref ) {
my ( $hash_id, $results ) = @$row;
next unless $results;
my @records;
my $entries = $json->decode( $results );
foreach my $m ( @$entries ) {
my $module = $module_mapping{ lc $m->{module} } // ucfirst lc $m->{module};
my $testcase =
( !defined $m->{testcase} or $m->{testcase} eq 'UNSPECIFIED' )
? 'Unspecified'
: $m->{testcase} =~ s/[a-z_]*/$module/ir;
if ($testcase eq 'Delegation01' and $m->{tag} =~ /^(NOT_)?ENOUGH_IPV[46]_NS_(CHILD|DEL)$/) {
my @ips = split( /;/, delete $m->{args}{ns_ip_list} );
my @names = split( /;/, delete $m->{args}{nsname_list} );
my @ns_list = map { join( '/', @$_ ) } zip_unflatten(@names, @ips);
$m->{args}{ns_list} = join( ';', @ns_list );
}
my $r = [
$hash_id,
$levels{ $m->{level} },
$module,
$testcase,
$m->{tag},
$m->{timestamp},
$json->encode( $m->{args} // {} ),
];
push @records, $r;
}
my $query_values = join ", ", ("(?, ?, ?, ?, ?, ?, ?)") x @records;
my $query = "INSERT INTO result_entries (hash_id, level, module, testcase, tag, timestamp, args) VALUES $query_values";
my $sth = $dbh->prepare( $query );
$sth = $sth->execute( map { @$_ } @records );
$row_updated += $dbh->do( "UPDATE test_results SET results = NULL WHERE hash_id = ?", undef, $hash_id );
}
# increase by min(row_updated, row_count)
$row_done += ( $row_updated < $row_count ) ? $row_updated : $row_count;
}
print "Progress update: $row_done / $row_total\n";
}
sub _update_data_normalize_domains {
my ( $db ) = @_;
my ( $row_total ) = $db->dbh->selectrow_array( 'SELECT count(*) FROM test_results' );
print "Will update $row_total rows\n";
my $sth1 = $db->dbh->prepare( 'SELECT hash_id, params FROM test_results' );
$sth1->execute;
my $row_done = 0;
my $progress = 0;
while ( my $row = $sth1->fetchrow_hashref ) {
my $hash_id = $row->{hash_id};
eval {
my $raw_params = decode_json($row->{params});
my $domain = $raw_params->{domain};
# This has never been cleaned
delete $raw_params->{user_ip};
my $params = $db->encode_params( $raw_params );
my $fingerprint = $db->generate_fingerprint( $raw_params );
$domain = Zonemaster::Backend::DB::_normalize_domain( $domain );
$db->dbh->do('UPDATE test_results SET domain = ?, params = ?, fingerprint = ? where hash_id = ?', undef, $domain, $params, $fingerprint, $hash_id);
};
if ($@) {
warn "Caught error while updating record with hash id $hash_id, ignoring: $@\n";
}
$row_done += 1;
my $new_progress = int(($row_done / $row_total) * 100);
if ( $new_progress != $progress ) {
$progress = $new_progress;
print("$progress%\n");
}
}
}
sub patch_db_mysql {
use Zonemaster::Backend::DB::MySQL;
my $db = Zonemaster::Backend::DB::MySQL->from_config( $config );
my $dbh = $db->dbh;
$dbh->{AutoCommit} = 0;
try {
$db->create_schema();
print( "\n-> (1/2) Populating new result_entries table\n" );
_update_data_result_entries( $dbh, 50000 );
print( "\n-> (2/2) Normalizing domain names\n" );
_update_data_normalize_domains( $db );
$dbh->commit();
} catch {
print( "\nCould not upgrade database: " . $_ );
$dbh->rollback();
};
}
sub _patch_db_postgresql_step1 {
my ($dbh, $chunk_size) = @_;
$chunk_size //= 100_000;
# This is used later for backslash-escaping data supplied to COPY … FROM
# STDIN commands.
my %conv = ( 8 => '\b', 9 => '\t', 10 => '\n', 11 => '\v', 12 => '\f', 13 => '\r', 92 => '\\\\' );
my $utf8 = find_encoding('utf8');
# Why a cursor instead of a plain SELECT statement? Because DBD::Pg does
# not use server-side cursors itself when reading the result of a SELECT
# query.
#
# And why is that a problem? Thats because the DBMS will try to compute
# the entire result set before handing it to the client. With large
# Zonemaster setups with years of history and millions of tests, this
# SELECT statement will generate hundreds of millions of rows. So without
# the appropriate precautions, a plain SELECT query like this one will
# definitely take out the machine it is running on!
print("Starting up\n");
$dbh->do(q[
DECLARE curs NO SCROLL CURSOR FOR
SELECT
test_results.hash_id,
log_level.value AS level,
CASE res.module
WHEN 'DNSSEC' THEN res.module
ELSE initcap(res.module)
END AS module,
CASE
WHEN res.testcase IS NULL THEN ''
WHEN res.testcase LIKE 'DNSSEC%' THEN res.testcase
ELSE initcap(res.testcase)
END AS testcase,
res.tag AS tag,
res.timestamp AS timestamp,
COALESCE(migrated_args.args, '{}') AS args
FROM test_results,
json_to_recordset(results)
AS res(module TEXT, testcase TEXT, tag TEXT, level TEXT, timestamp REAL, args JSONB)
LEFT JOIN log_level ON (res.level = log_level.level)
LEFT JOIN LATERAL (
SELECT CASE WHEN res.testcase = 'DELEGATION01'
AND res.tag ~ '^(NOT_)?ENOUGH_IPV[46]_NS_(CHILD|DEL)$'
AND (NOT res.args ? 'ns_list')
THEN (
SELECT res.args
- ARRAY['ns_ip_list', 'nsname_list']
|| jsonb_build_object('ns_list', string_agg(name || '/' || ip, ';'))
FROM unnest(
string_to_array(res.args->>'ns_ip_list', ';'),
string_to_array(res.args->>'nsname_list', ';'))
AS unnest(ip, name))
ELSE res.args
END) AS migrated_args(args) ON TRUE]);
# Ive tried to avoid hardcoding numbers but FETCH statements somehow
# dont like being parameterized with placeholders. This will have to do.
my $read_sth = $dbh->prepare(sprintf(q[FETCH FORWARD %d FROM curs], $chunk_size));
my $row_inserted = 0;
while ($read_sth->execute(), (my $row_count = $read_sth->rows()) > 0) {
my @copydata = ();
print("Progress update: ${row_inserted} rows inserted\n");
$row_inserted += $row_count;
$dbh->do(q[COPY result_entries FROM STDIN]);
while (my $row = $read_sth->fetchrow_arrayref) {
my @columns = map {
if (defined $_) {
# Replaces invalid UTF-8 sequences with U+FFFD and escapes
# characters as required by PostgreSQLs text COPY data
# format.
$utf8->encode($utf8->decode($_) =~ s/[\x08-\x0D\\]/$conv{ord $&}/aegr);
} else {
'\N';
}
} @$row;
my $line = join("\t", @columns) . "\n";
push @copydata, $line;
$dbh->pg_putcopydata( $line );
}
try {
$dbh->pg_putcopyend();
}
catch {
print("An error occurred while trying to copy some data.\n");
my ($fh, $filename) = tempfile();
print $fh @copydata;
close $fh;
print("The data supplied to COPY causing the failure has been ",
"stored in $filename for inspection\n");
die $_;
}
}
$dbh->do(q[CLOSE curs]);
print("Done inserting ${row_inserted} rows\n");
}
sub patch_db_postgresql {
use Zonemaster::Backend::DB::PostgreSQL;
my $db = Zonemaster::Backend::DB::PostgreSQL->from_config( $config );
my $dbh = $db->dbh;
$dbh->{AutoCommit} = 0;
try {
$db->create_schema();
# Make sure the planner knows that log_level is a small table
# so it can optimize step 1 appropriately
$dbh->do(q[ANALYZE log_level]);
print( "\n-> (1/2) Populating new result_entries table\n" );
_patch_db_postgresql_step1( $dbh );
$dbh->do(
'UPDATE test_results SET results = NULL WHERE results IS NOT NULL'
);
print( "\n-> (2/2) Normalizing domain names\n" );
_update_data_normalize_domains( $db );
$dbh->commit();
} catch {
print( "\nCould not upgrade database: " . $_ );
$dbh->rollback();
};
}
sub patch_db_sqlite {
use Zonemaster::Backend::DB::SQLite;
my $db = Zonemaster::Backend::DB::SQLite->from_config( $config );
my $dbh = $db->dbh;
$dbh->{AutoCommit} = 0;
try {
$db->create_schema();
print( "\n-> (1/2) Populating new result_entries table\n" );
_update_data_result_entries( $dbh, 142 );
print( "\n-> (2/2) Normalizing domain names\n" );
_update_data_normalize_domains( $db );
$dbh->commit();
} catch {
print( "\nError while upgrading database: " . $_ );
$dbh->rollback();
};
}

View File

@@ -0,0 +1,32 @@
use strict;
use warnings;
use Zonemaster::Backend::Config;
use Zonemaster::Engine;
my $config = Zonemaster::Backend::Config->load_config();
my $db_engine = $config->DB_engine;
print "Configured database engine: $db_engine\n";
if ( $db_engine =~ /^(MySQL|PostgreSQL|SQLite)$/ ) {
print( "Starting database migration\n" );
_update_result_entries( $config->new_DB()->dbh() );
print( "\nMigration done\n" );
}
else {
die "Unknown database engine configured: $db_engine\n";
}
sub _update_result_entries {
my ( $dbh ) = @_;
$dbh->do(<<SQL) or die 'Migration failed';
UPDATE result_entries
SET module = 'Backend'
WHERE upper(module) = 'BACKEND_TEST_AGENT';
SQL
}

View File

@@ -0,0 +1,253 @@
use strict;
use warnings;
use Try::Tiny;
use Zonemaster::Backend::Config;
my $config = Zonemaster::Backend::Config->load_config();
my %patch = (
mysql => \&patch_db_mysql,
postgresql => \&patch_db_postgresql,
sqlite => \&patch_db_sqlite,
);
my $db_engine = $config->DB_engine;
if ( $db_engine =~ /^(MySQL|PostgreSQL|SQLite)$/ ) {
$patch{ lc $db_engine }();
}
else {
die "Unknown database engine configured: $db_engine\n";
}
sub patch_db_mysql {
use Zonemaster::Backend::DB::MySQL;
my $db = Zonemaster::Backend::DB::MySQL->from_config( $config );
my $dbh = $db->dbh;
# add table constraints
$dbh->do( 'ALTER TABLE users ADD CONSTRAINT UNIQUE (username)' );
$dbh->do( 'ALTER TABLE test_results ADD CONSTRAINT UNIQUE (hash_id)' );
# update columns names, data type and default value
$dbh->do( 'ALTER TABLE test_results MODIFY COLUMN id BIGINT AUTO_INCREMENT' );
$dbh->do( 'ALTER TABLE test_results CHANGE COLUMN creation_time created_at DATETIME NOT NULL' );
$dbh->do( 'ALTER TABLE test_results CHANGE COLUMN test_start_time started_at DATETIME DEFAULT NULL' );
$dbh->do( 'ALTER TABLE test_results CHANGE COLUMN test_end_time ended_at DATETIME DEFAULT NULL' );
$dbh->do( 'ALTER TABLE batch_jobs CHANGE COLUMN creation_time created_at DATETIME NOT NULL' );
$dbh->{AutoCommit} = 0;
try {
# normalize "domain" column
$dbh->do(
q[
UPDATE test_results
SET domain = LOWER(domain)
WHERE CAST(domain AS BINARY) RLIKE '[A-Z]'
]
);
$dbh->do(
q[
UPDATE test_results
SET domain = '.'
WHERE domain = '..' OR domain = '...' OR domain = '....'
]
);
$dbh->do(
q[
UPDATE test_results
SET domain = TRIM( TRAILING '.' FROM domain )
WHERE domain != '.' AND domain LIKE '%.'
]
);
$dbh->commit();
} catch {
print( "Could not upgrade database: " . $_ );
eval { $dbh->rollback() };
};
}
sub patch_db_postgresql {
use Zonemaster::Backend::DB::PostgreSQL;
my $db = Zonemaster::Backend::DB::PostgreSQL->from_config( $config );
my $dbh = $db->dbh;
$dbh->{AutoCommit} = 0;
try {
# update sequence data type to BIGINT
$dbh->do( 'ALTER SEQUENCE test_results_id_seq AS BIGINT' );
$dbh->do( 'ALTER TABLE test_results ALTER COLUMN id SET DATA TYPE BIGINT' );
# remove default value for "creation_time"
$dbh->do( 'ALTER TABLE test_results ALTER COLUMN creation_time DROP DEFAULT' );
$dbh->do( 'ALTER TABLE batch_jobs ALTER COLUMN creation_time DROP DEFAULT' );
# rename columns
$dbh->do( 'ALTER TABLE test_results RENAME COLUMN creation_time TO created_at' );
$dbh->do( 'ALTER TABLE test_results RENAME COLUMN test_start_time TO started_at' );
$dbh->do( 'ALTER TABLE test_results RENAME COLUMN test_end_time TO ended_at' );
$dbh->do( 'ALTER TABLE batch_jobs RENAME COLUMN creation_time TO created_at' );
# add table constraints
$dbh->do( 'ALTER TABLE test_results ADD UNIQUE (hash_id)' );
$dbh->do( 'ALTER TABLE users ADD UNIQUE (username)' );
# normalize "domain" column
$dbh->do(
q[
UPDATE test_results
SET domain = LOWER(domain)
WHERE domain != LOWER(domain)
]
);
$dbh->do(
q[
UPDATE test_results
SET domain = '.'
WHERE domain = '..' OR domain = '...' OR domain = '....'
]
);
$dbh->do(
q[
UPDATE test_results
SET domain = RTRIM(domain, '.')
WHERE domain != '.' AND domain LIKE '%.'
]
);
$dbh->commit();
} catch {
print( "Could not upgrade database: " . $_ );
eval { $dbh->rollback() };
};
}
sub patch_db_sqlite {
use Zonemaster::Backend::DB::SQLite;
my $db = Zonemaster::Backend::DB::SQLite->from_config( $config );
my $dbh = $db->dbh;
$dbh->{AutoCommit} = 0;
# since we change the default value for a column, the whole table needs to
# be recreated
# 1. rename the table to "<table>_old"
# 2. recreate a clean table schema
# 3. populate it with the values from "<table>_old"
# 4. remove "<table>_old" and indexes
# 5. recreate the indexes
try {
$dbh->do('ALTER TABLE test_results RENAME TO test_results_old');
$dbh->do('ALTER TABLE batch_jobs RENAME TO batch_jobs_old');
$dbh->do('ALTER TABLE users RENAME TO users_old');
# create the tables
$db->create_schema();
# populate the tables
$dbh->do(
q[
INSERT INTO test_results
(
id,
hash_id,
domain,
batch_id,
created_at,
started_at,
ended_at,
priority,
queue,
progress,
fingerprint,
params,
results,
undelegated
)
SELECT
id,
hash_id,
lower(domain),
batch_id,
creation_time,
test_start_time,
test_end_time,
priority,
queue,
progress,
fingerprint,
params,
results,
undelegated
FROM test_results_old
]
);
$dbh->do(
q[
UPDATE test_results
SET domain = '.'
WHERE domain = '..' OR domain = '...' OR domain = '....'
]
);
$dbh->do(
q[
UPDATE test_results
SET domain = RTRIM(domain, '.')
WHERE domain != '.' AND domain LIKE '%.'
]
);
$dbh->do('
INSERT INTO batch_jobs
(
id,
username,
created_at
)
SELECT
id,
username,
creation_time
FROM batch_jobs_old
');
$dbh->do('
INSERT INTO users
(
id,
username,
api_key
)
SELECT
id,
username,
api_key
FROM users_old
');
# delete old tables
$dbh->do('DROP TABLE test_results_old');
$dbh->do('DROP TABLE batch_jobs_old');
$dbh->do('DROP TABLE users_old');
# recreate indexes
$db->create_schema();
$dbh->commit();
} catch {
print( "Error while upgrading database: " . $_ );
eval { $dbh->rollback() };
};
}

View File

@@ -0,0 +1,40 @@
use strict;
use warnings;
use DBI qw(:utils);
use Zonemaster::Backend::Config;
use Zonemaster::Backend::DB::MySQL;
my $config = Zonemaster::Backend::Config->load_config();
if ( $config->DB_engine ne 'MySQL' ) {
die "The configuration file does not contain the MySQL backend";
}
my $dbh = Zonemaster::Backend::DB::MySQL->from_config( $config )->dbh;
sub patch_db {
####################################################################
# TEST RESULTS
####################################################################
$dbh->do( 'ALTER TABLE test_results ADD COLUMN hash_id VARCHAR(16) NULL' );
$dbh->do( 'UPDATE test_results SET hash_id = (SELECT SUBSTRING(MD5(CONCAT(RAND(), UUID())) from 1 for 16))' );
$dbh->do( 'ALTER TABLE test_results MODIFY hash_id VARCHAR(16) DEFAULT NULL NOT NULL' );
$dbh->do(
'CREATE TRIGGER before_insert_test_results
BEFORE INSERT ON test_results
FOR EACH ROW
BEGIN
IF new.hash_id IS NULL OR new.hash_id=\'\'
THEN
SET new.hash_id = SUBSTRING(MD5(CONCAT(RAND(), UUID())) from 1 for 16);
END IF;
END;
'
);
}
patch_db();

View File

@@ -0,0 +1,22 @@
use strict;
use warnings;
use DBI qw(:utils);
use Zonemaster::Backend::Config;
use Zonemaster::Backend::DB::MySQL;
my $config = Zonemaster::Backend::Config->load_config();
if ( $config->DB_engine ne 'MySQL' ) {
die "The configuration file does not contain the MySQL backend";
}
my $dbh = Zonemaster::Backend::DB::MySQL->from_config( $config )->dbh;
sub patch_db {
####################################################################
# TEST RESULTS
####################################################################
$dbh->do( 'ALTER TABLE test_results ADD COLUMN nb_retries INTEGER NOT NULL DEFAULT 0' );
}
patch_db();

View File

@@ -0,0 +1,22 @@
use strict;
use warnings;
use DBI qw(:utils);
use Zonemaster::Backend::Config;
use Zonemaster::Backend::DB::MySQL;
my $config = Zonemaster::Backend::Config->load_config();
if ( $config->DB_engine ne 'MySQL' ) {
die "The configuration file does not contain the MySQL backend";
}
my $dbh = Zonemaster::Backend::DB::MySQL->from_config( $config )->dbh;
sub patch_db {
############################################################################
# Convert column "results" to MEDIUMBLOB so that it can hold larger results
############################################################################
$dbh->do( 'ALTER TABLE test_results MODIFY results mediumblob' );
}
patch_db();

View File

@@ -0,0 +1,76 @@
use strict;
use warnings;
use JSON::PP;
use DBI qw(:utils);
use Zonemaster::Backend::Config;
use Zonemaster::Backend::DB::MySQL;
my $config = Zonemaster::Backend::Config->load_config();
if ( $config->DB_engine ne 'MySQL' ) {
die "The configuration file does not contain the MySQL backend";
}
my $db = Zonemaster::Backend::DB::MySQL->from_config( $config );
my $dbh = $db->dbh;
sub patch_db {
# Remove the trigger
$dbh->do( 'DROP TRIGGER IF EXISTS before_insert_test_results' );
# Set the "hash_id" field to NOT NULL
eval {
$dbh->do( 'ALTER TABLE test_results MODIFY COLUMN hash_id VARCHAR(16) NOT NULL' );
};
print( "Error while changing DB schema: " . $@ ) if ($@);
# Rename column "params_deterministic_hash" into "fingerprint"
# Since MariaDB 10.5.2 (2020-03-26) <https://mariadb.com/kb/en/mariadb-1052-release-notes/>
# ALTER TABLE t1 RENAME COLUMN old_col TO new_col;
# Before that we need to use CHANGE COLUMN <https://mariadb.com/kb/en/alter-table/#change-column>
eval {
$dbh->do('ALTER TABLE test_results CHANGE COLUMN params_deterministic_hash fingerprint CHARACTER VARYING(32)');
};
print( "Error while changing DB schema: " . $@ ) if ($@);
# Update index
eval {
# retrieve all indexes by key name
my $indexes = $dbh->selectall_hashref( 'SHOW INDEXES FROM test_results', 'Key_name' );
if ( exists($indexes->{test_results__params_deterministic_hash}) ) {
$dbh->do( "DROP INDEX test_results__params_deterministic_hash ON test_results" );
}
$dbh->do( "CREATE INDEX test_results__fingerprint ON test_results (fingerprint)" );
};
print( "Error while updating the index: " . $@ ) if ($@);
# Update the "undelegated" column
my $sth1 = $dbh->prepare('SELECT id, params from test_results', undef);
$sth1->execute;
while ( my $row = $sth1->fetchrow_hashref ) {
my $id = $row->{id};
my $raw_params = decode_json($row->{params});
my $ds_info_values = scalar grep !/^$/, map { values %$_ } @{$raw_params->{ds_info}};
my $nameservers_values = scalar grep !/^$/, map { values %$_ } @{$raw_params->{nameservers}};
my $undelegated = $ds_info_values > 0 || $nameservers_values > 0 || 0;
$dbh->do('UPDATE test_results SET undelegated = ? where id = ?', undef, $undelegated, $id);
}
# remove the "user_info" column from the "users" table
# the IF EXISTS clause is available with MariaDB but not MySQL
eval {
$dbh->do( "ALTER TABLE users DROP COLUMN user_info" );
};
print( "Error while dropping the column: " . $@ ) if ($@);
# remove the "nb_retries" column from the "test_results" table
eval {
$dbh->do( "ALTER TABLE test_results DROP COLUMN nb_retries" );
};
print( "Error while dropping the column: " . $@ ) if ($@);
}
patch_db();

View File

@@ -0,0 +1,23 @@
use strict;
use warnings;
use DBI qw(:utils);
use Zonemaster::Backend::Config;
use Zonemaster::Backend::DB::MySQL;
my $config = Zonemaster::Backend::Config->load_config();
if ( $config->DB_engine ne 'MySQL' ) {
die "The configuration file does not contain the MySQL backend";
}
my $dbh = Zonemaster::Backend::DB::MySQL->from_config( $config )->dbh;
sub patch_db {
####################################################################
# TEST RESULTS
####################################################################
$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' );
}
patch_db();

View File

@@ -0,0 +1,23 @@
use strict;
use warnings;
use DBI qw(:utils);
use Zonemaster::Backend::Config;
use Zonemaster::Backend::DB::PostgreSQL;
my $config = Zonemaster::Backend::Config->load_config();
if ( $config->DB_engine ne 'PostgreSQL' ) {
die "The configuration file does not contain the PostgreSQL backend";
}
my $dbh = Zonemaster::Backend::DB::PostgreSQL->from_config( $config )->dbh;
sub patch_db {
####################################################################
# TEST RESULTS
####################################################################
$dbh->do( 'ALTER TABLE test_results ADD COLUMN nb_retries INTEGER NOT NULL DEFAULT 0' );
}
patch_db();

View File

@@ -0,0 +1,109 @@
use strict;
use warnings;
use JSON::PP;
use Encode;
use DBI qw(:utils);
use Zonemaster::Backend::Config;
use Zonemaster::Backend::DB::PostgreSQL;
my $config = Zonemaster::Backend::Config->load_config();
if ( $config->DB_engine ne 'PostgreSQL' ) {
die "The configuration file does not contain the PostgreSQL backend";
}
my $db = Zonemaster::Backend::DB::PostgreSQL->from_config( $config );
my $dbh = $db->dbh;
sub patch_db {
# Drop default value for the "hash_id" field
$dbh->do( 'ALTER TABLE test_results ALTER COLUMN hash_id DROP DEFAULT' );
# Rename column "params_deterministic_hash" into "fingerprint"
eval {
$dbh->do( 'ALTER TABLE test_results RENAME COLUMN params_deterministic_hash TO fingerprint' );
};
print( "Error while changing DB schema: " . $@ ) if ($@);
# Update index
eval {
$dbh->do( "DROP INDEX IF EXISTS test_results__params_deterministic_hash" );
$dbh->do( "CREATE INDEX test_results__fingerprint ON test_results (fingerprint)" );
};
print( "Error while updating the index: " . $@ ) if ($@);
# test_start_time and test_end_time default to NULL
eval {
$dbh->do('ALTER TABLE test_results ALTER COLUMN test_start_time SET DEFAULT NULL');
$dbh->do('ALTER TABLE test_results ALTER COLUMN test_end_time SET DEFAULT NULL');
};
print( "Error while changing DB schema: " . $@ ) if ($@);
# Add missing "domain" and "undelegated" columns
eval {
$dbh->do( "ALTER TABLE test_results ADD COLUMN domain VARCHAR(255) NOT NULL DEFAULT ''" );
$dbh->do( 'ALTER TABLE test_results ADD COLUMN undelegated integer NOT NULL DEFAULT 0' );
};
print( "Error while changing DB schema: " . $@ ) if ($@);
# Update index
eval {
$dbh->do( "DROP INDEX IF EXISTS test_results__domain_undelegated" );
$dbh->do( "CREATE INDEX test_results__domain_undelegated ON test_results (domain, undelegated)" );
};
print( "Error while updating the index: " . $@ ) if ($@);
# New index
eval {
$dbh->do( 'CREATE INDEX IF NOT EXISTS test_results__progress_priority_id ON test_results (progress, priority DESC, id) WHERE (progress = 0)' );
};
print( "Error while creating the index: " . $@ ) if ($@);
# Update the "domain" column
$dbh->do( "UPDATE test_results SET domain = (params->>'domain')" );
# remove default value to "domain" column
$dbh->do( "ALTER TABLE test_results ALTER COLUMN domain DROP DEFAULT" );
# Update the "undelegated" column
my $sth1 = $dbh->prepare('SELECT id, params from test_results', undef);
$sth1->execute;
while ( my $row = $sth1->fetchrow_hashref ) {
my $id = $row->{id};
my $raw_params;
if (utf8::is_utf8($row->{params}) ) {
$raw_params = decode_json( encode_utf8 ( $row->{params} ) );
} else {
$raw_params = decode_json( $row->{params} );
}
my $ds_info_values = scalar grep !/^$/, map { values %$_ } @{$raw_params->{ds_info}};
my $nameservers_values = scalar grep !/^$/, map { values %$_ } @{$raw_params->{nameservers}};
my $undelegated = $ds_info_values > 0 || $nameservers_values > 0 || 0;
$dbh->do('UPDATE test_results SET undelegated = ? where id = ?', undef, $undelegated, $id);
}
# add "username" and "api_key" columns to the "users" table
eval {
$dbh->do( 'ALTER TABLE users ADD COLUMN username VARCHAR(128)' );
$dbh->do( 'ALTER TABLE users ADD COLUMN api_key VARCHAR(512)' );
};
print( "Error while changing DB schema: " . $@ ) if ($@);
# update the columns
eval {
$dbh->do( "UPDATE users SET username = (user_info->>'username'), api_key = (user_info->>'api_key')" );
};
print( "Error while updating the users table: " . $@ ) if ($@);
# remove the "user_info" column from the "users" table
$dbh->do( "ALTER TABLE users DROP COLUMN IF EXISTS user_info" );
# remove the "nb_retries" column from the "test_results" table
$dbh->do( "ALTER TABLE test_results DROP COLUMN IF EXISTS nb_retries" );
}
patch_db();

View File

@@ -0,0 +1,95 @@
use strict;
use warnings;
use JSON::PP;
use DBI qw(:utils);
use Zonemaster::Backend::Config;
use Zonemaster::Backend::DB::SQLite;
my $config = Zonemaster::Backend::Config->load_config();
if ( $config->DB_engine ne 'SQLite' ) {
die "The configuration file does not contain the SQLite backend";
}
my $db = Zonemaster::Backend::DB::SQLite->from_config( $config );
my $dbh = $db->dbh;
sub patch_db {
# since we change the default value for a column, the whole table needs to
# be recreated
# 1. rename the "test_results" table to "test_results_old"
# 2. create the new "test_results" table
# 3. populate it with the values from "test_results_old"
# 4. remove old table and indexes
# 5. recreate the indexes
eval {
$dbh->do('ALTER TABLE test_results RENAME TO test_results_old');
# create the table
$db->create_schema();
# populate it
# - nb_retries is omitted as we remove this column
# - params_deterministic_hash is renamed to fingerprint
$dbh->do('
INSERT INTO test_results
SELECT id,
hash_id,
domain,
batch_id,
creation_time,
test_start_time,
test_end_time,
priority,
queue,
progress,
params_deterministic_hash,
params,
results,
undelegated
FROM test_results_old
');
$dbh->do('DROP TABLE test_results_old');
# recreate indexes
$db->create_schema();
};
print( "Error while updating the 'test_results' table schema: " . $@ ) if ($@);
# Update the "undelegated" column
my $sth1 = $dbh->prepare('SELECT id, params from test_results', undef);
$sth1->execute;
while ( my $row = $sth1->fetchrow_hashref ) {
my $id = $row->{id};
my $raw_params = decode_json($row->{params});
my $ds_info_values = scalar grep !/^$/, map { values %$_ } @{$raw_params->{ds_info}};
my $nameservers_values = scalar grep !/^$/, map { values %$_ } @{$raw_params->{nameservers}};
my $undelegated = $ds_info_values > 0 || $nameservers_values > 0 || 0;
$dbh->do('UPDATE test_results SET undelegated = ? where id = ?', undef, $undelegated, $id);
}
# in order to properly drop a column, the whole table needs to be recreated
# 1. rename the "users" table to "users_old"
# 2. create the new "users" table
# 3. populate it with the values from "users_old"
# 4. remove old table
eval {
$dbh->do('ALTER TABLE users RENAME TO users_old');
# create the table
$db->create_schema();
# populate it
$dbh->do('INSERT INTO users SELECT id, username, api_key FROM users_old');
$dbh->do('DROP TABLE users_old');
};
print( "Error while updating the 'users' table schema: " . $@ ) if ($@);
}
patch_db();

View File

@@ -0,0 +1,85 @@
msgid ""
msgstr ""
"Project-Id-Version: 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-09-18 08:43+0200\n"
"PO-Revision-Date: 2024-09-18 10:05+0200\n"
"Last-Translator: milijan@arnes.si\n"
"Language-Team: Zonemaster project\n"
"Language: sl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.5\n"
#: ../lib/Zonemaster/Backend/RPCAPI.pm:858
msgid "Invalid method parameter(s)."
msgstr "Nepravilni parametri."
#: ../lib/Zonemaster/Backend/RPCAPI.pm:888
msgid "Missing property"
msgstr "Manjkajoče polje"
#: ../lib/Zonemaster/Backend/TestAgent.pm:213
msgid ""
"Warning: Zonemaster::LDNS not compiled with IDN support, cannot handle non-"
"ASCII names correctly."
msgstr ""
"Opozorilo: Zonemaster::LDNS ne podpira IDN, ni mogoče obdelati ne-ASCII "
"imena."
#. BACKEND_TEST_AGENT:TEST_DIED
#: ../lib/Zonemaster/Backend/Translator.pm:23
msgid "An error occured and Zonemaster could not start or finish the test."
msgstr "Zgodila se je napaka, Zonemaster ne more začeti ali končati testa."
#. BACKEND_TEST_AGENT:UNABLE_TO_FINISH_TEST
#: ../lib/Zonemaster/Backend/Translator.pm:27
#, perl-brace-format
msgid ""
"The test took too long to run (the current limit is {max_execution_time} "
"seconds). Maybe there are too many name servers or the name servers are "
"either unreachable or not responsive enough."
msgstr ""
"Test traja predolgo (trenutna meja je {max_execution_time} sekund). Mogoče "
"je preveč strežnikov za preveriti, ali so neodzivni ali pa počasni."
#: ../lib/Zonemaster/Backend/Validator.pm:162
msgid "Invalid digest format"
msgstr "Nepravilen digest format"
#: ../lib/Zonemaster/Backend/Validator.pm:167
msgid "Algorithm must be a positive integer"
msgstr "Algoritem mora biti pozitivno število"
#: ../lib/Zonemaster/Backend/Validator.pm:172
msgid "Digest type must be a positive integer"
msgstr "Tip izvlečka za DS mora biti pozitivno število"
#: ../lib/Zonemaster/Backend/Validator.pm:177
msgid "Keytag must be a positive integer"
msgstr "Oznaka za ključ mora biti pozitivno število"
#: ../lib/Zonemaster/Backend/Validator.pm:282
msgid "Domain name required"
msgstr "Domena je obvezna"
#: ../lib/Zonemaster/Backend/Validator.pm:314
msgid "Invalid language tag format"
msgstr "Nepravilen format zastavice za jezik"
#: ../lib/Zonemaster/Backend/Validator.pm:317
msgid "Unkown language string"
msgstr "Neznan jezik"
#: ../lib/Zonemaster/Backend/Validator.pm:332
msgid "Invalid IP address"
msgstr "Neveljaven IP naslov"
#: ../lib/Zonemaster/Backend/Validator.pm:356
msgid "Invalid profile format"
msgstr "Neveljaven format profila"
#: ../lib/Zonemaster/Backend/Validator.pm:360
msgid "Unknown profile"
msgstr "Neznan profil"

View File

@@ -0,0 +1,92 @@
msgid ""
msgstr ""
"Project-Id-Version: 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-05-21 21:29+0000\n"
"PO-Revision-Date: 2023-05-21 21:29+0000\n"
"Last-Translator: mats.dufberg@iis.se\n"
"Language-Team: Zonemaster project\n"
"Language: sv\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "Invalid method parameter(s)."
msgstr "Ogiltig metodparameter."
msgid "Missing property"
msgstr "Attribut saknas"
msgid ""
"Warning: Zonemaster::LDNS not compiled with IDN support, cannot handle non-"
"ASCII names correctly."
msgstr ""
"Varning: Zonemaster::LDNS är inte kompilerad med IDNA-stöd, så enbart ASCII-"
"namn kan hanteras."
#. BACKEND_TEST_AGENT:TEST_DIED
msgid "An error occured and Zonemaster could not start or finish the test."
msgstr ""
"Ett fel har inträffat så att Zonemaster inte kunde starta eller slutföra "
"testet."
#. BACKEND_TEST_AGENT:UNABLE_TO_FINISH_TEST
#, perl-brace-format
msgid ""
"The test took too long to run (the current limit is {max_execution_time} "
"seconds). Maybe there are too many name servers or the name servers are "
"either unreachable or not responsive enough."
msgstr ""
"Det tog för lång tid att köra testet (övre tidsgränsen är f.n. "
"{max_execution_time} sekunder). Kanske har domänen för många namnservrar "
"eller så är namnservrarna oåtkomliga eller så tar namnservrarna för lång på "
"att svara."
msgid "Invalid digest format"
msgstr "Ogiltigt format på digest-data"
msgid "Algorithm must be a positive integer"
msgstr "Algoritm måste vara ett positivt heltal"
msgid "Digest type must be a positive integer"
msgstr "Digest-typ måste vara ett positivt heltal"
msgid "Keytag must be a positive integer"
msgstr "Keytag måste vara ett positivt heltal"
msgid "Domain name required"
msgstr "Domännamn är obligatoriskt"
msgid "The domain name is IDNA invalid"
msgstr "Domännamnet är ogiltigt enligt IDN-standarden"
msgid ""
"The domain name contains non-ascii characters and IDNA support is not "
"installed"
msgstr ""
"Domännamnet innehåller icke-ASCII-tecken, men stöd för IDN är inte "
"installerat"
msgid "The domain name character(s) are not supported"
msgstr "Domännamnstecken stöds inte"
msgid "The domain name contains consecutive dots"
msgstr "Domännamnet innehåller flera punkter i följd"
msgid "The domain name or label is too long"
msgstr "Domännamnet eller en domännamnsdel är för långt"
msgid "Invalid language tag format"
msgstr "Ogiltigt format på språkkoden"
msgid "Unkown language string"
msgstr "Okänd språksträng"
msgid "Invalid IP address"
msgstr "Ogiltig IP-adress"
msgid "Invalid profile format"
msgstr "Ogiltigt profilformat"
msgid "Unknown profile"
msgstr "Okänd profil"

View File

@@ -0,0 +1,2 @@
#Type Path Mode UID GID Age Argument
d /run/zonemaster 0755 zonemaster zonemaster - -

View File

@@ -0,0 +1,9 @@
#!/bin/sh
if [ -z "$1" ] ; then
echo "error: No PO file specified." >&2
exit 2
fi
po_file="$1" ; shift
make update-po POFILES="$po_file"

View File

@@ -0,0 +1,73 @@
#!/bin/sh
#
### BEGIN INIT INFO
# Provides: zm-rpcapi
# Required-Start: $network $local_fs
# Required-Stop: $network $local_fs
# Should-Start: mysql postgresql
# Should-Stop: mysql postgresql
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: A JSON-RPC frontend for Zonemaster Backend
# Description: zm-rpcapi lets you add new tests and check for results in
# the the Zonemaster Backend database
### END INIT INFO
BINDIR=${ZM_BACKEND_BINDIR:-/usr/local/bin}
LOGFILE=${ZM_BACKEND_LOGFILE:-/var/log/zonemaster/zm-rpcapi.log}
PIDFILE=${ZM_BACKEND_PIDFILE:-/var/run/zonemaster/zm-rpcapi.pid}
LISTENIP=${ZM_BACKEND_LISTENIP:-127.0.0.1}
LISTENPORT=${ZM_BACKEND_LISTENPORT:-5000}
USER=${ZM_BACKEND_USER:-zonemaster}
GROUP=${ZM_BACKEND_GROUP:-zonemaster}
STARMAN=`PATH="$PATH:/usr/local/bin" /usr/bin/which starman`
#export ZM_BACKEND_RPCAPI_LOGLEVEL='warning' # Set this variable to override the default log level
. /lib/lsb/init-functions
start () {
$STARMAN --listen=$LISTENIP:$LISTENPORT --preload-app --user=$USER --group=$GROUP --pid=$PIDFILE --error-log=$LOGFILE --daemonize $BINDIR/zonemaster_backend_rpcapi.psgi || exit 1
}
stop () {
if [ -f $PIDFILE ]
then
kill `cat $PIDFILE`
fi
}
status () {
status="0"
pidofproc -p "$PIDFILE" starman >/dev/null || status="$?"
if [ "$status" = 0 ]; then
log_success_msg "zm-rpcapi is running"
return 0
elif [ "$status" = 4 ]; then
log_failure_msg "could not access PID file for zm-rpcapi"
return $status
else
log_failure_msg "zm-rpcapi is not running"
return $status
fi
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart|force-reload)
stop
start
;;
status)
status
;;
*)
echo "usage: $0 [start|stop|restart|force-reload|status]"
exit 1
esac
exit 0

View File

@@ -0,0 +1,13 @@
[Unit]
Description=RPC server for Zonemaster Backend
After=network.target mariadb.service postgresql.service
Wants=mariadb.service postgresql.service
[Service]
Type=simple
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
KillSignal=SIGQUIT
PIDFile=/run/zonemaster/zm-rpcapi.pid
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,60 @@
#!/bin/sh
#
### BEGIN INIT INFO
# Provides: zm-testagent
# Required-Start: $network $local_fs
# Required-Stop: $network $local_fs
# Should-Start: mysql postgresql
# Should-Stop: mysql postgresql
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: An asynchronous execution backend for Zonemaster Backend
# Description: zm-testagent checks the Zonemaster Backend database for new
# tests, executes them and writes back progress and results.
### END INIT INFO
BINDIR=${ZM_BACKEND_BINDIR:-/usr/local/bin}
LOGFILE=${ZM_BACKEND_LOGFILE:-/var/log/zonemaster/zm-testagent.log}
OUTFILE=${ZM_BACKEND_OUTFILE:-/var/log/zonemaster/zm-testagent.out}
PIDFILE=${ZM_BACKEND_PIDFILE:-/var/run/zonemaster/zm-testagent.pid}
USER=${ZM_BACKEND_USER:-zonemaster}
GROUP=${ZM_BACKEND_GROUP:-zonemaster}
#ZM_BACKEND_TESTAGENT_LOGLEVEL='info' # Set this variable to override the default log level
testagent_args="--logfile=$LOGFILE --outfile=$OUTFILE --pidfile=$PIDFILE --user=$USER --group=$GROUP"
if [ -n "$ZM_BACKEND_TESTAGENT_LOGLEVEL" ] ; then
testagent_args="$testagent_args --loglevel=$ZM_BACKEND_TESTAGENT_LOGLEVEL"
fi
start () {
$BINDIR/zonemaster_backend_testagent $testagent_args start || exit 1
}
stop () {
$BINDIR/zonemaster_backend_testagent $testagent_args stop
}
status () {
$BINDIR/zonemaster_backend_testagent $testagent_args status
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart|force-reload)
stop
start
;;
status)
status
;;
*)
echo "usage: $0 [start|stop|restart|status]"
exit 1
esac
exit 0

View File

@@ -0,0 +1,13 @@
[Unit]
Description=test agent for Zonemaster Backend
After=network.target mariadb.service postgresql.service
Wants=mariadb.service postgresql.service
[Service]
Type=simple
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
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
PIDFile=/run/zonemaster/zm-testagent.pid
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,28 @@
#!/bin/sh
# PROVIDE: zm_rpcapi
# REQUIRE: NETWORKING mysql postgresql
# KEYWORD: shutdown
. /etc/rc.subr
name="zm_rpcapi"
rcvar="${name}_enable"
load_rc_config $name
: ${zm_rpcapi_enable="NO"}
: ${zm_rpcapi_user="zonemaster"}
: ${zm_rpcapi_group="zonemaster"}
: ${zm_rpcapi_pidfile="/var/run/zonemaster/${name}.pid"}
: ${zm_rpcapi_logfile="/var/log/zonemaster/${name}.log"}
: ${zm_rpcapi_listen="127.0.0.1:5000"}
export ZONEMASTER_BACKEND_CONFIG_FILE="/usr/local/etc/zonemaster/backend_config.ini"
#export ZM_BACKEND_RPCAPI_LOGLEVEL='warning' # Set this variable to override the default log level
command="/usr/local/bin/starman"
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"
pidfile="${zm_rpcapi_pidfile}"
required_files="/usr/local/etc/zonemaster/backend_config.ini /usr/local/bin/zonemaster_backend_rpcapi.psgi"
run_rc_command "$1"

View File

@@ -0,0 +1,46 @@
#!/bin/sh
# PROVIDE: zm_testagent
# REQUIRE: NETWORKING mysql postgresql
# KEYWORD: shutdown
. /etc/rc.subr
name="zm_testagent"
rcvar="${name}_enable"
load_rc_config $name
: ${zm_testagent_enable="NO"}
: ${zm_testagent_user="zonemaster"}
: ${zm_testagent_group="zonemaster"}
: ${zm_testagent_pidfile="/var/run/zonemaster/${name}.pid"}
export ZONEMASTER_BACKEND_CONFIG_FILE="/usr/local/etc/zonemaster/backend_config.ini"
#ZM_BACKEND_TESTAGENT_LOGLEVEL='info' # Set this variable to override the default log level
# Make Perl available for service() when executed via env() in script
export PATH="$PATH:/usr/local/bin"
command="/usr/local/bin/zonemaster_backend_testagent"
command_args="--user=${zm_testagent_user} --group=${zm_testagent_group} --pidfile=${zm_testagent_pidfile}"
if [ -n "$ZM_BACKEND_TESTAGENT_LOGLEVEL" ] ; then
command_args="$testagent_args --loglevel=$ZM_BACKEND_TESTAGENT_LOGLEVEL"
fi
pidfile="${zm_testagent_pidfile}"
procname="/usr/local/bin/perl"
required_files="/usr/local/etc/zonemaster/backend_config.ini"
start_precmd="${name}_prestart"
stop_precmd="${name}_prestop"
zm_testagent_prestart()
{
rc_flags="${rc_flags} start"
}
zm_testagent_prestop()
{
rc_flags="${rc_flags} stop"
}
run_rc_command "$1"

View File

@@ -0,0 +1,12 @@
use 5.014002;
use strict;
use warnings FATAL => 'all';
use Test::More;
plan tests => 1;
BEGIN {
use_ok( 'Zonemaster::Backend::Config' ) || print "Bail out!\n";
}
done_testing;

View File

@@ -0,0 +1,197 @@
package TestUtil;
use strict;
use warnings;
use Test::More;
use Zonemaster::Engine;
use Zonemaster::Backend::Config;
=head1 NAME
TestUtil - a set of methods to ease Zonemaster::Backend unit testing
=head1 SYNOPSIS
Because this package lies in the testing folder C<t/> and that folder is
unknown to the include path @INC, it can be including using the following code:
my $t_path;
BEGIN {
use File::Spec::Functions qw( rel2abs );
use File::Basename qw( dirname );
$t_path = dirname( rel2abs( $0 ) );
}
use lib $t_path;
use TestUtil;
Explicitely load any dependencies to Zonemaster::Backend::RPCAPI or
Zonemaster::Backend::TestAgent modules with
use TestUtil qw( RPCAPI TestAgent );
=head1 ENVIRONMENT
=head2 TARGET
Set the database to use.
Can be C<SQLite>, C<MySQL> or C<PostgreSQL>.
Default to C<SQLite>.
=head2 ZONEMASTER_RECORD
If set, the data from the test is recorded to a file. Otherwise the data is
loaded from a file.
=cut
# Use the TARGET environment variable to set the database to use
# default to SQLite
my $db_backend = Zonemaster::Backend::Config->check_db( $ENV{TARGET} || 'SQLite' );
note "database: $db_backend";
sub import {
my ( $class, @args ) = @_;
if ( grep { $_ eq 'RPCAPI' } @args ) {
require Zonemaster::Backend::RPCAPI;
Zonemaster::Backend::RPCAPI->import();
}
if ( grep { $_ eq 'TestAgent' } @args ) {
require Zonemaster::Backend::TestAgent;
Zonemaster::Backend::TestAgent->import();
}
}
sub db_backend {
return $db_backend;
}
sub restore_datafile {
my ( $datafile ) = @_;
if ( not $ENV{ZONEMASTER_RECORD} ) {
die q{Stored data file missing} if not -r $datafile;
Zonemaster::Engine->preload_cache( $datafile );
Zonemaster::Engine->profile->set( q{no_network}, 1 );
} else {
diag "recording";
}
}
sub save_datafile {
my ( $datafile ) = @_;
if ( $ENV{ZONEMASTER_RECORD} ) {
Zonemaster::Engine->save_cache( $datafile );
}
}
sub prepare_db {
my ( $db ) = @_;
$db->drop_tables();
$db->create_schema();
}
sub init_db {
my ( $config ) = @_;
my $dbclass = Zonemaster::Backend::DB->get_db_class( $db_backend );
my $db = $dbclass->from_config( $config );
prepare_db( $db );
return $db;
}
sub create_rpcapi {
my ( $config ) = @_;
my $rpcapi;
eval {
$rpcapi = Zonemaster::Backend::RPCAPI->new(
{
dbtype => $db_backend,
config => $config,
}
);
};
if ( $@ ) {
diag explain( $@ );
BAIL_OUT( 'Could not connect to database' );
}
if ( not $rpcapi->isa('Zonemaster::Backend::RPCAPI' ) ) {
BAIL_OUT( 'Not a Zonemaster::Backend::RPCAPI object' );
}
prepare_db( $rpcapi->{db} );
return $rpcapi;
}
sub create_testagent {
my ( $config ) = @_;
my $agent = Zonemaster::Backend::TestAgent->new(
{
dbtype => "$db_backend",
config => $config
}
);
if ( not $agent->isa('Zonemaster::Backend::TestAgent' ) ) {
BAIL_OUT( 'Not a Zonemaster::Backend::TestAgent object' );
}
return $agent;
}
=head1 METHODS
=over
=item db_backend()
Returns the name of the currently used database engine. This value is set via
the TARGET environment variable.
=item restore_datafile($datafile)
If the ZONEMASTER_RECORD environment variable is unset, the data from
C<$datafile> is used for all the current tests.
=item save_datafile($datafile)
If the ZONEMASTER_RECORD environment variable is set, the data from the current
tests are stored to C<$datafile>.
=item prepare_db($db)
Recreate all tables anew for the associated C<$db>.
=item init_db($config)
Returns a new Zonemaster::Backend::DB object using the provided C<$config>
file.
Database tables are dropped and created anew.
=item create_rpcapi($config)
Returns a new Zonemaster::Backend::RPCAPI object using the provided C<$config>
file.
Database tables are dropped and created anew.
=item create_testagent($config)
Returns a new Zonemaster::Backend::TestAgent object using the provided
C<$config> file.
=back
=cut
1;

View File

@@ -0,0 +1,361 @@
use strict;
use warnings;
use 5.14.2;
use Data::Dumper;
use File::Temp qw[tempdir];
use POSIX qw( strftime );
use Time::Local qw( timelocal_modern );
use Test::Exception;
use Test::More; # see done_testing()
use Test::Differences;
my $t_path;
BEGIN {
use File::Spec::Functions qw( rel2abs );
use File::Basename qw( dirname );
$t_path = dirname( rel2abs( $0 ) );
}
use lib $t_path;
use TestUtil qw( RPCAPI );
use Zonemaster::Backend::Config;
my $db_backend = TestUtil::db_backend();
my $tempdir = tempdir( CLEANUP => 1 );
my $config = <<EOF;
[DB]
engine = $db_backend
[MYSQL]
host = localhost
user = zonemaster_test
password = zonemaster
database = zonemaster_test
[POSTGRESQL]
host = localhost
user = zonemaster_test
password = zonemaster
database = zonemaster_test
[SQLITE]
database_file = $tempdir/zonemaster.sqlite
[LANGUAGE]
locale = en_US
[PUBLIC PROFILES]
test_profile=$t_path/test_profile.json
EOF
my $user = {
username => 'user',
api_key => 'key'
};
# define the default properties for the tests
my $params = {
client_id => 'Unit Test',
client_version => '1.0',
ipv4 => JSON::PP::true,
ipv6 => JSON::PP::true,
profile => 'test_profile',
};
# Create Zonemaster::Backend::RPCAPI object
sub init_backend {
my ( $config ) = @_;
my $rpcapi = TestUtil::create_rpcapi( $config );
# create a user
$rpcapi->add_api_user( $user );
return $rpcapi;
}
sub to_timestamp {
my ( $date ) = @_;
my ( $year, $month, $day, $hour, $min, $sec ) = split( /[\s:-]+/, $date );
my $time = timelocal_modern( $sec, $min, $hour, $day, $month-1, $year );
return $time;
}
sub check_tolerance {
my ( $ref_time, $msg ) = @_;
my $current_time = strftime "%Y-%m-%d %H:%M:%S", gmtime( time() );
my $delta = abs( to_timestamp($current_time) - to_timestamp($ref_time) );
my $tolerance = 60; # 1 minute is tolerable between ret_time and current_time
cmp_ok( $delta, '<=', $tolerance, $msg);
}
subtest 'RPCAPI add_batch_job' => sub {
my $config = Zonemaster::Backend::Config->parse( $config );
my $rpcapi = init_backend( $config );
my $dbh = $rpcapi->{db}->dbh;
my @domains = ( 'afnic.fr' );
my $res = $rpcapi->add_batch_job(
{
%$user,
domains => \@domains,
test_params => $params
}
);
is( $res, 1, 'correct batch job id returned' );
subtest 'table "batch_jobs" contains an entry' => sub {
my ( $count ) = $dbh->selectrow_array( q[ SELECT count(*) FROM batch_jobs ] );
is( $count, 1, 'one row in table' );
my ( $id, $username, $created_at ) = $dbh->selectrow_array( q[ SELECT * FROM batch_jobs ]);
is( $id, 1, 'first batch id is 1' );
is( $username, $user->{username}, 'correct batch user' );
ok( $created_at, 'defined creation time' );
check_tolerance( $created_at, 'creation time in tolerance zone' );
};
subtest 'table "test_results" contains an entry' => sub {
my ( $count ) = $dbh->selectrow_array( q[ SELECT count(*) FROM test_results ] );
is( $count, 1, 'one row in table' );
my ( $hash_id, $domain, $batch_id, $created_at, $started_at, $ended_at, $params ) = $dbh->selectrow_array(
q[
SELECT
hash_id,
domain,
batch_id,
created_at,
started_at,
ended_at,
params
FROM test_results
]
);
is( length($hash_id), 16, 'correct hash_id length' );
is( $domain, $domains[0], 'correct domain' );
is( $batch_id, 1, 'correct batch_id' );
ok( $created_at, 'defined creation time' );
check_tolerance( $created_at, 'creation time in tolerance zone' );
ok( ! defined $started_at, 'undefined start time' );
ok( ! defined $ended_at, 'undefined end time' );
};
};
subtest 'RPCAPI batch_status' => sub {
my $config = Zonemaster::Backend::Config->parse( $config );
my $rpcapi = init_backend( $config );
subtest 'batch job exists' => sub {
my @domains = ( 'afnic.fr' );
my $batch_id = $rpcapi->add_batch_job(
{
%$user,
domains => \@domains,
test_params => $params
}
);
is( $batch_id, 1, 'correct batch job id returned' );
my $res = $rpcapi->batch_status( { batch_id => $batch_id } );
is( $res->{waiting_count}, scalar @domains, 'correct number of runninng tests' );
is( $res->{running_count}, 0, 'correct number of finished tests' );
is( $res->{finished_count}, 0, 'correct number of finished tests' );
ok( !exists $res->{waiting_tests}, 'list of waiting tests expected to be absent' );
ok( !exists $res->{running_tests}, 'list of running tests expected to be absent' );
ok( !exists $res->{finished_tests}, 'list of finished tests to be absent' );
};
subtest 'unknown batch (batch_status)' => sub {
my $unknown_batch = 10;
dies_ok {
$rpcapi->batch_status( { batch_id => $unknown_batch } );
} 'getting results for an unknown batch_id should die';
my $res = $@;
is( $res->{error}, 'Zonemaster::Backend::Error::ResourceNotFound', 'correct error type' );
is( $res->{message}, 'Unknown batch', 'correct error message' );
is( $res->{data}->{batch_id}, $unknown_batch, 'correct data type returned' );
};
};
subtest 'batch with several domains' => sub {
my $config = Zonemaster::Backend::Config->parse( $config );
my $rpcapi = init_backend( $config );
my $dbh = $rpcapi->{db}->dbh;
my @domains = sort( 'afnic.fr', 'iis.se' );
my $res = $rpcapi->add_batch_job(
{
%$user,
domains => \@domains,
test_params => $params
}
);
is( $res, 1, 'correct batch job id returned' );
# No lists of test IDs requested
$res = $rpcapi->batch_status( { batch_id => 1 } );
is( $res->{waiting_count}, scalar @domains, 'correct number of running tests' );
is( $res->{running_count}, 0, 'correct number of finished tests' );
is( $res->{finished_count}, 0, 'correct number of finished tests' );
ok( !exists $res->{waiting_tests}, 'list of waiting tests expected to be absent' );
ok( !exists $res->{running_tests}, 'list of running tests expected to be absent' );
ok( !exists $res->{finished_tests}, 'list of finished tests expected to be absent' );
# List of waiting test IDs requested
$res = $rpcapi->batch_status( { batch_id => 1, list_waiting_tests => 1 } );
is( $res->{waiting_count}, scalar @domains, 'correct number of runninng tests' );
is( $res->{running_count}, 0, 'correct number of finished tests' );
is( $res->{finished_count}, 0, 'correct number of finished tests' );
is( scalar @{ $res->{waiting_tests} }, scalar @domains, 'correct number of elements in waiting_tests' );
ok( !exists $res->{running_tests}, 'list of running tests expected to be absent' );
ok( !exists $res->{finished_tests}, 'list of finished tests expected to be absent' );
subtest 'table "test_results" contains 2 entries' => sub {
my ( $count ) = $dbh->selectrow_array( q[ SELECT count(*) FROM test_results ] );
is( $count, @domains, 'two rows in table' );
my $rows = $dbh->selectall_hashref(
q[
SELECT
hash_id,
domain,
batch_id,
created_at,
started_at,
ended_at,
params
FROM test_results
],
'domain'
);
my @keys = sort keys %$rows;
is_deeply( \@keys, \@domains, 'correct domains' );
foreach my $domain ( @keys ) {
is( length($rows->{$domain}->{hash_id}), 16, "[$domain] correct hash_id length" );
is( $rows->{$domain}->{batch_id}, 1, "[$domain] correct batch_id" );
ok( $rows->{$domain}->{created_at}, "[$domain] defined creation time" );
check_tolerance( $rows->{$domain}->{created_at}, "[$domain] creation time in tolerance zone" );
ok( ! defined $rows->{$domain}->{started_at}, "[$domain] undefined start time" );
ok( ! defined $rows->{$domain}->{ended_at}, "[$domain] undefined end time" );
}
};
};
subtest 'batch job still running' => sub {
my $config = Zonemaster::Backend::Config->parse( $config );
my $rpcapi = init_backend( $config );
my $dbh = $rpcapi->{db}->dbh;
my @domains = ( 'afnic.fr' );
my $batch_id = $rpcapi->add_batch_job(
{
%$user,
domains => \@domains,
test_params => $params
}
);
is( $batch_id, 1, 'correct batch job id returned' );
subtest 'a batch is already running for the user, new batch creation should not fail' => sub {
my $batch_id = $rpcapi->add_batch_job(
{
%$user,
domains => \@domains,
test_params => $params
}
);
is( $batch_id, 2, 'same user can create another batch' );
};
subtest 'use another user' => sub {
my $another_user = { username => 'another', api_key => 'token' };
$rpcapi->add_api_user( $another_user );
my $batch_id = $rpcapi->add_batch_job(
{
%$another_user,
domains => \@domains,
test_params => $params
}
);
is( $batch_id, 3, 'another_user can create another batch' );
};
};
subtest 'duplicate user should fail' => sub {
my $config = Zonemaster::Backend::Config->parse( $config );
my $rpcapi = init_backend( $config );
# do not output any error message
my $printerror_before = $rpcapi->{db}->dbh->{PrintError};
$rpcapi->{db}->dbh->{PrintError} = 0;
dies_ok {
$rpcapi->add_api_user( { username => $user->{username}, api_key => "another api key" } );
} 'a user with the same username already exists, add_api_user should die';
my $res = $@;
is( $res->{error}, 'Zonemaster::Backend::Error::Conflict', 'correct error type' );
is( $res->{message}, 'User already exists', 'correct error message' );
is( $res->{data}->{username}, $user->{username}, 'correct data type returned' );
# reset attribute value
$rpcapi->{db}->dbh->{PrintError} = $printerror_before;
};
subtest 'normalize "domain" column' => sub {
my $config = Zonemaster::Backend::Config->parse( $config );
my $rpcapi = init_backend( $config );
my $dbh = $rpcapi->{db}->dbh;
my %domains_to_test = (
"aFnIc.Fr" => "afnic.fr",
"afnic.fr." => "afnic.fr",
"aFnic.Fr." => "afnic.fr"
);
my @domains = keys %domains_to_test;
my $batch_id = $rpcapi->add_batch_job(
{
%$user,
domains => \@domains,
test_params => $params
}
);
my @db_domain = map { $$_[0] } $dbh->selectall_array( "SELECT domain FROM test_results WHERE batch_id=?", undef, $batch_id );
is( @db_domain, 3, '3 tests created' );
my @expected = values %domains_to_test;
is_deeply( \@db_domain, \@expected, 'domains are normalized' );
};
# TODO: create an agent and run batch tests
## Create the agent
#use_ok( 'Zonemaster::Backend::TestAgent' );
#my $agent = Zonemaster::Backend::TestAgent->new( { dbtype => "$db_backend", config => $config } );
#isa_ok($agent, 'Zonemaster::Backend::TestAgent', 'agent');
done_testing();

View File

@@ -0,0 +1,891 @@
use strict;
use warnings;
use utf8;
use Test::More tests => 2;
use Test::NoWarnings;
use Test::Differences;
use Test::Exception;
use Log::Any::Test; # Must come before use Log::Any
use File::Basename qw( dirname );
use File::Slurp qw( read_file );
use File::Spec::Functions qw( catfile );
use Log::Any qw( $log );
subtest 'Everything but NoWarnings' => sub {
use_ok( 'Zonemaster::Backend::Config' );
subtest 'Set values' => sub {
my $text = q{
[DB]
engine = sqlite
polling_interval = 1.5
[MYSQL]
host = mysql-host
port = 3456
user = mysql_user
password = mysql_password
database = mysql_database
[POSTGRESQL]
host = postgresql-host
port = 6543
user = postgresql_user
password = postgresql_password
database = postgresql_database
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[LANGUAGE]
locale = sv_FI
[PUBLIC PROFILES]
default = /path/to/default.profile
two = /path/to/two.profile
[PRIVATE PROFILES]
three = /path/to/three.profile
four = /path/to/four.profile
[ZONEMASTER]
max_zonemaster_execution_time = 1200
number_of_processes_for_frontend_testing = 30
number_of_processes_for_batch_testing = 40
lock_on_queue = 1
age_reuse_previous_test = 800
};
my $config = Zonemaster::Backend::Config->parse( $text );
isa_ok $config, 'Zonemaster::Backend::Config', 'parse() return value';
is $config->DB_engine, 'SQLite', 'set: DB.engine';
is $config->DB_polling_interval, 1.5, 'set: DB.polling_interval';
is $config->MYSQL_host, 'mysql-host', 'set: MYSQL.host';
is $config->MYSQL_port, 3456, 'set: MYSQL.port';
is $config->MYSQL_user, 'mysql_user', 'set: MYSQL.user';
is $config->MYSQL_password, 'mysql_password', 'set: MYSQL.password';
is $config->MYSQL_database, 'mysql_database', 'set: MYSQL.database';
is $config->POSTGRESQL_host, 'postgresql-host', 'set: POSTGRESQL.host';
is $config->POSTGRESQL_port, 6543, 'set: POSTGRESQL.port';
is $config->POSTGRESQL_user, 'postgresql_user', 'set: POSTGRESQL.user';
is $config->POSTGRESQL_password, 'postgresql_password', 'set: POSTGRESQL.password';
is $config->POSTGRESQL_database, 'postgresql_database', 'set: POSTGRESQL.database';
is $config->SQLITE_database_file, '/var/db/zonemaster.sqlite', 'set: SQLITE.database_file';
eq_or_diff { $config->LANGUAGE_locale }, { sv => 'sv_FI' }, 'set: LANGUAGE.locale';
eq_or_diff { $config->PUBLIC_PROFILES }, { #
default => '/path/to/default.profile',
two => '/path/to/two.profile'
},
'set: PUBLIC PROFILES';
eq_or_diff { $config->PRIVATE_PROFILES }, { #
three => '/path/to/three.profile',
four => '/path/to/four.profile'
},
'set: PRIVATE PROFILES';
is $config->ZONEMASTER_max_zonemaster_execution_time, 1200, 'set: ZONEMASTER.max_zonemaster_execution_time';
is $config->ZONEMASTER_number_of_processes_for_frontend_testing, 30, 'set: ZONEMASTER.number_of_processes_for_frontend_testing';
is $config->ZONEMASTER_number_of_processes_for_batch_testing, 40, 'set: ZONEMASTER.number_of_processes_for_batch_testing';
is $config->ZONEMASTER_lock_on_queue, 1, 'set: ZONEMASTER.lock_on_queue';
is $config->ZONEMASTER_age_reuse_previous_test, 800, 'set: ZONEMASTER.age_reuse_previous_test';
};
subtest 'Default values' => sub {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
};
my $config = Zonemaster::Backend::Config->parse( $text );
cmp_ok abs( $config->DB_polling_interval - 0.5 ), '<', 0.000001, 'default: DB.polling_interval';
is $config->MYSQL_port, 3306, 'default: MYSQL.port';
is $config->POSTGRESQL_port, 5432, 'default: POSTGRESQL.port';
eq_or_diff { $config->LANGUAGE_locale }, { en => 'en_US' }, 'default: LANGUAGE.locale';
eq_or_diff { $config->PUBLIC_PROFILES }, { default => undef }, 'default: PUBLIC_PROFILES';
eq_or_diff { $config->PRIVATE_PROFILES }, {}, 'default: PRIVATE_PROFILES';
is $config->ZONEMASTER_max_zonemaster_execution_time, 600, 'default: ZONEMASTER.max_zonemaster_execution_time';
is $config->ZONEMASTER_number_of_processes_for_frontend_testing, 20, 'default: ZONEMASTER.number_of_processes_for_frontend_testing';
is $config->ZONEMASTER_number_of_processes_for_batch_testing, 20, 'default: ZONEMASTER.number_of_processes_for_batch_testing';
is $config->ZONEMASTER_lock_on_queue, 0, 'default: ZONEMASTER.lock_on_queue';
is $config->ZONEMASTER_age_reuse_previous_test, 600, 'default: ZONEMASTER.age_reuse_previous_test';
is $config->RPCAPI_enable_add_api_user, 0, 'default: RPCAPI.enable_add_api_user';
is $config->RPCAPI_enable_add_batch_job, 1, 'default: RPCAPI.enable_add_batch_job';
};
SKIP: {
skip "no more deprecated values", 1;
subtest 'Deprecated values and fallbacks that are unconditional' => sub {
$log->clear();
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
};
my $config = Zonemaster::Backend::Config->parse( $text );
};
}
subtest 'Warnings' => sub {
$log->clear();
my $text = q{
[DB]
engine = MySQL
[MYSQL]
host = localhost
port = 3333
user = mysql_user
password = mysql_password
database = mysql_database
};
my $config = Zonemaster::Backend::Config->parse( $text );
$log->contains_ok( qr/MYSQL\.port.*MYSQL\.host/, 'warning: MYSQL.host is "localhost" and MYSQL.port defined' );
is $config->MYSQL_host, 'localhost', 'set: MYSQL.host';
is $config->MYSQL_port, 3333, 'set: MYSQL.port';
};
throws_ok {
$log->clear();
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[LANGUAGE]
locale =
};
Zonemaster::Backend::Config->parse( $text );
}
qr/Use of empty LANGUAGE.locale property is not permitted/, 'die: Invalid empty locale tag';
throws_ok {
my $text = '{"this":"is","not":"a","valid":"ini","file":"!"}';
Zonemaster::Backend::Config->parse( $text );
}
qr/Failed to parse config/, 'die: Invalid INI format';
throws_ok {
my $text = q{
[DB]
engine = Excel
[SQLITE]
databse_file = /var/db/zonemaster.sqlite
[ZNMEOTAESR]
lock_on_queue = 1
};
Zonemaster::Backend::Config->parse( $text );
}
qr{section.*ZNMEOTAESR}, 'die: Invalid section name';
throws_ok {
my $text = q{
[DB]
engine = SQLite
pnlilog_iatnvrel = 0.5
[SQLITE]
database_file = /var/db/zonemaster.sqlite
};
Zonemaster::Backend::Config->parse( $text );
}
qr{property.*pnlilog_iatnvrel}, 'die: Invalid property name';
throws_ok {
my $text = q{
[DB]
engine = Excel
};
Zonemaster::Backend::Config->parse( $text );
}
qr/DB\.engine.*Excel/, 'die: Invalid DB.engine value';
throws_ok {
my $text = q{
[DB]
engine = SQLite
polling_interval = hourly
[SQLITE]
databse_file = /var/db/zonemaster.sqlite
};
Zonemaster::Backend::Config->parse( $text );
}
qr{DB\.polling_interval.*hourly}, 'die: Invalid DB.polling_interval value';
throws_ok {
my $text = q{
[DB]
engine = MySQL
[MYSQL]
host = 192.0.2.1:3306
user = zonemaster_user
password = zonemaster_password
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr{MYSQL\.host.*192.0.2.1:3306}, 'die: Invalid MYSQL.host value';
throws_ok {
my $text = q{
[DB]
engine = MySQL
[MYSQL]
host = zonemaster-host
user = Robert'); DROP TABLE Students;--
password = zonemaster_password
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr{MYSQL\.user.*Robert'\); DROP TABLE Students;--}, 'die: Invalid MYSQL.user value';
throws_ok {
my $text = q{
[DB]
engine = MySQL
[MYSQL]
host = zonemaster-host
user = zonemaster
password = (╯°□°)╯︵ ┻━┻
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr{MYSQL\.password.*\(╯°□°\)╯︵ ┻━┻}, 'die: Invalid MYSQL.password value';
throws_ok {
my $text = q{
[DB]
engine = MySQL
[MYSQL]
host = zonemaster-host
user = zonemaster_user
password = zonemaster_password
database = |)/-\'|'/-\|3/-\$[-
};
Zonemaster::Backend::Config->parse( $text );
}
qr{MYSQL\.database.*|\)/-\'|'/-\\|3/-\\$[-}, 'die: Invalid MYSQL.database value';
throws_ok {
my $text = q{
[DB]
engine = PostgreSQL
[POSTGRESQL]
host = 192.0.2.1:5432
user = zonemaster_user
password = zonemaster_password
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr{POSTGRESQL\.host.*192.0.2.1:5432}, 'die: Invalid POSTGRESQL.host value';
throws_ok {
my $text = q{
[DB]
engine = PostgreSQL
[POSTGRESQL]
host = zonemaster-host
user = Robert'); DROP TABLE Students;--
password = zonemaster_password
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr{POSTGRESQL\.user.*Robert'\); DROP TABLE Students;--}, 'die: Invalid POSTGRESQL.user value';
throws_ok {
my $text = q{
[DB]
engine = PostgreSQL
[POSTGRESQL]
host = zonemaster-host
user = zonemaster
password = (╯°□°)╯︵ ┻━┻
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr{POSTGRESQL\.password.*\(╯°□°\)╯︵ ┻━┻}, 'die: Invalid POSTGRESQL.password value';
throws_ok {
my $text = q{
[DB]
engine = PostgreSQL
[POSTGRESQL]
host = zonemaster-host
user = zonemaster_user
password = zonemaster_password
database = |)/-\'|'/-\|3/-\$[-
};
Zonemaster::Backend::Config->parse( $text );
}
qr{POSTGRESQL\.database.*|\)/-\'|'/-\\|3/-\\$[-}, 'die: Invalid POSTGRESQL.database value';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = ./relative/path/to/zonemaster.sqlite
};
Zonemaster::Backend::Config->parse( $text );
}
qr{SQLITE\.database_file.*\./relative/path/to/zonemaster.sqlite}, 'die: Invalid SQLITE.database_file value';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[ZONEMASTER]
max_zonemaster_execution_time = 0
};
Zonemaster::Backend::Config->parse( $text );
}
qr{ZONEMASTER\.max_zonemaster_execution_time.*0}, 'die: Invalid ZONEMASTER.max_zonemaster_execution_time value';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[ZONEMASTER]
lock_on_queue = -1
};
Zonemaster::Backend::Config->parse( $text );
}
qr{ZONEMASTER\.lock_on_queue.*-1}, 'die: Invalid ZONEMASTER.lock_on_queue value';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[ZONEMASTER]
number_of_processes_for_frontend_testing = 0
};
Zonemaster::Backend::Config->parse( $text );
}
qr{ZONEMASTER\.number_of_processes_for_frontend_testing.*0}, 'die: Invalid ZONEMASTER.number_of_processes_for_frontend_testing value';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[ZONEMASTER]
number_of_processes_for_batch_testing = 100000
};
Zonemaster::Backend::Config->parse( $text );
}
qr{ZONEMASTER\.number_of_processes_for_batch_testing.*100000}, 'die: Invalid ZONEMASTER.number_of_processes_for_batch_testing value';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[ZONEMASTER]
age_reuse_previous_test = 0
};
Zonemaster::Backend::Config->parse( $text );
}
qr{ZONEMASTER\.age_reuse_previous_test.*0}, 'die: Invalid ZONEMASTER.age_reuse_previous_test value';
throws_ok {
my $text = q{
[DB]
engine = MySQL
[MYSQL]
user = zonemaster_user
password = zonemaster_password
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr/MYSQL\.host/, 'die: Missing MYSQL.host value';
throws_ok {
my $text = q{
[DB]
engine = MySQL
[MYSQL]
host = zonemaster-host
password = zonemaster_password
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr/MYSQL\.user/, 'die: Missing MYSQL.user value';
throws_ok {
my $text = q{
[DB]
engine = MySQL
[MYSQL]
host = zonemaster-host
user = zonemaster_user
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr/MYSQL\.password/, 'die: Missing MYSQL.password value';
throws_ok {
my $text = q{
[DB]
engine = MySQL
[MYSQL]
host = zonemaster-host
user = zonemaster_user
password = zonemaster_password
};
Zonemaster::Backend::Config->parse( $text );
}
qr/MYSQL\.database/, 'die: Missing MYSQL.database value';
throws_ok {
my $text = q{
[DB]
engine = PostgreSQL
[POSTGRESQL]
user = zonemaster_user
password = zonemaster_password
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr/POSTGRESQL\.host/, 'die: Missing POSTGRESQL.host value';
throws_ok {
my $text = q{
[DB]
engine = PostgreSQL
[POSTGRESQL]
host = zonemaster-host
password = zonemaster_password
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr/POSTGRESQL\.user/, 'die: Missing POSTGRESQL.user value';
throws_ok {
my $text = q{
[DB]
engine = PostgreSQL
[POSTGRESQL]
host = zonemaster-host
user = zonemaster_user
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr/POSTGRESQL\.password/, 'die: Missing POSTGRESQL.password value';
throws_ok {
my $text = q{
[DB]
engine = PostgreSQL
[POSTGRESQL]
host = zonemaster-host
user = zonemaster_user
password = zonemaster_password
};
Zonemaster::Backend::Config->parse( $text );
}
qr/POSTGRESQL\.database/, 'die: Missing POSTGRESQL.database value';
throws_ok {
my $text = q{
[DB]
engine = MySQL
[MYSQL]
user = zonemaster_user
password = zonemaster_password
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr/MYSQL\.host/, 'die: Missing MYSQL.host value';
throws_ok {
my $text = q{
[DB]
engine = MySQL
[MYSQL]
host = zonemaster-host
password = zonemaster_password
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr/MYSQL\.user/, 'die: Missing MYSQL.user value';
throws_ok {
my $text = q{
[DB]
engine = MySQL
[MYSQL]
host = zonemaster-host
user = zonemaster_user
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr/MYSQL\.password/, 'die: Missing MYSQL.password value';
throws_ok {
my $text = q{
[DB]
engine = MySQL
[MYSQL]
host = zonemaster-host
user = zonemaster_user
password = zonemaster_password
};
Zonemaster::Backend::Config->parse( $text );
}
qr/MYSQL\.database/, 'die: Missing MYSQL.database value';
throws_ok {
my $text = q{
[DB]
engine = PostgreSQL
[POSTGRESQL]
user = zonemaster_user
password = zonemaster_password
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr/POSTGRESQL\.host/, 'die: Missing POSTGRESQL.host value';
throws_ok {
my $text = q{
[DB]
engine = PostgreSQL
[POSTGRESQL]
host = zonemaster-host
password = zonemaster_password
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr/POSTGRESQL\.user/, 'die: Missing POSTGRESQL.user value';
throws_ok {
my $text = q{
[DB]
engine = PostgreSQL
[POSTGRESQL]
host = zonemaster-host
user = zonemaster_user
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr/POSTGRESQL\.password/, 'die: Missing POSTGRESQL.password value';
throws_ok {
my $text = q{
[DB]
engine = PostgreSQL
[POSTGRESQL]
host = zonemaster-host
user = zonemaster_user
password = zonemaster_password
};
Zonemaster::Backend::Config->parse( $text );
}
qr/POSTGRESQL\.database/, 'die: Missing POSTGRESQL.database value';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[LANGUAGE]
locale = English
};
Zonemaster::Backend::Config->parse( $text );
}
qr/LANGUAGE\.locale.*English/, 'die: Invalid locale_tag in LANGUAGE.locale';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[LANGUAGE]
locale = en_GB en_US
};
Zonemaster::Backend::Config->parse( $text );
}
qr/LANGUAGE\.locale.*en/, 'die: Repeated language code in LANGUAGE.locale';
lives_and {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[PUBLIC PROFILES]
DEFAULT = /path/to/my.profile
[PRIVATE PROFILES]
SECRET = /path/to/my.profile
};
my $config = Zonemaster::Backend::Config->parse( $text );
eq_or_diff { $config->PUBLIC_PROFILES }, { default => '/path/to/my.profile' }, 'normalize profile names under PUBLIC PROFILES';
eq_or_diff { $config->PRIVATE_PROFILES }, { secret => '/path/to/my.profile' }, 'normalize profile names under PRIVATE PROFILES';
};
throws_ok {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[PUBLIC PROFILES]
-invalid-name- = /path/to/my.profile
};
Zonemaster::Backend::Config->parse( $text );
}
qr/PUBLIC PROFILES.*-invalid-name-/, 'die: Invalid profile name in PUBLIC PROFILES';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[PRIVATE PROFILES]
-invalid-name- = /path/to/my.profile
};
Zonemaster::Backend::Config->parse( $text );
}
qr/PRIVATE PROFILES.*-invalid-name-/, 'die: Invalid profile name in PRIVATE PROFILES';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[PUBLIC PROFILES]
valid-name = relative/path/to/my.profile
};
Zonemaster::Backend::Config->parse( $text );
}
qr/absolute.*valid-name/, 'die: Invalid absolute path in PUBLIC PROFILES';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[PRIVATE PROFILES]
valid-name = relative/path/to/my.profile
};
Zonemaster::Backend::Config->parse( $text );
}
qr/absolute.*valid-name/, 'die: Invalid absolute path in PRIVATE PROFILES';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[PUBLIC PROFILES]
valid-name = /path/to/my.profile
valid-name = /path/to/my.profile
};
Zonemaster::Backend::Config->parse( $text );
}
qr/unique.*valid-name/, 'die: Repeated profile name in PUBLIC PROFILES section';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[PRIVATE PROFILES]
valid-name = /path/to/my.profile
valid-name = /path/to/my.profile
};
Zonemaster::Backend::Config->parse( $text );
}
qr/unique.*valid-name/, 'die: Repeated profile name in PRIVATE PROFILES section';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[PUBLIC PROFILES]
pub-and-priv = /path/to/my.profile
[PRIVATE PROFILES]
pub-and-priv = /path/to/my.profile
};
Zonemaster::Backend::Config->parse( $text );
}
qr/unique.*pub-and-priv/, 'die: Repeated profile name across sections';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[PRIVATE PROFILES]
default = /path/to/my.profile
};
Zonemaster::Backend::Config->parse( $text );
}
qr/PRIVATE PROFILES.*default/, 'die: Default profile in PRIVATE PROFILES';
subtest 'RPCAPI experimental aliases' => sub {
subtest 'default values' => sub {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
};
my $config = Zonemaster::Backend::Config->parse( $text );
is $config->RPCAPI_enable_add_api_user, 0, 'default: RPCAPI.enable_add_api_user';
is $config->RPCAPI_enable_add_batch_job, 1, 'default: RPCAPI.enable_add_batch_job';
is $config->RPCAPI_enable_user_create, 0, 'default: RPCAPI.enable_user_create';
is $config->RPCAPI_enable_batch_create, 1, 'default: RPCAPI.enable_batch_create';
};
subtest 'specifying stable and experimental parameters is forbidden' => sub {
throws_ok {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[RPCAPI]
enable_user_create = no
enable_add_api_user = yes
};
Zonemaster::Backend::Config->parse( $text );
}
qr/Error:.+RPCAPI\.enable_add_api_user.+RPCAPI\.enable_user_create/, 'die: RPCAPI stable and experimental alias (add_api_user/user_create)';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[RPCAPI]
enable_add_batch_job = no
enable_batch_create = no
};
Zonemaster::Backend::Config->parse( $text );
}
qr/Error:.+RPCAPI\.enable_add_batch_job.+RPCAPI\.enable_batch_create/, 'die: RPCAPI stable and experimental alias (batch_job/batch_create)';
};
subtest 'setting alias' => sub {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[RPCAPI]
enable_user_create = no
enable_batch_create = no
};
my $config = Zonemaster::Backend::Config->parse( $text );
is $config->RPCAPI_enable_user_create, 0, 'set: RPCAPI.enable_user_create';
is $config->RPCAPI_enable_batch_create, 0, 'set: RPCAPI.enable_batch_create';
is $config->RPCAPI_enable_add_api_user, 0, 'aliased: RPCAPI.enable_add_api_user';
is $config->RPCAPI_enable_add_batch_job, 0, 'aliased: RPCAPI.enable_add_batch_job';
};
};
{
my $path = catfile( dirname( $0 ), '..', 'share', 'backend_config.ini' );
my $text = read_file( $path );
lives_ok {
Zonemaster::Backend::Config->parse( $text );
} 'default config is valid';
}
};

221
zonemaster-backend/t/db.t Normal file
View File

@@ -0,0 +1,221 @@
use strict;
use warnings;
use utf8;
use Encode;
use Test::More; # see done_testing()
use_ok( 'Zonemaster::Backend::DB' );
sub encode_and_fingerprint {
my $params = shift;
my $self = "Zonemaster::Backend::DB";
my $encoded_params = $self->encode_params( $params );
my $fingerprint = $self->generate_fingerprint( $params );
return ( $encoded_params, $fingerprint );
}
subtest 'encoding and fingerprint' => sub {
subtest 'missing properties' => sub {
my %params = ( domain => "example.com" );
my $expected_encoded_params = '{"domain":"example.com","ds_info":[],"ipv4":null,"ipv6":null,"nameservers":[],"profile":"default"}';
my ( $encoded_params, $fingerprint ) = encode_and_fingerprint( \%params );
is $encoded_params, $expected_encoded_params, 'domain only: the encoded strings should match';
#diag ($fingerprint);
my $expected_encoded_params_v4_true = '{"domain":"example.com","ds_info":[],"ipv4":true,"ipv6":null,"nameservers":[],"profile":"default"}';
$params{ipv4} = JSON::PP->true;
my ( $encoded_params_ipv4, $fingerprint_ipv4 ) = encode_and_fingerprint( \%params );
is $encoded_params_ipv4, $expected_encoded_params_v4_true, 'add ipv4: the encoded strings should match';
isnt $fingerprint_ipv4, $fingerprint, 'fingerprints should not match';
};
subtest 'array properties' => sub {
subtest 'ds_info' => sub {
my %params1 = (
domain => "example.com",
ds_info => [{
algorithm => 8,
keytag => 11627,
digtype => 2,
digest => "a6cca9e6027ecc80ba0f6d747923127f1d69005fe4f0ec0461bd633482595448"
}]
);
my %params2 = (
ds_info => [{
digtype => 2,
algorithm => 8,
keytag => 11627,
digest => "a6cca9e6027ecc80ba0f6d747923127f1d69005fe4f0ec0461bd633482595448"
}],
domain => "example.com"
);
my ( $encoded_params1, $fingerprint1 ) = encode_and_fingerprint( \%params1 );
my ( $encoded_params2, $fingerprint2 ) = encode_and_fingerprint( \%params2 );
is $fingerprint1, $fingerprint2, 'ds_info same fingerprint';
is $encoded_params1, $encoded_params2, 'ds_info same encoded string';
};
subtest 'nameservers order' => sub {
my %params1 = (
domain => "example.com",
nameservers => [
{ ns => "ns2.nic.fr", ip => "192.134.4.1" },
{ ns => "ns1.nic.fr" },
{ ip => "192.0.2.1", ns => "ns3.nic.fr"}
]
);
my %params2 = (
nameservers => [
{ ns => "ns3.nic.fr", ip => "192.0.2.1" },
{ ns => "ns1.nic.fr" },
{ ip => "192.134.4.1", ns => "ns2.nic.fr"}
],
domain => "example.com"
);
my %params3 = (
domain => "example.com",
nameservers => [
{ ip => "", ns => "ns1.nic.fr" },
{ ns => "ns3.nic.FR", ip => "192.0.2.1" },
{ ns => "ns2.nic.fr", ip => "192.134.4.1" }
]
);
my %params4 = (
domain => "example.com",
nameservers => [
{ ip => "192.134.4.1", ns => "nS2.Nic.FR"},
{ ns => "Ns1.nIC.fR", ip => "" },
{ ns => "ns3.nic.fr", ip => "192.0.2.1" }
]
);
my ( $encoded_params1, $fingerprint1 ) = encode_and_fingerprint( \%params1 );
my ( $encoded_params2, $fingerprint2 ) = encode_and_fingerprint( \%params2 );
my ( $encoded_params3, $fingerprint3 ) = encode_and_fingerprint( \%params3 );
my ( $encoded_params4, $fingerprint4 ) = encode_and_fingerprint( \%params4 );
is $fingerprint1, $fingerprint2, 'nameservers: same fingerprint';
is $encoded_params1, $encoded_params2, 'nameservers: same encoded string';
is $fingerprint1, $fingerprint3, 'nameservers: same fingerprint (empty ip)';
is $encoded_params1, $encoded_params3, 'nameservers: same encoded string (empty ip)';
is $fingerprint1, $fingerprint4, 'nameservers: same fingerprint (ignore nameservers\' ns case)';
is $encoded_params1, $encoded_params4, 'nameservers: same encoded string (ignore nameservers\' ns case)';
};
};
subtest 'should be case insensitive' => sub {
my %params1 = ( domain => "example.com" );
my %params2 = ( domain => "eXamPLe.COm" );
my ( $encoded_params1, $fingerprint1 ) = encode_and_fingerprint( \%params1 );
my ( $encoded_params2, $fingerprint2 ) = encode_and_fingerprint( \%params2 );
is $fingerprint1, $fingerprint2, 'same fingerprint';
is $encoded_params1, $encoded_params2, 'same encoded string';
};
subtest 'garbage properties set' => sub {
my $expected_encoded_params = '{"client":"GUI v3.3.0","domain":"example.com","ds_info":[],"ipv4":null,"ipv6":null,"nameservers":[],"profile":"default"}';
my %params1 = (
domain => "example.com",
);
my %params2 = (
domain => "example.com",
client => "GUI v3.3.0"
);
my ( $encoded_params1, $fingerprint1 ) = encode_and_fingerprint( \%params1 );
my ( $encoded_params2, $fingerprint2 ) = encode_and_fingerprint( \%params2 );
is $fingerprint1, $fingerprint2, 'leave out garbage property in fingerprint computation...';
is $encoded_params2, $expected_encoded_params, '...but keep it in the encoded string';
};
subtest 'should have different fingerprints' => sub {
subtest 'different profiles' => sub {
my %params1 = (
domain => "example.com",
profile => "profile_1"
);
my %params2 = (
domain => "example.com",
profile => "profile_2"
);
my ( undef, $fingerprint1 ) = encode_and_fingerprint( \%params1 );
my ( undef, $fingerprint2 ) = encode_and_fingerprint( \%params2 );
isnt $fingerprint1, $fingerprint2, 'different profiles, different fingerprints';
};
subtest 'different IP protocols' => sub {
my %params1 = (
domain => "example.com",
ipv4 => "true",
ipv6 => "false"
);
my %params2 = (
domain => "example.com",
ipv4 => "false",
ipv6 => "true"
);
my ( undef, $fingerprint1 ) = encode_and_fingerprint( \%params1 );
my ( undef, $fingerprint2 ) = encode_and_fingerprint( \%params2 );
isnt $fingerprint1, $fingerprint2, 'different IP protocols, different fingerprints';
};
};
subtest 'IDN domain' => sub {
my $expected_encoded_params = encode_utf8( '{"domain":"xn--caf-dma.example","ds_info":[],"ipv4":true,"ipv6":true,"nameservers":[],"profile":"default"}' );
my $expected_fingerprint = '8cb027ff2c175f48aed2623abad0cdd2';
my %params = ( domain => "café.example" );
$params{ipv4} = JSON::PP->true;
$params{ipv6} = JSON::PP->true;
my ( $encoded_params, $fingerprint ) = encode_and_fingerprint( \%params );
is $encoded_params, $expected_encoded_params, 'IDN domain: the encoded strings should match';
is $fingerprint, $expected_fingerprint, 'IDN domain: correct fingerprint';
};
subtest 'final dots' => sub {
subtest 'in domain' => sub {
my %params1 = ( domain => "example.com" );
my %params2 = ( domain => "example.com." );
my $expected_encoded_params = encode_utf8( '{"domain":"example.com","ds_info":[],"ipv4":null,"ipv6":null,"nameservers":[],"profile":"default"}' );
my ( $encoded_params1, $fingerprint1 ) = encode_and_fingerprint( \%params1 );
my ( $encoded_params2, $fingerprint2 ) = encode_and_fingerprint( \%params2 );
is $fingerprint1, $fingerprint2, 'same fingerprint';
is $encoded_params1, $expected_encoded_params, 'the encoded strings should match';
};
subtest 'in nameserver' => sub {
my %params1 = ( domain => "example.com", nameservers => [ { ns => "ns1.example.com." } ] );
my %params2 = ( domain => "example.com", nameservers => [ { ns => "ns1.example.com" } ] );
my $expected_encoded_params = encode_utf8( '{"domain":"example.com","ds_info":[],"ipv4":null,"ipv6":null,"nameservers":[{"ns":"ns1.example.com"}],"profile":"default"}' );
my ( $encoded_params1, $fingerprint1 ) = encode_and_fingerprint( \%params1 );
my ( $encoded_params2, $fingerprint2 ) = encode_and_fingerprint( \%params2 );
is $fingerprint1, $fingerprint2, 'same fingerprint';
is $encoded_params1, $expected_encoded_params, 'the encoded strings should match';
};
subtest 'root is not modified' => sub {
my %params = ( domain => "." );
my $expected_encoded_params = encode_utf8( '{"domain":".","ds_info":[],"ipv4":null,"ipv6":null,"nameservers":[],"profile":"default"}' );
my ( $encoded_params, $fingerprint ) = encode_and_fingerprint( \%params );
is $encoded_params, $expected_encoded_params, 'the encoded strings should match';
};
};
};
done_testing();

View File

@@ -0,0 +1,150 @@
use strict;
use warnings;
use Test::More tests => 2;
use Test::Exception;
use Test::NoWarnings qw(warnings clear_warnings);
use File::ShareDir qw[dist_file];
use File::Temp qw[tempdir];
my $t_path;
BEGIN {
use File::Spec::Functions qw( rel2abs );
use File::Basename qw( dirname );
$t_path = dirname( rel2abs( $0 ) );
}
use lib $t_path;
use TestUtil;
use Zonemaster::Engine;
use Zonemaster::Backend::Config;
my $db_backend = TestUtil::db_backend();
my $tempdir = tempdir( CLEANUP => 1 );
my $config = Zonemaster::Backend::Config->parse( <<EOF );
[DB]
engine = $db_backend
[MYSQL]
host = localhost
user = zonemaster_test
password = zonemaster
database = zonemaster_test
[POSTGRESQL]
host = localhost
user = zonemaster_test
password = zonemaster
database = zonemaster_test
[SQLITE]
database_file = $tempdir/zonemaster.sqlite
[ZONEMASTER]
age_reuse_previous_test = 10
EOF
my $dbclass = Zonemaster::Backend::DB->get_db_class( $db_backend );
my $db = $dbclass->from_config( $config );
subtest 'Everything but Test::NoWarnings' => sub {
subtest 'drop and create' => sub {
subtest 'first drop (cleanup) ... ' => sub {
$db->drop_tables();
dies_ok {
$db->dbh->do( 'SELECT 1 FROM test_results' )
}
'table "test_results" sould not exist';
};
subtest '... then drop after create ...' => sub {
$db->create_schema();
my ( $res ) = $db->dbh->selectrow_array( 'SELECT count(*) FROM test_results' );
is $res, 0, 'a. after create, table "test_results" should exist and be empty';
$db->drop_tables();
dies_ok {
$db->dbh->do( 'SELECT 1 FROM test_results' )
}
'b. after drop, table "test_results" sould be removed';
};
};
subtest 'constraints' => sub {
$db->create_schema();
subtest 'constraint unique' => sub {
my $time = $db->format_time( time() );
my @constraints = (
{
table => 'test_results',
key => 'hash_id',
sql => "INSERT INTO test_results (hash_id,domain,created_at,params)
VALUES ('0123456789abcdef', 'domain.test', '$time', '{}')"
},
{
table => 'log_level',
key => 'level',
sql => "INSERT INTO log_level (level, value) VALUES ('OTHER', 10)"
},
{
table => 'users',
key => 'username',
sql => "INSERT INTO users (username) VALUES ('user1')"
},
);
for my $c (@constraints) {
$db->dbh->do( $c->{sql} );
throws_ok {
$db->dbh->do( $c->{sql} );
}
qr/(unique constraint|duplicate entry)/i, "$c->{table}($c->{key}) key should be unique";
}
};
subtest 'constraint on foreign key' => sub {
subtest 'result_entries - hash_id should exist in test_results(hash_id)' => sub {
my $hash_id_ok = "0123456789abcdef";
# INFO is 1
my $sql = "INSERT INTO result_entries (hash_id, level, module, testcase, tag, timestamp, args)
VALUES ('$hash_id_ok', 1, 'MODULE', 'TESTCASE', 'TAG', 42, '{}')";
my $inserted_rows = $db->dbh->do( $sql );
is $inserted_rows, 1, 'can insert an entry with an existing hash_id';
throws_ok {
my $hash_id_ko = "aaaaaaaaaaaaaaaa";
my $sql = "INSERT INTO result_entries (hash_id, level, module, testcase, tag, timestamp, args)
VALUES ('$hash_id_ko', 1, 'MODULE', 'TESTCASE', 'TAG', 42, '{}')";
$db->dbh->do( $sql );
}
qr/foreign key/i, 'cannot insert an entry with an non-existing hash_id';
};
subtest 'result_entries - level should exist in log_level(level)' => sub {
my $level = 1; # INFO
my $sql = "INSERT INTO result_entries (hash_id, level, module, testcase, tag, timestamp, args)
VALUES ('0123456789abcdef', '$level', 'MODULE', 'TESTCASE', 'TAG', 42, '{}')";
my $inserted_rows = $db->dbh->do( $sql );
is $inserted_rows, 1, 'can insert an entry with an existing level';
throws_ok {
my $level = 42; # does not exist
my $sql = "INSERT INTO result_entries (hash_id, level, module, testcase, tag, timestamp, args)
VALUES ('0123456789abcdef', '$level', 'MODULE', 'TESTCASE', 'TAG', 42, '{}')";
$db->dbh->do( $sql );
}
qr/foreign key/i, 'cannot insert an entry with an non-existing level';
};
};
};
};
# FIXME: hack to avoid getting warnings from Test::NoWarnings
my @warn = warnings();
if ( @warn == 7 ) {
clear_warnings();
}

View File

@@ -0,0 +1,26 @@
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=="}}}}
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}}}}
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"}}}}
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=="}}}}
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}}}}
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=="}}}}
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}}}}
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}}}}
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}}}}
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"}}}}
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}}}}
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"}}}}
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}}}}
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=="}}}}
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}}}}
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}}}}
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}}}}
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"}}}}
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}}}}
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"}}}}
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}}}}
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"}}}}
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=="}}}}
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}}}}
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=="}}}}
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"}}}}

126
zonemaster-backend/t/idn.t Normal file
View File

@@ -0,0 +1,126 @@
use strict;
use warnings;
use 5.14.2;
use Data::Dumper;
use File::Temp qw[tempdir];
use Test::Exception;
use Test::More; # see done_testing()
use utf8;
my $t_path;
BEGIN {
use File::Spec::Functions qw( rel2abs );
use File::Basename qw( dirname );
$t_path = dirname( rel2abs( $0 ) );
}
use lib $t_path;
use TestUtil qw( TestAgent );
use Zonemaster::Backend::Config;
my $db_backend = TestUtil::db_backend();
my $datafile = "$t_path/idn.data";
TestUtil::restore_datafile( $datafile );
my $tempdir = tempdir( CLEANUP => 1 );
my $configuration = <<"EOF";
[DB]
engine = $db_backend
[MYSQL]
host = localhost
user = zonemaster_test
password = zonemaster
database = zonemaster_test
[POSTGRESQL]
host = localhost
user = zonemaster_test
password = zonemaster
database = zonemaster_test
[SQLITE]
database_file = $tempdir/zonemaster.sqlite
[LANGUAGE]
locale = en_US
EOF
if ( $ENV{ZONEMASTER_RECORD} ) {
$configuration .= <<"EOF";
[PUBLIC PROFILES]
test_profile=$t_path/test_profile_network_true.json
default=$t_path/test_profile_network_true.json
EOF
} else {
$configuration .= <<"EOF";
[PUBLIC PROFILES]
test_profile=$t_path/test_profile_no_network.json
default=$t_path/test_profile_no_network.json
EOF
}
my $config = Zonemaster::Backend::Config->parse( $configuration );
my $db = TestUtil::init_db( $config );
my $agent = TestUtil::create_testagent( $config );
# define the default properties for the tests
my $params = {
client_id => 'Unit Test',
client_version => '1.0',
domain => 'café.example',
ipv4 => JSON::PP::true,
ipv6 => JSON::PP::true,
profile => 'default',
};
my $test_id;
subtest 'test IDN domain' => sub {
$test_id = $db->create_new_test( $params->{domain}, $params, 10 );
my $res = $db->get_test_params( $test_id );
note Dumper($res);
is( $res->{domain}, $params->{domain}, 'Retrieve the correct "domain" value' );
};
# run the test
$db->claim_test( $test_id )
or BAIL_OUT( "test needs to be claimed before calling run()" );
$agent->run( $test_id ); # blocking call
subtest 'test get_test_results' => sub {
my $res = $db->test_results( $test_id );
is( $res->{params}->{domain}, $params->{domain}, 'Retrieve the correct domain name' );
};
subtest 'test IDN nameserver' => sub {
$params->{nameservers} = [ { ns => "anøthær.example" } ];
$test_id = $db->create_new_test( $params->{domain}, $params, 10 );
subtest 'get_test_params' => sub {
my $res = $db->get_test_params( $test_id );
note Dumper($res);
is_deeply( $res->{nameservers}, $params->{nameservers}, 'Retrieve the correct "nameservers" value' );
};
# run the test
$db->claim_test( $test_id )
or BAIL_OUT( "test needs to be claimed before calling run()" );
$agent->run( $test_id ); # blocking call
subtest 'test_results' => sub {
my $res = $db->test_results( $test_id );
is_deeply( $res->{params}->{nameservers}, $params->{nameservers}, 'Retrieve the correct nameservers parameters' );
};
};
TestUtil::save_datafile( $datafile );
done_testing();

View File

@@ -0,0 +1,301 @@
use strict;
use warnings;
use 5.14.2;
use Test::More tests => 2;
use Test::Exception;
use Test::NoWarnings;
use Log::Any::Test;
use Log::Any qw( $log );
my $TIME;
BEGIN {
$TIME = CORE::time();
*CORE::GLOBAL::time = sub { $TIME };
}
use Data::Dumper;
use File::ShareDir qw[dist_file];
use File::Temp qw[tempdir];
my $t_path;
BEGIN {
use File::Spec::Functions qw( rel2abs );
use File::Basename qw( dirname );
$t_path = dirname( rel2abs( $0 ) );
}
use lib $t_path;
use TestUtil;
use Zonemaster::Engine;
use Zonemaster::Backend::Config;
use Zonemaster::Backend::DB qw( $TEST_WAITING $TEST_RUNNING $TEST_COMPLETED );
sub advance_time {
my ( $delta ) = @_;
$TIME += $delta;
}
my $db_backend = TestUtil::db_backend();
my $tempdir = tempdir( CLEANUP => 1 );
my $config = Zonemaster::Backend::Config->parse( <<EOF );
[DB]
engine = $db_backend
[MYSQL]
host = localhost
user = zonemaster_test
password = zonemaster
database = zonemaster_test
[POSTGRESQL]
host = localhost
user = zonemaster_test
password = zonemaster
database = zonemaster_test
[SQLITE]
database_file = $tempdir/zonemaster.sqlite
[ZONEMASTER]
age_reuse_previous_test = 10
EOF
sub count_cancellation_messages {
my $results = shift;
return scalar grep { $_->{tag} eq 'UNABLE_TO_FINISH_TEST' } @{ $results->{results} };
}
sub count_died_messages {
my $results = shift;
return scalar grep { $_->{tag} eq 'TEST_DIED' } @{ $results->{results} };
}
subtest 'Everything but Test::NoWarnings' => sub {
lives_ok { # Make sure we get to print log messages in case of errors.
my $db = TestUtil::init_db( $config );
subtest 'State transitions' => sub {
my $testid1 = $db->create_new_test( "1.transition.test", {}, 10 );
is ref $testid1, '', "create_new_test should return 'testid' scalar";
my $current_state = $db->test_state( $testid1 );
is $current_state, $TEST_WAITING, "New test starts out in 'waiting' state.";
my @cases = (
{
old_state => $TEST_WAITING,
transition => [ 'store_results', '{}' ],
throws => qr/illegal transition/,
},
{
old_state => $TEST_WAITING,
transition => ['claim_test'],
returns => 1, # true
new_state => $TEST_RUNNING,
},
{
old_state => $TEST_RUNNING,
transition => ['claim_test'],
returns => '', # false
},
{
old_state => $TEST_RUNNING,
transition => [ 'store_results', '{}' ],
returns => undef,
new_state => $TEST_COMPLETED,
},
{
old_state => $TEST_COMPLETED,
transition => ['claim_test'],
returns => '', #false
},
{
old_state => $TEST_COMPLETED,
transition => [ 'store_results', '{}' ],
throws => qr/illegal transition/,
},
);
for my $case ( @cases ) {
if ( $case->{old_state} ne $current_state ) {
BAIL_OUT( "Assuming to be in '$case->{old_state}' but we're actually in '$current_state'!" );
}
my ( $transition, @args ) = @{ $case->{transition} };
if ( exists $case->{returns} ) {
my $rv_string = Data::Dumper->new( [ $case->{returns} ] )->Indent( 0 )->Terse( 1 )->Dump;
my $result = $db->$transition( $testid1, @args );
is $result,
$case->{returns},
"In state '$case->{old_state}' transition '$transition' should return $rv_string,";
if ( $case->{new_state} ) {
$current_state = $db->test_state( $testid1 );
is $current_state,
$case->{new_state},
"and it should move the test to '$case->{new_state}' state.";
}
else {
$current_state = $db->test_state( $testid1 );
is $current_state,
$case->{old_state},
"and it should not affect the actual state.";
}
}
elsif ( exists $case->{throws} ) {
throws_ok {
$db->$transition( $testid1, @args )
}
$case->{throws}, "In state '$case->{old_state}' transition '$transition' should throw an exception,";
$current_state = $db->test_state( $testid1 );
is $current_state,
$case->{old_state},
"and it should not affect the actual state.";
}
else {
BAIL_OUT( "Invalid case specification!" );
}
}
};
subtest 'Progress' => sub {
my $testid1 = $db->create_new_test( "1.progress.test", {}, 10 );
is ref $testid1, '', "create_new_test should return 'testid' scalar";
throws_ok { $db->test_progress( $testid1, 1 ) } qr/illegal update/, "Setting progress should throw an exception in 'waiting' state.";
$db->claim_test( $testid1 );
# Logically progress is 0 entering the 'running' state, but because
# of implementation details we're clamping it to the range 1-99
# inclusive.
is $db->test_progress( $testid1 ), 1, "Progress should be 1 entering the 'running' state.";
is $db->test_progress( $testid1, 0 ), 1, "Setting progress to 0 should succeed, but actual clamped value is returned,";
is $db->test_progress( $testid1 ), 1, "and it should persist at the clamped value.";
is $db->test_progress( $testid1, 0 ), 1, "Setting the same progress again should succeed.";
is $db->test_progress( $testid1, 2 ), 2, "Setting a higher progress should be allowed,";
is $db->test_progress( $testid1 ), 2, "and it should persist at the new value.";
is $db->test_progress( $testid1, 2 ), 2, "Setting the same progress again should succeed.";
throws_ok { $db->test_progress( $testid1, 0 ) } qr/illegal update/, "Setting a lower progress should throw an exception,";
is $db->test_progress( $testid1 ), 2, "and it should persist at the old value.";
is $db->test_progress( $testid1, 100 ), 99, "Setting progress to 100 should succeed, but actual clamped value is returned,";
is $db->test_progress( $testid1 ), 99, "and it should persist at the clamped value.";
$db->store_results( $testid1, '{}' );
throws_ok { $db->test_progress( $testid1, 100 ) } qr/illegal update/, "Setting progress should throw an exception in 'completed' state.";
};
subtest 'Testid reuse' => sub {
my $testid1 = $db->create_new_test( "zone1.rpcapi.example", {}, 10 );
is ref $testid1, '', 'create_new_test returns "testid" scalar';
advance_time( 11 );
my $testid2 = $db->create_new_test( "zone1.rpcapi.example", {}, 10 );
is $testid2, $testid1, 'reuse is determined from start time (as opposed to creation time)';
$db->claim_test( $testid1 );
advance_time( 10 );
my $testid3 = $db->create_new_test( "zone1.rpcapi.example", {}, 10 );
is $testid3, $testid1, 'old testid is reused before it expires';
advance_time( 1 );
my $testid4 = $db->create_new_test( "zone1.rpcapi.example", {}, 10 );
isnt $testid4, $testid1, 'a new testid is generated after the old one expires';
};
subtest 'Termination of timed out tests' => sub {
my $testid2 = $db->create_new_test( "zone2.rpcapi.example", {}, 10 );
my $testid3 = $db->create_new_test( "zone3.rpcapi.example", {}, 10 );
# testid2 started 11 seconds ago, testid3 started 10 seconds ago
$db->claim_test( $testid2 );
advance_time( 1 );
$db->claim_test( $testid3 );
advance_time( 10 );
$db->process_unfinished_tests( undef, 10 );
is $db->test_progress( $testid3 ), 1, 'leave test alone AT its timeout';
is $db->test_progress( $testid2 ), 100, 'terminate test AFTER its timeout';
is count_cancellation_messages( $db->test_results( $testid3 ) ), 0, 'no cancellation message present AT timeout';
is count_cancellation_messages( $db->test_results( $testid2 ) ), 1, 'one cancellation message present AFTER timeout';
};
subtest 'Termination of crashed tests' => sub {
my $testid4 = $db->create_new_test( "zone4.rpcapi.example", {}, 10 );
$db->claim_test( $testid4 );
$db->process_dead_test( $testid4 );
is $db->test_progress( $testid4 ), 100, 'terminates test';
is count_died_messages( $db->test_results( $testid4 ) ), 1, 'one died message present after crash';
};
subtest 'Do not reuse batch tests' => sub {
my %user = (
username => "user",
api_key => "key"
);
my @domains = ( 'zone1.rpcapi.example', 'zone5.rpcapi.example' );
my $params = {
%user,
domains => \@domains,
test_params => {
priority => 5,
queue => 0
}
};
$db->add_api_user( $user{username}, $user{api_key} );
my $batch_id = $db->add_batch_job( $params );
my @batch_test_ids = $db->dbh->selectall_array(
q[
SELECT hash_id
FROM test_results
WHERE batch_id = ?
],
undef,
$batch_id
);
@batch_test_ids = map { $$_[0] } @batch_test_ids;
if ( @batch_test_ids != 2 ) {
BAIL_OUT( 'There should be 2 tests in database for this batch_id' );
}
my ( $count_zone1 ) = $db->dbh->selectrow_array(
q[
SELECT count(*)
FROM test_results
WHERE domain = 'zone1.rpcapi.example'
]
);
is( $count_zone1, 3, '3 tests for domain "zone1.rpcapi.example' );
my $test_id = $db->create_new_test( 'zone5.rpcapi.example', {}, 10 );
ok( ! grep(/$test_id/, @batch_test_ids), 'new single test should not reuse batch tests' );
};
};
};
for my $msg ( @{ $log->msgs } ) {
my $text = sprintf( "%s: %s", $msg->{level}, $msg->{message} );
if ( $msg->{level} =~ /trace|debug|info|notice/ ) {
note $text;
}
else {
diag $text;
}
}

View File

@@ -0,0 +1,37 @@
#!perl
use v5.14.2;
use strict;
use warnings;
use utf8;
use Test::More tests => 2;
use Test::NoWarnings;
use File::Basename qw( dirname );
chdir dirname( dirname( __FILE__ ) ) or BAIL_OUT( "chdir: $!" );
my $makebin = 'make';
sub make {
my @make_args = @_;
undef $ENV{MAKEFLAGS};
my $command = join( ' ', $makebin, '-s', @make_args );
my $output = `$command 2>&1`;
if ( $? == -1 ) {
BAIL_OUT( "failed to execute: $!" );
}
elsif ( $? & 127 ) {
BAIL_OUT( "child died with signal %d, %s coredump\n", ( $? & 127 ), ( $? & 128 ) ? 'with' : 'without' );
}
return $output, $? >> 8;
}
subtest "distcheck" => sub {
my ( $output, $status ) = make "distcheck";
is $status, 0, $makebin . ' distcheck exits with value 0';
is $output, "", $makebin . ' distcheck gives empty output';
};

View File

@@ -0,0 +1,237 @@
use strict;
use warnings;
use 5.14.2;
use utf8;
use Test::More tests => 4;
use Test::NoWarnings;
use Cwd;
use File::Temp qw[tempdir];
use Zonemaster::Backend::Config;
use Zonemaster::Backend::RPCAPI;
use JSON::Validator::Joi "joi";
use JSON::PP;
my $tempdir = tempdir( CLEANUP => 1 );
my $cwd = cwd();
my $config = Zonemaster::Backend::Config->parse( <<EOF );
[DB]
engine = SQLite
[SQLITE]
database_file = $tempdir/zonemaster.sqlite
[PUBLIC PROFILES]
test = $cwd/t/test_profile.json
EOF
my $rpcapi = Zonemaster::Backend::RPCAPI->new(
{
dbtype => $config->DB_engine,
config => $config,
}
);
sub test_validation {
my ( $method_name, $method_schema, $test_cases ) = @_;
subtest "Method $method_name" => sub {
for my $test_case (@$test_cases) {
subtest 'Test case: ' . $test_case->{name} => sub {
my @res = $rpcapi->validate_params( $method_schema, $test_case->{input});
is_deeply(\@res, $test_case->{output}, 'Matched validation output' ) or diag( encode_json \@res);
};
}
};
}
subtest 'Test JSON schema' => sub {
my $test_joi_schema = joi->new->object->strict->props(
hostname => joi->new->string->max(10)->required
);
my $test_raw_schema = {
type => 'object',
additionalProperties => 0,
required => [ 'hostname' ],
properties => {
hostname => {
type => 'string',
maxLength => 10
}
}
};
my $test_cases = [
{
name => 'Empty request',
input => {},
output => [{
message => 'Missing property',
path => '/hostname'
}]
},
{
name => 'Correct request',
input => {
hostname => 'example'
},
output => []
},
{
name => 'Bad request',
input => {
hostname => 'example.toolong'
},
output => [{
message => 'String is too long: 15/10.',
path => '/hostname'
}]
}
];
test_validation 'test_joi', $test_joi_schema, $test_cases;
test_validation 'test_raw', $test_raw_schema, $test_cases;
};
subtest 'Test custom error message' => sub {
my $test_custom_error_schema = {
type => 'object',
additionalProperties => 0,
required => [ 'hostname' ],
additionalProperties => 0,
properties => {
hostname => {
type => 'string',
'x-error-message' => 'Bad hostname, should be a string less than 10 characters long',
maxLength => 10
},
nameservers => {
type => 'array',
items => {
type => 'object',
required => [ 'ip' ],
additionalProperties => 0,
properties => {
ip => {
type => 'string',
'x-error-message' => 'Bad IP address',
pattern => '^[a-f0-9\.:]+$'
}
}
}
}
}
};
my $test_cases = [
{
name => 'Bad input',
input => {
hostname => 'This is a bad input',
nameservers => [
{ ip => 'Very bad indeed'},
{ ip => '10.10.10.10' },
{ ip => 'But not the previous property' }
]
},
output => [
{
path => '/hostname',
message => 'Bad hostname, should be a string less than 10 characters long',
},
{
path => '/nameservers/0/ip',
message => 'Bad IP address',
},
{
path => '/nameservers/2/ip',
message => 'Bad IP address',
}
]
}
];
test_validation 'test_custom_error', $test_custom_error_schema, $test_cases;
};
subtest 'Test custom formats' => sub {
my $test_extra_validator_schema = {
type => 'object',
properties => {
my_ip => {
type => 'string',
format => 'ip',
},
my_lang => {
type => 'string',
format => 'language_tag',
},
my_domain => {
type => 'string',
format => 'domain',
},
my_profile => {
type => 'string',
format => 'profile',
},
}
};
my $test_cases = [
{
name => 'Input ok',
input => {
my_ip => '192.0.2.1',
my_lang => 'en',
my_domain => 'zonemaster.net',
my_profile => 'test',
},
output => []
},
{
name => 'Bad ip',
input => {
my_ip => 'abc',
},
output => [{
path => '/my_ip',
message => 'Invalid IP address'
}]
},
{
name => 'Bad language format',
input => {
my_lang => 'abc',
},
output => [{
path => '/my_lang',
message => 'Invalid language tag format'
}]
},
{
name => 'Bad domain',
input => {
my_domain => 'not a domain',
},
output => [{
path => '/my_domain',
message => 'Domain name has an ASCII label ("not a domain") with a character not permitted.'
}]
},
{
name => 'Bad profile',
input => {
my_profile => 'other_profile',
},
output => [{
path => '/my_profile',
message => 'Unknown profile'
}]
},
];
test_validation 'test_extra_validator', $test_extra_validator_schema, $test_cases;
};

View File

@@ -0,0 +1,66 @@
#!perl
# This file is not included in the distribution package and not run
# at installation with cpanm().
use v5.14.2;
use strict;
use warnings;
use utf8;
use Test::More; # see done_testing()
use File::Basename qw( dirname );
chdir dirname( dirname( __FILE__ ) ) or BAIL_OUT( "chdir: $!" );
chdir 'share' or BAIL_OUT( "chdir: $!" );
my $makebin = 'make';
sub make {
my @make_args = @_;
undef $ENV{MAKEFLAGS};
my $command = join( ' ', $makebin, '--silent', '--no-print-directory', @make_args );
my $output = `$command`;
if ( $? == -1 ) {
BAIL_OUT( "failed to execute: $!" );
}
elsif ( $? & 127 ) {
BAIL_OUT( "child died with signal %d, %s coredump\n", ( $? & 127 ), ( $? & 128 ) ? 'with' : 'without' );
}
return $output, $? >> 8;
}
subtest "no fuzzy marks" => sub {
my ( $output, $status ) = make "show-fuzzy";
is $status, 0, $makebin . ' show-fuzzy exits with value 0';
is $output, "", $makebin . ' show-fuzzy gives empty output';
};
subtest "check po files" => sub {
my ( $output, $status ) = make "check-po";
is $status, 0, $makebin . ' check-po exits with value 0';
is $output, "", $makebin . ' check-po gives empty output';
};
subtest "tidy po files" => sub {
SKIP: {
my ( $output, $status );
$output = `git diff --numstat`;
skip 'git repo should be clean to run this test', 3 if $output ne '';
( $output, $status ) = make "tidy-po";
is $status, 0, $makebin . ' tidy-po exits with value 0';
is $output, "", $makebin . ' tidy-po gives empty output';
$output = `git diff --numstat`;
is $output, "", 'all files are tidied (if not run "make tidy-po")';
}
};
done_testing();

View File

@@ -0,0 +1,86 @@
use strict;
use warnings;
use 5.14.2;
use Test::More tests => 2;
use Test::NoWarnings;
use Log::Any::Test;
use File::Basename qw( dirname );
use File::Spec::Functions qw( rel2abs );
use File::Temp qw( tempdir );
use Log::Any qw( $log );
use Test::Differences;
use Test::Exception;
use Zonemaster::Backend::Config;
use Zonemaster::Backend::DB qw( $TEST_RUNNING );
use Zonemaster::Engine;
my $t_path;
BEGIN {
$t_path = dirname( rel2abs( $0 ) );
}
use lib $t_path;
use TestUtil;
my $db_backend = TestUtil::db_backend();
my $tempdir = tempdir( CLEANUP => 1 );
my $config = Zonemaster::Backend::Config->parse( <<EOF );
[DB]
engine = $db_backend
[MYSQL]
host = localhost
user = zonemaster_test
password = zonemaster
database = zonemaster_test
[POSTGRESQL]
host = localhost
user = zonemaster_test
password = zonemaster
database = zonemaster_test
[SQLITE]
database_file = $tempdir/zonemaster.sqlite
[ZONEMASTER]
age_reuse_previous_test = 10
EOF
subtest 'Everything but Test::NoWarnings' => sub {
lives_ok { # Make sure we get to print log messages in case of errors.
my $db = TestUtil::init_db( $config );
subtest 'Claiming waiting tests for processing' => sub {
eq_or_diff
[ $db->get_test_request( undef ) ],
[ undef, undef ],
"An empty list is returned when queue is empty";
my $testid1 = $db->create_new_test( "1.claim.test", {}, 10 );
eq_or_diff
[ $db->get_test_request( undef ) ],
[ $testid1, undef ],
"A waiting test is returned if one is available";
eq_or_diff
[ $db->get_test_request( undef ) ],
[ undef, undef ],
"Claimed test is removed from queue";
is
$db->test_state( $testid1 ),
$TEST_RUNNING,
"Claimed test is in 'running' state";
};
};
};
for my $msg ( @{ $log->msgs } ) {
my $text = sprintf( "%s: %s", $msg->{level}, $msg->{message} );
if ( $msg->{level} =~ /trace|debug|info|notice/ ) {
note $text;
}
else {
diag $text;
}
}

View File

@@ -0,0 +1,242 @@
use strict;
use warnings;
use 5.14.2;
use utf8;
use Test::More tests => 30;
use Test::NoWarnings;
use Cwd;
use File::Temp qw[tempdir];
use Zonemaster::Backend::Config;
use Zonemaster::Backend::RPCAPI;
use JSON::Validator::Joi "joi";
use JSON::PP;
###
### Setup
###
my $tempdir = tempdir( CLEANUP => 1 );
my $cwd = cwd();
my $config = Zonemaster::Backend::Config->parse( <<EOF );
[DB]
engine = SQLite
[SQLITE]
database_file = $tempdir/zonemaster.sqlite
[PUBLIC PROFILES]
test = $cwd/t/test_profile.json
EOF
my $rpcapi = Zonemaster::Backend::RPCAPI->new(
{
dbtype => $config->DB_engine,
config => $config,
}
);
###
### JSONRPC request object construction helper
###
sub jsonrpc
{
my ($method, $params, $force_undef) = @_;
my $object = {
jsonrpc => '2.0',
id => 'testing',
method => $method
};
if (defined $params or $force_undef) {
$object->{params} = $params;
}
return $object;
}
###
### JSONRPC error response construction helpers
###
sub jsonrpc_error
{
my ($message, $code, $data, $id) = @_;
my $object = {
jsonrpc => '2.0',
id => $id,
error => {
message => $message,
code => $code
}
};
$object->{error}{data} = $data if defined $data;
return $object;
}
sub error_bad_jsonrpc
{
my ($data) = @_;
jsonrpc_error('The JSON sent is not a valid request object.', '-32600', $data, undef);
}
sub error_missing_params
{
jsonrpc_error("Missing 'params' object", '-32602', undef, 'testing');
}
sub error_bad_params
{
my ($messages) = @_;
my @data;
while (@$messages) {
my $path = shift @$messages;
my $message = shift @$messages;
push @data, { path => $path, message => $message };
}
jsonrpc_error('Invalid method parameter(s).', '-32602', \@data, 'testing');
}
sub no_error
{
return '';
}
###
### Test wrapper functions
###
sub test_validation
{
my ($input, $output, $message) = @_;
my $res = $rpcapi->jsonrpc_validate($input);
is_deeply($res, $output, $message) or diag(encode_json($res));
}
###
### The tests themselves
###
test_validation undef,
error_bad_jsonrpc('/: Expected object - got null.'),
"Sending undef is an error";
test_validation JSON::PP::false,
error_bad_jsonrpc('/: Expected object - got boolean.'),
"Sending a boolean is an error";
test_validation -1,
error_bad_jsonrpc('/: Expected object - got number.'),
"Sending a number is an error";
test_validation "hello",
error_bad_jsonrpc('/: Expected object - got string.'),
"Sending a string is an error";
test_validation [qw(a b c)],
error_bad_jsonrpc('/: Expected object - got array.'),
"Sending an array is an error";
test_validation {},
error_bad_jsonrpc('/jsonrpc: Missing property. /method: Missing property.'),
"Sending an empty object is an error";
test_validation { jsonrpc => '2.0' },
error_bad_jsonrpc('/method: Missing property.'),
"Sending an incomplete object is an error";
test_validation { jsonrpc => '2.0', method => 'system_versions' },
error_bad_jsonrpc(''),
"Sending an object with no ID is an error";
test_validation { jsonrpc => '2.0', method => 'system_versions', id => JSON::PP::false },
error_bad_jsonrpc('/id: Expected null/number/string - got boolean.'),
"Sending an object whose ID is a boolean is an error";
test_validation { jsonrpc => '2.0', method => 'system_versions', id => [qw(a b c)] },
error_bad_jsonrpc('/id: Expected null/number/string - got array.'),
"Sending an object whose ID is an array is an error";
test_validation { jsonrpc => '2.0', method => 'system_versions', id => { a => 1 } },
error_bad_jsonrpc('/id: Expected null/number/string - got object.'),
"Sending an object whose ID is an object is an error";
test_validation jsonrpc("job_status"),
error_missing_params(),
"Calling job_status without parameters is an error";
test_validation jsonrpc("job_status", undef, 1),
error_bad_params(["/" => "Expected object - got null."]),
"Passing null as parameter to job_status is an error";
test_validation jsonrpc("job_status", JSON::PP::false),
error_bad_params(["/" => "Expected object - got boolean."]),
"Passing boolean as parameter to job_status is an error";
test_validation jsonrpc("job_status", 1),
error_bad_params(["/" => "Expected object - got number."]),
"Passing number as parameter to job_status is an error";
test_validation jsonrpc("job_status", "hello"),
error_bad_params(["/" => "Expected object - got string."]),
"Passing string as parameter to job_status is an error";
test_validation jsonrpc("job_status", [qw(a b c)]),
error_bad_params(["/" => "Expected object - got array."]),
"Passing array as parameter to job_status is an error";
test_validation jsonrpc("job_status", {}),
error_bad_params(["/job_id" => "Missing property"]),
"Passing empty object as parameter to job_status is an error";
test_validation jsonrpc("job_status", { job_id => 'this_will_definitely_never_ever_exist' }),
error_bad_params(["/job_id" => 'String does not match (?^u:^[0-9a-f]{16}$).']),
"Calling job_status with a bad job_id is an error";
test_validation jsonrpc("job_status", { job_id => '0123456789abcdef', data => "something" }),
error_bad_params(["/" => "Properties not allowed: data."]),
"Calling job_status with unknown parameters is an error";
test_validation jsonrpc("job_status", { job_id => '0123456789abcdef' }),
no_error,
"Calling job_status with a good job_id succeeds";
test_validation jsonrpc("system_versions"),
no_error,
"Calling system_versions with no parameters is OK";
test_validation jsonrpc("system_versions", undef, 1),
error_bad_params(["/" => "Expected object - got null."]),
"Passing null as parameter to system_versions is an error";
test_validation jsonrpc("system_versions", JSON::PP::false),
error_bad_params(["/" => "Expected object - got boolean."]),
"Passing number as parameter to system_versions is an error";
test_validation jsonrpc("system_versions", -1),
error_bad_params(["/" => "Expected object - got number."]),
"Passing number as parameter to system_versions is an error";
test_validation jsonrpc("system_versions", "hello"),
error_bad_params(["/" => "Expected object - got string."]),
"Passing string as parameter to system_versions is an error";
test_validation jsonrpc("system_versions", [qw(a b c)]),
error_bad_params(["/" => "Expected object - got array."]),
"Passing array as parameter to system_versions is an error";
test_validation jsonrpc("system_versions", { data => "something" }),
error_bad_params(["/" => "Properties not allowed: data."]),
"Calling system_versions with unrecognized parameter is an error";
test_validation jsonrpc("system_versions", {}),
no_error,
"Calling system_versions with empty object succeeds";

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,425 @@
use strict;
use warnings;
use 5.14.2;
my $t_path;
BEGIN {
use File::Spec::Functions qw( rel2abs );
use File::Basename qw( dirname );
$t_path = dirname( rel2abs( $0 ) );
}
use lib $t_path;
use TestUtil qw( RPCAPI TestAgent );
use Data::Dumper;
use File::Temp qw[tempdir];
use Test::Exception;
use Test::More; # see done_testing()
use Zonemaster::Engine;
use Zonemaster::Backend::Config;
my $db_backend = TestUtil::db_backend();
my $datafile = "$t_path/test01.data";
TestUtil::restore_datafile( $datafile );
my $tempdir = tempdir( CLEANUP => 1 );
my $configuration = <<"EOF";
[DB]
engine = $db_backend
[MYSQL]
host = localhost
user = zonemaster_test
password = zonemaster
database = zonemaster_test
[POSTGRESQL]
host = localhost
user = zonemaster_test
password = zonemaster
database = zonemaster_test
[SQLITE]
database_file = $tempdir/zonemaster.sqlite
[LANGUAGE]
locale = en_US
EOF
if ( $ENV{ZONEMASTER_RECORD} ) {
$configuration .= <<"EOF";
[PUBLIC PROFILES]
test_profile=$t_path/test_profile_network_true.json
default=$t_path/test_profile_network_true.json
EOF
} else {
$configuration .= <<"EOF";
[PUBLIC PROFILES]
test_profile=$t_path/test_profile_no_network.json
default=$t_path/test_profile_no_network.json
EOF
}
my $config = Zonemaster::Backend::Config->parse( $configuration );
my $rpcapi = TestUtil::create_rpcapi( $config );
my $dbh = $rpcapi->{db}->dbh;
# Create the agent
my $agent = TestUtil::create_testagent( $config );
# define the default properties for the tests
my $params = {
client_id => 'Unit Test',
client_version => '1.0',
domain => 'afnic.fr',
ipv4 => JSON::PP::true,
ipv6 => JSON::PP::true,
profile => 'test_profile',
nameservers => [
{ ns => 'ns1.nic.fr' },
{ ns => 'ns2.nic.fr', ip => '192.134.4.1' }
],
ds_info => [
{
keytag => 11627,
algorithm => 8,
digtype => 2,
digest => 'a6cca9e6027ecc80ba0f6d747923127f1d69005fe4f0ec0461bd633482595448'
}
]
};
my $hash_id;
# This is the first test added to the DB, its 'id' is 1
my $test_id = 1;
subtest 'add a first test' => sub {
$hash_id = $rpcapi->start_domain_test( $params );
ok( $hash_id, "API start_domain_test OK" );
is( length($hash_id), 16, "Test has a 16 characters length hash ID (hash_id=$hash_id)" );
my ( $test_id_db, $hash_id_db ) = $dbh->selectrow_array( "SELECT id, hash_id FROM test_results WHERE id=?", undef, $test_id );
is( $test_id_db, $test_id , 'API start_domain_test -> Test inserted in the DB' );
is( $hash_id_db, $hash_id , 'Correct hash_id in database' );
# test test_progress API
my $progress = $rpcapi->test_progress( { test_id => $hash_id } );
is( $progress, 0 , 'Test has been created, its progress is 0' );
};
subtest 'get and run test' => sub {
my ( $hash_id_from_db ) = $rpcapi->{db}->get_test_request();
is( $hash_id_from_db, $hash_id, 'Get correct test to run' );
my $progress = $rpcapi->test_progress( { test_id => $hash_id } );
is( $progress, 1, 'Test has been picked, its progress is 1' );
diag "running the agent on test $hash_id";
$agent->run( $hash_id ); # blocking call
$progress = $rpcapi->test_progress( { test_id => $hash_id } );
is( $progress, 100 , 'Test has finished, its progress is 100' );
};
subtest 'API calls' => sub {
subtest 'get_test_results' => sub {
local $@ = undef;
my $res = eval { $rpcapi->get_test_results( { id => $hash_id, language => 'en' } ) };
if ( $@ ) {
fail 'Crashed while fetching job results: ' . Dumper( $@ );
}
ok( ! defined $res->{id}, 'Do not expose primary key' );
is( $res->{hash_id}, $hash_id, 'Retrieve the correct "hash_id"' );
ok( defined $res->{params}, 'Value "params" properly defined' );
ok( ! exists $res->{creation_time}, 'Key "creation_time" should be missing' );
ok( defined $res->{created_at}, 'Value "created_at" properly defined' );
ok( defined $res->{results}, 'Value "results" properly defined' );
if ( @{ $res->{results} } > 1 ) {
pass 'The test has some results';
}
else {
fail 'The test has some results: ' . Dumper( $res->{results} );
}
};
subtest 'get_test_params' => sub {
my $res = $rpcapi->get_test_params( { test_id => $hash_id } );
is( $res->{domain}, $params->{domain}, 'Retrieve the correct "domain" value' );
is( $res->{profile}, $params->{profile}, 'Retrieve the correct "profile" value' );
is( $res->{client_id}, $params->{client_id}, 'Retrieve the correct "client_id" value' );
is( $res->{client_version}, $params->{client_version}, 'Retrieve the correct "client_version" value' );
is( $res->{ipv4}, $params->{ipv4}, 'Retrieve the correct "ipv4" value' );
is( $res->{ipv6}, $params->{ipv6}, 'Retrieve the correct "ipv6" value' );
is_deeply( $res->{nameservers}, $params->{nameservers}, 'Retrieve the correct "nameservers" value' );
is_deeply( $res->{ds_info}, $params->{ds_info}, 'Retrieve the correct "ds_info" value' );
};
subtest 'add_api_user' => sub {
my $res;
eval {
$res = $rpcapi->add_api_user( { username => "zonemaster_test", api_key => "zonemaster_test's api key" } );
};
is( $res, 1, 'API add_api_user success');
my $user_check_query = q/SELECT * FROM users WHERE username = 'zonemaster_test'/;
is( scalar( $dbh->selectrow_array( $user_check_query ) ), 1 ,'API add_api_user user created' );
};
subtest 'version_info' => sub {
my $res = $rpcapi->version_info();
ok( defined( $res->{zonemaster_ldns} ), 'Has a "zonemaster_ldns" key' );
ok( defined( $res->{zonemaster_engine} ), 'Has a "zonemaster_engine" key' );
ok( defined( $res->{zonemaster_backend} ), 'Has a "zonemaster_backend" key' );
};
subtest 'profile_names' => sub {
my $res = $rpcapi->profile_names();
is( scalar( @$res ), 2, 'There are exactly 2 public profiles' );
ok( grep( /default/, @$res ), 'The profile "default" is defined' );
ok( grep( /test_profile/, @$res ), 'The profile "test_profile" is defined' );
};
subtest 'get_data_from_parent_zone' => sub {
my $res = $rpcapi->get_data_from_parent_zone( { domain => "fr" } );
note explain( $res );
ok( defined( $res->{ns_list} ), 'Has a list of nameservers' );
ok( defined( $res->{ds_list} ), 'Has a list of DS records' );
my @ns_list = map { $_->{ns} } @{ $res->{ns_list} };
ok( grep( /d\.nic\.fr/, @ns_list ), 'Has "d.nic.fr" nameserver' );
ok( grep( /f\.ext\.nic\.fr/, @ns_list ), 'Has "f.ext.nic.fr" nameserver' );
ok( grep( /g\.ext\.nic\.fr/, @ns_list ), 'Has "g.ext.nic.fr" nameserver' );
my @ip_list = map { $_->{ip} } @{ $res->{ns_list} };
ok( grep( /194\.0\.9\.1/, @ip_list ), 'Has "194.0.9.1" ip' ); # d.nic.fr
ok( grep( /2001:678:c::1/, @ip_list ), 'Has "2001:678:c::1" ip' );
ok( grep( /194\.0\.36\.1/, @ip_list ), 'Has "194.0.36.1" ip' ); # g.ext.nic.fr
ok( grep( /2001:678:4c::1/, @ip_list ), 'Has "2001:678:4c::1" ip' );
ok( grep( /194\.146\.106\.46/, @ip_list ), 'Has "194.146.106.46" ip' ); # f.ext.nic.fr
ok( grep( /2001:67c:1010:11::53/, @ip_list ), 'Has "2001:67c:1010:11::53" ip' );
my $ds_value = {
'algorithm' => 13,
'digest' => '1303e8da8fb60db500d5bea1ee5dc9a2bcc93dfe2fc43d346576658feccf5749', # must match case
'digtype' => 2,
'keytag' => 29133
};
is( scalar( @{ $res->{ds_list} } ), 1, 'Has only one DS set' );
is_deeply( $res->{ds_list}[0], $ds_value, 'Has correct DS values' );
};
};
# start a second test with IPv6 disabled
$params->{ipv6} = 0;
$hash_id = $rpcapi->start_domain_test( $params );
$rpcapi->{db}->claim_test( $hash_id )
or BAIL_OUT( "test needs to be claimed before calling run()" );
diag "running the agent on test $hash_id";
$agent->run($hash_id);
subtest 'second test has IPv6 disabled' => sub {
my $res = $rpcapi->get_test_params( { test_id => $hash_id } );
is( $res->{ipv4}, $params->{ipv4}, 'Retrieve the correct "ipv4" value' );
is( $res->{ipv6}, $params->{ipv6}, 'Retrieve the correct "ipv6" value' );
$res = $rpcapi->get_test_results( { id => $hash_id, language => 'en' } );
my @msgs = map { $_->{message} } @{ $res->{results} };
ok( grep( /IPv6 is disabled/, @msgs ), 'Results contain an "IPv6 is disabled" message' );
};
my $test_history;
subtest 'get_test_history' => sub {
my $offset = 0;
my $limit = 10;
my $method_params = {
frontend_params => { domain => $params->{domain} },
offset => $offset,
limit => $limit
};
$test_history = $rpcapi->get_test_history( $method_params );
note explain( $test_history );
is( scalar( @$test_history ), 2, 'Two tests created' );
foreach my $res (@$test_history) {
is( length($res->{id}), 16, 'Test has 16 characters length hash ID' );
is( $res->{undelegated}, JSON::PP::true, 'Test is undelegated' );
ok( ! exists $res->{creation_time}, 'Key "creation_time" should be missing' );
ok( defined $res->{created_at}, 'Value "created_at" properly defined' );
ok( defined $res->{overall_result}, 'Value "overall_result" properly defined' );
}
subtest 'include finished tests only' => sub {
# start a thirs test with IPv4 disabled
$params->{ipv6} = 1;
$params->{ipv4} = 0;
# create the test, retrieve its id but we don't run it
$rpcapi->start_domain_test( $params );
( $hash_id ) = $rpcapi->{db}->get_test_request();
$test_history = $rpcapi->get_test_history( $method_params );
note explain( $test_history );
is( scalar( @$test_history ), 2, 'Only 2 tests should be retrieved' );
# now run the test
diag "running the agent on test $hash_id";
$agent->run( $hash_id );
$test_history = $rpcapi->get_test_history( $method_params );
is( scalar( @$test_history ), 3, 'Now 3 tests should be retrieved' );
}
};
subtest 'mock another client (i.e. reuse a previous test)' => sub {
$params->{client_id} = 'Another Client';
$params->{client_version} = '0.1';
my $new_hash_id = $rpcapi->start_domain_test( $params );
is( $new_hash_id, $hash_id, 'Has the same hash than previous test' );
subtest 'check test_params values' => sub {
my $res = $rpcapi->get_test_params( { test_id => "$hash_id" } );
# the following values are part of the fingerprint
is( $res->{domain}, $params->{domain}, 'Retrieve the correct "domain" value' );
is( $res->{profile}, $params->{profile}, 'Retrieve the correct "profile" value' );
is( $res->{ipv4}, $params->{ipv4}, 'Retrieve the correct "ipv4" value' );
is( $res->{ipv6}, $params->{ipv6}, 'Retrieve the correct "ipv6" value' );
is_deeply( $res->{nameservers}, $params->{nameservers}, 'Retrieve the correct "nameservers" value' );
is_deeply( $res->{ds_info}, $params->{ds_info}, 'Retrieve the correct "ds_info" value' );
# both client_id and client_version are different since an old test has been reused
isnt( $res->{client_id}, $params->{client_id}, 'The "client_id" value is not the same (which is fine)' );
isnt( $res->{client_version}, $params->{client_version}, 'The "client_version" value is not the same (which is fine)' );
};
};
subtest 'check historic tests' => sub {
# Verifies that delegated and undelegated tests are coded correctly when started
# and that the filter option in "get_test_history" works correctly
my $domain = 'xa';
# Non-batch for "start_domain_test":
my $params_un1 = { # undelegated, non-batch
domain => $domain,
nameservers => [
{ ns => 'ns2.nic.fr', ip => '192.134.4.1' },
],
};
my $params_un2 = { # undelegated, non-batch
domain => $domain,
ds_info => [
{ keytag => 11627, algorithm => 8, digtype => 2, digest => 'a6cca9e6027ecc80ba0f6d747923127f1d69005fe4f0ec0461bd633482595448' },
],
};
my $params_dn1 = { # delegated, non-batch
domain => $domain,
};
# Batch for "add_batch_job"
my $domain2 = 'xb';
my $params_ub1 = { # undelegated, batch
domains => [ $domain, $domain2 ],
test_params => {
nameservers => [
{ ns => 'ns2.nic.fr', ip => '192.134.4.1' },
],
},
};
my $params_ub2 = { # undelegated, batch
domains => [ $domain, $domain2 ],
test_params => {
ds_info => [
{ keytag => 11627, algorithm => 8, digtype => 2, digest => 'a6cca9e6027ecc80ba0f6d747923127f1d69005fe4f0ec0461bd633482595448' },
],
},
};
my $params_db1 = { # delegated, batch
domains => [ $domain, $domain2 ],
};
# The batch jobs, $params_ub1, $params_ub2 and $params_db1, cannot be run from here due to limitation in the API. See issue #827.
foreach my $param ($params_un1, $params_un2, $params_dn1) {
my $testid = $rpcapi->start_domain_test( $param );
ok( $testid, "API start_domain_test ID OK" );
$rpcapi->{db}->claim_test( $testid )
or BAIL_OUT( "test needs to be claimed before calling run()" );
diag "running the agent on test $testid";
$agent->run( $testid );
is( $rpcapi->test_progress( { test_id => $testid } ), 100 , 'API test_progress -> Test finished' );
};
my $test_history_delegated = $rpcapi->get_test_history(
{
filter => 'delegated',
frontend_params => {
domain => $domain,
}
} );
my $test_history_undelegated = $rpcapi->get_test_history(
{
filter => 'undelegated',
frontend_params => {
domain => $domain,
}
} );
note explain( $test_history_delegated );
is( scalar( @$test_history_delegated ), 1, 'One delegated test created' );
note explain( $test_history_undelegated );
is( scalar( @$test_history_undelegated ), 2, 'Two undelegated tests created' );
subtest 'domain is case and trailing dot insensitive' => sub {
my $test_history_delegated = $rpcapi->get_test_history(
{
filter => 'delegated',
frontend_params => {
domain => $domain . '.',
}
} );
my $test_history_undelegated = $rpcapi->get_test_history(
{
filter => 'undelegated',
frontend_params => {
domain => ucfirst( $domain ),
}
} );
is( scalar( @$test_history_delegated ), 1, 'One delegated test created' );
is( scalar( @$test_history_undelegated ), 2, 'Two undelegated tests created' );
};
};
subtest 'normalize "domain" column' => sub {
my %domains_to_test = (
"aFnIc.Fr" => "afnic.fr",
"afnic.fr." => "afnic.fr",
"aFnic.Fr." => "afnic.fr"
);
my $test_params = {
client_id => 'Unit Test',
client_version => '1.0',
};
while ( my ($domain, $expected) = each (%domains_to_test) ) {
$test_params->{domain} = $domain;
$hash_id = $rpcapi->start_domain_test( $test_params );
my ( $db_domain ) = $dbh->selectrow_array( "SELECT domain FROM test_results WHERE hash_id=?", undef, $hash_id );
is( $db_domain, $expected, 'stored domain name is normalized' );
}
};
TestUtil::save_datafile( $datafile );
done_testing();

View File

@@ -0,0 +1,84 @@
{
"logfilter": {
"BASIC":{
"IPV6_DISABLED" : [
{
"when": {
"rrtype": [ "SOA", "NS" ]
},
"set": "INFO"
}
]
}
},
"test_cases": [
"address01",
"address02",
"address03",
"basic00",
"basic01",
"basic02",
"basic03",
"connectivity01",
"connectivity02",
"connectivity03",
"consistency01",
"consistency02",
"consistency03",
"consistency04",
"consistency05",
"consistency06",
"dnssec01",
"dnssec02",
"dnssec03",
"dnssec04",
"dnssec05",
"dnssec07",
"dnssec06",
"dnssec08",
"dnssec09",
"dnssec10",
"dnssec11",
"dnssec13",
"dnssec14",
"dnssec15",
"dnssec16",
"dnssec17",
"dnssec18",
"delegation01",
"delegation02",
"delegation03",
"delegation04",
"delegation05",
"delegation06",
"delegation07",
"nameserver01",
"nameserver02",
"nameserver04",
"nameserver05",
"nameserver06",
"nameserver07",
"nameserver10",
"nameserver11",
"nameserver12",
"nameserver13",
"syntax01",
"syntax02",
"syntax03",
"syntax04",
"syntax05",
"syntax06",
"syntax07",
"syntax08",
"zone01",
"zone02",
"zone03",
"zone04",
"zone05",
"zone06",
"zone07",
"zone08",
"zone09",
"zone10"
]
}

View File

@@ -0,0 +1,85 @@
{
"no_network" : false,
"logfilter": {
"BASIC":{
"IPV6_DISABLED" : [
{
"when": {
"rrtype": [ "SOA", "NS" ]
},
"set": "INFO"
}
]
}
},
"test_cases": [
"address01",
"address02",
"address03",
"basic00",
"basic01",
"basic02",
"basic03",
"connectivity01",
"connectivity02",
"connectivity03",
"consistency01",
"consistency02",
"consistency03",
"consistency04",
"consistency05",
"consistency06",
"dnssec01",
"dnssec02",
"dnssec03",
"dnssec04",
"dnssec05",
"dnssec07",
"dnssec06",
"dnssec08",
"dnssec09",
"dnssec10",
"dnssec11",
"dnssec13",
"dnssec14",
"dnssec15",
"dnssec16",
"dnssec17",
"dnssec18",
"delegation01",
"delegation02",
"delegation03",
"delegation04",
"delegation05",
"delegation06",
"delegation07",
"nameserver01",
"nameserver02",
"nameserver04",
"nameserver05",
"nameserver06",
"nameserver07",
"nameserver10",
"nameserver11",
"nameserver12",
"nameserver13",
"syntax01",
"syntax02",
"syntax03",
"syntax04",
"syntax05",
"syntax06",
"syntax07",
"syntax08",
"zone01",
"zone02",
"zone03",
"zone04",
"zone05",
"zone06",
"zone07",
"zone08",
"zone09",
"zone10"
]
}

View File

@@ -0,0 +1,85 @@
{
"no_network" : true,
"logfilter": {
"BASIC":{
"IPV6_DISABLED" : [
{
"when": {
"rrtype": [ "SOA", "NS" ]
},
"set": "INFO"
}
]
}
},
"test_cases": [
"address01",
"address02",
"address03",
"basic00",
"basic01",
"basic02",
"basic03",
"connectivity01",
"connectivity02",
"connectivity03",
"consistency01",
"consistency02",
"consistency03",
"consistency04",
"consistency05",
"consistency06",
"dnssec01",
"dnssec02",
"dnssec03",
"dnssec04",
"dnssec05",
"dnssec07",
"dnssec06",
"dnssec08",
"dnssec09",
"dnssec10",
"dnssec11",
"dnssec13",
"dnssec14",
"dnssec15",
"dnssec16",
"dnssec17",
"dnssec18",
"delegation01",
"delegation02",
"delegation03",
"delegation04",
"delegation05",
"delegation06",
"delegation07",
"nameserver01",
"nameserver02",
"nameserver04",
"nameserver05",
"nameserver06",
"nameserver07",
"nameserver10",
"nameserver11",
"nameserver12",
"nameserver13",
"syntax01",
"syntax02",
"syntax03",
"syntax04",
"syntax05",
"syntax06",
"syntax07",
"syntax08",
"zone01",
"zone02",
"zone03",
"zone04",
"zone05",
"zone06",
"zone07",
"zone08",
"zone09",
"zone10"
]
}

View File

@@ -0,0 +1,289 @@
use strict;
use warnings;
use 5.14.2;
use utf8;
use Test::More tests => 2;
use Test::NoWarnings;
use Encode;
use File::ShareDir qw[dist_file];
use JSON::PP;
use File::Temp qw[tempdir];
use Zonemaster::Backend::Config;
use Zonemaster::Backend::RPCAPI;
my $tempdir = tempdir( CLEANUP => 1 );
my $config = Zonemaster::Backend::Config->parse( <<EOF );
[DB]
engine = SQLite
[SQLITE]
database_file = $tempdir/zonemaster.sqlite
[LANGUAGE]
locale = en_US fr_FR da_DK fi_FI nb_NO sl_SI sv_SE
EOF
my $engine = Zonemaster::Backend::RPCAPI->new(
{
dbtype => $config->DB_engine,
config => $config,
}
);
sub start_domain_validate_params {
return $engine->validate_params( $Zonemaster::Backend::RPCAPI::json_schemas{start_domain_test}, @_ );
}
subtest 'Everything but NoWarnings' => sub {
my $can_use_threads = eval 'use threads; 1';
my $frontend_params = {
ipv4 => 1,
ipv6 => 1,
};
$frontend_params->{nameservers} = [ # list of the namaserves up to 32
{ ns => 'ns1.nic.fr', ip => '1.2.3.4' }, # key values pairs representing nameserver => namesterver_ip
{ ns => 'ns2.nic.fr', ip => '192.134.4.1' },
];
subtest 'domain present' => sub {
my @res = start_domain_validate_params(
{
%$frontend_params, domain => 'afnic.fr'
}
);
is( scalar @res, 0 );
};
subtest 'consecutive dots' => sub {
my @res = start_domain_validate_params(
{
%$frontend_params, domain => 'afnic..fr'
}
);
is( scalar @res, 1 );
};
subtest encode_utf8( 'idn domain=[é]' ) => sub {
my @res = start_domain_validate_params(
{
%$frontend_params, domain => 'é'
}
);
is( scalar @res, 0 )
or diag( encode_json @res );
};
subtest encode_utf8( 'idn domain=[éé]' ) => sub {
my @res = start_domain_validate_params(
{
%$frontend_params, domain => 'éé'
}
);
is( scalar @res, 0 )
or diag( encode_json @res );
};
subtest '253 characters long domain without dot' => sub {
my @res = start_domain_validate_params(
{
%$frontend_params, domain => '123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.com'
}
);
is( scalar @res, 0 )
or diag( encode_json @res );
};
subtest '254 characters long domain with trailing dot' => sub {
my @res = start_domain_validate_params(
{
%$frontend_params, domain => '123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.com.'
}
);
is( scalar @res, 0 )
or diag( encode_json @res );
};
subtest '254 characters long domain without trailing dot' => sub {
my @res = start_domain_validate_params(
{
%$frontend_params, domain => '123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.club'
}
);
cmp_ok( scalar @res, '>', 0 )
or diag( encode_json @res );
};
subtest '63 characters long domain label' => sub {
my @res = start_domain_validate_params(
{
%$frontend_params, domain => '012345678901234567890123456789012345678901234567890123456789-63.fr'
}
);
is( scalar @res, 0 )
or diag( encode_json @res );
};
subtest '64 characters long domain label' => sub {
my @res = start_domain_validate_params(
{
%$frontend_params, domain => '012345678901234567890123456789012345678901234567890123456789--64.fr'
}
);
cmp_ok( scalar @res, '>', 0 )
or diag( encode_json @res );
};
#TEST NS
$frontend_params->{domain} = 'afnic.fr';
$frontend_params->{nameservers}->[0]->{ip} = '1.2.3.4';
# domain present?
$frontend_params->{nameservers}->[0]->{ns} = 'afnic.fr';
is( scalar start_domain_validate_params( $frontend_params ), 0, 'domain present' );
# idn
$frontend_params->{nameservers}->[0]->{ns} = 'é';
is( scalar start_domain_validate_params( $frontend_params ), 0, encode_utf8( 'idn domain=[é]' ) )
or diag( encode_json start_domain_validate_params( $frontend_params ) );
# idn
$frontend_params->{nameservers}->[0]->{ns} = 'éé';
is( scalar start_domain_validate_params( $frontend_params ), 0, encode_utf8( 'idn domain=[éé]' ) )
or diag( encode_json start_domain_validate_params( $frontend_params ) );
# 253 characters long domain without dot
$frontend_params->{nameservers}->[0]->{ns} =
'123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.com';
is(
scalar start_domain_validate_params( $frontend_params ), 0,
encode_utf8( '253 characters long domain without dot' )
) or diag( encode_json start_domain_validate_params( $frontend_params ) );
# 254 characters long domain with trailing dot
$frontend_params->{nameservers}->[0]->{ns} =
'123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.com.';
is(
scalar start_domain_validate_params( $frontend_params ), 0,
encode_utf8( '254 characters long domain with trailing dot' )
) or diag( encode_json start_domain_validate_params( $frontend_params ) );
# 254 characters long domain without trailing
$frontend_params->{nameservers}->[0]->{ns} =
'123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.club';
cmp_ok(
scalar start_domain_validate_params( $frontend_params ), '>', 0,
encode_utf8( '254 characters long domain without trailing dot' )
) or diag( encode_jsonstart_domain_validate_params( $frontend_params ) );
# 63 characters long domain label
$frontend_params->{nameservers}->[0]->{ns} = '012345678901234567890123456789012345678901234567890123456789-63.fr';
is(
scalar start_domain_validate_params( $frontend_params ), 0,
encode_utf8( '63 characters long domain label' )
) or diag( encode_json start_domain_validate_params( $frontend_params ) );
# 64 characters long domain label
$frontend_params->{nameservers}->[0]->{ns} = '012345678901234567890123456789012345678901234567890123456789-64-.fr';
cmp_ok( scalar start_domain_validate_params( $frontend_params ), '>', 0,
encode_utf8( '64 characters long domain label' ) )
or diag(encode_json start_domain_validate_params( $frontend_params ) );
# DELEGATED TEST
delete( $frontend_params->{nameservers} );
$frontend_params->{domain} = 'afnic.fr';
is( scalar start_domain_validate_params( $frontend_params ), 0, encode_utf8( 'delegated domain exists' ) )
or diag( encode_json start_domain_validate_params( $frontend_params ) );
# IP ADDRESS FORMAT
$frontend_params->{domain} = 'afnic.fr';
$frontend_params->{nameservers}->[0]->{ns} = 'ns1.nic.fr';
$frontend_params->{nameservers}->[0]->{ip} = '1.2.3.4';
is( scalar start_domain_validate_params( $frontend_params ), 0, encode_utf8( 'Valid IPV4' ) )
or diag( encode_json start_domain_validate_params( $frontend_params ) );
$frontend_params->{nameservers}->[0]->{ip} = '1.2.3.4444';
cmp_ok( scalar start_domain_validate_params( $frontend_params ), '>', 0, encode_utf8( 'Invalid IPV4' ) )
or diag( encode_json start_domain_validate_params( $frontend_params ) );
$frontend_params->{nameservers}->[0]->{ip} = 'fe80::6ef0:49ff:fe7b:e4bb';
is( scalar start_domain_validate_params( $frontend_params ), 0, encode_utf8( 'Valid IPV6' ) )
or diag( encode_json start_domain_validate_params( $frontend_params ) );
$frontend_params->{nameservers}->[0]->{ip} = 'fe80::6ef0:49ff:fe7b:e4bbffffff';
cmp_ok( start_domain_validate_params( $frontend_params ), '>', 0, encode_utf8( 'Invalid IPV6' ) )
or diag( encode_json start_domain_validate_params( $frontend_params ) );
# DS
$frontend_params->{domain} = 'afnic.fr';
$frontend_params->{nameservers}->[0]->{ns} = 'ns1.nic.fr';
$frontend_params->{nameservers}->[0]->{ip} = '1.2.3.4';
$frontend_params->{ds_info}->[0]->{algorithm} = 1;
$frontend_params->{ds_info}->[0]->{digest} = '0123456789012345678901234567890123456789';
$frontend_params->{ds_info}->[0]->{digtype} = 1;
$frontend_params->{ds_info}->[0]->{keytag} = 5000;
is( scalar start_domain_validate_params( $frontend_params ), 0, encode_utf8( 'Valid Algorithm Type [numeric format]' ) )
or diag( encode_json start_domain_validate_params( $frontend_params ) );
$frontend_params->{ds_info}->[0]->{algorithm} = 'a';
$frontend_params->{ds_info}->[0]->{digest} = '0123456789012345678901234567890123456789';
is( scalar start_domain_validate_params( $frontend_params ), 1, encode_utf8( 'Invalid Algorithm Type' ) )
or diag( encode_json start_domain_validate_params( $frontend_params ) );
$frontend_params->{ds_info}->[0]->{algorithm} = 1;
$frontend_params->{ds_info}->[0]->{digest} = '01234567890123456789012345678901234567890';
is( scalar start_domain_validate_params( $frontend_params ), 1, encode_utf8( 'Invalid digest length' ) )
or diag( encode_json start_domain_validate_params( $frontend_params ) );
$frontend_params->{ds_info}->[0]->{digest} = 'Z123456789012345678901234567890123456789';
is( scalar start_domain_validate_params( $frontend_params ), 1, encode_utf8( 'Invalid digest format' ) )
or diag( encode_json start_domain_validate_params( $frontend_params ) );
$frontend_params->{ds_info}->[0]->{digest} = '0123456789012345678901234567890123456789';
$frontend_params->{ds_info}->[0]->{digtype} = -1;
is( scalar start_domain_validate_params( $frontend_params ), 1, encode_utf8( 'Invalid digest type' ) )
or diag( encode_json start_domain_validate_params( $frontend_params ) );
$frontend_params->{ds_info}->[0]->{digtype} = 1;
$frontend_params->{ds_info}->[0]->{keytag} = 'not a int';
is( scalar start_domain_validate_params( $frontend_params ), 1, encode_utf8( 'Invalid keytag' ) )
or diag( encode_json start_domain_validate_params( $frontend_params ) );
$frontend_params->{ds_info}->[0]->{keytag} = 5000;
{
local $frontend_params->{language} = "zz";
my @res = start_domain_validate_params( $frontend_params );
is( scalar @res, 1, 'Invalid language, "zz" unknown' ) or diag( explain \@res );
}
{
local $frontend_params->{language} = "fr-FR";
my @res = start_domain_validate_params( $frontend_params );
is( scalar @res, 1, 'Invalid language tag syntax' ) or diag( explain \@res );
}
{
local $frontend_params->{language} = "nb_NO";
my @res = start_domain_validate_params( $frontend_params );
is( scalar @res, 1, 'Invalid language tag syntax' ) or diag( explain \@res );
}
};

View File

@@ -0,0 +1,81 @@
#!/usr/bin/env perl
use v5.16;
use warnings;
use utf8;
use Test::More;
use Locale::Messages qw( LC_ALL );
use POSIX qw( setlocale );
BEGIN {
# Set correct locale for translation in case not set in calling environment
delete $ENV{"LANG"};
delete $ENV{"LANGUAGE"};
delete $ENV{"LC_CTYPE"};
delete $ENV{"LC_MESSAGES"};
setlocale( LC_ALL, "C.UTF-8" );
use_ok( 'Zonemaster::Backend::Translator' )
or BAIL_OUT "Cannot continue without translator module";
}
my $translator = Zonemaster::Backend::Translator->instance();
isa_ok $translator, 'Zonemaster::Backend::Translator', "Zonemaster::Backend::Translator->instance()"
or BAIL_OUT "Cannot continue without a translator instance";
subtest 'Basic tests' => sub {
isa_ok 'Zonemaster::Backend::Translator', 'Zonemaster::Engine::Translator';
my $locale = 'fr_FR.UTF-8';
ok( $translator->locale( $locale ), "Setting locale to '$locale' works" );
};
subtest 'Testing some translations' => sub {
my $message = {
module => 'System',
testcase => 'Unspecified',
timestamp => '0.000778913497924805',
level => 'INFO',
tag => 'GLOBAL_VERSION',
args => { version => 'v5.0.0' }
};
my $translation = $translator->translate_tag( $message );
like $translation, qr/\AUtilisation de la version .* du moteur Zonemaster\.\Z/, 'Translating a GLOBAL_VERSION message tag works';
};
subtest 'Test a message translation from Engine with non-ASCII strings' => sub {
my $message = {
module => 'Basic',
testcase => 'Basic02',
timestamp => '4.085114956678410350',
level => 'ERROR',
tag => 'B02_NS_BROKEN',
args => { ns => 'ns1.example' }
};
my $translation = $translator->translate_tag( $message );
like $translation, qr/\ARéponse cassée du serveur de noms /, 'Translating a B02_NS_BROKEN message works';
like $translation, qr/cass\x{e9}e/, 'Translation is a string of Unicode codepoints, not bytes';
};
subtest 'Test a Backend-specific translation' => sub {
my $message = {
module => 'Backend',
testcase => '',
timestamp => '59',
level => 'CRITICAL',
tag => 'TEST_DIED',
args => {}
};
my $translation = $translator->translate_tag( $message );
like $translation, qr/\AUne erreur est survenue /, 'Translating a backend-specific TEST_DIED message tag works';
};
subtest 'Test a test case translation with non-ASCII strings' => sub {
my $translation = $translator->test_case_description( 'Consistency01' );
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';
};
done_testing;

View File

@@ -0,0 +1,215 @@
#!perl -T
use strict;
use warnings;
use utf8;
use Test::More tests => 2;
use Test::NoWarnings;
use Test::Differences;
use Scalar::Util qw( tainted );
use JSON::Validator::Schema::Draft7;
# Get a tainted copy of a string
sub taint {
my ( $string ) = @_;
if ( !tainted $0 ) {
BAIL_OUT( 'We need $0 to be tainted' );
}
return substr $string . $0, length $0;
}
sub compile_schema {
my $jv = JSON::Validator::Schema::Draft7->new->coerce('booleans,numbers,strings')->data(@_);
$jv->formats(Zonemaster::Backend::Validator::formats( undef ));
return $jv;
}
subtest 'Everything but NoWarnings' => sub {
use_ok( 'Zonemaster::Backend::Validator', ':untaint' );
subtest 'ds_info' => sub {
my $v = compile_schema( Zonemaster::Backend::Validator->new->ds_info );
my $ds_info_40 = { digest => '0' x 40, algorithm => 0, digtype => 0, keytag => 0 };
my $ds_info_64 = { digest => '0' x 64, algorithm => 0, digtype => 0, keytag => 0 };
eq_or_diff [ $v->validate( $ds_info_40 ) ], [], 'accept ds_info with 40-digit hash';
eq_or_diff [ $v->validate( $ds_info_64 ) ], [], 'accept ds_info with 64-digit hash';
};
subtest 'ip_address' => sub {
my $v = compile_schema( Zonemaster::Backend::Validator->new->ip_address );
eq_or_diff [ $v->validate( '192.168.0.2' ) ], [], 'accept: 192.168.0.2';
eq_or_diff [ $v->validate( '2001:db8::1' ) ], [], 'accept: 2001:db8::1';
};
subtest 'untaint_abs_path' => sub {
is scalar untaint_abs_path( '/var/db/zonemaster.sqlite' ), '/var/db/zonemaster.sqlite', 'accept: /var/db/zonemaster.sqlite';
is scalar untaint_abs_path( 'zonemaster.sqlite' ), undef, 'reject: zonemaster.sqlite';
is scalar untaint_abs_path( './zonemaster.sqlite' ), undef, 'reject: ./zonemaster.sqlite';
ok !tainted( untaint_abs_path( taint( 'localhost' ) ) ), 'launder taint';
};
subtest 'untaint_engine_type' => sub {
is scalar untaint_engine_type( 'MySQL' ), 'MySQL', 'accept: MySQL';
is scalar untaint_engine_type( 'mysql' ), 'mysql', 'accept: mysql';
is scalar untaint_engine_type( 'PostgreSQL' ), 'PostgreSQL', 'accept: PostgreSQL';
is scalar untaint_engine_type( 'postgresql' ), 'postgresql', 'accept: postgresql';
is scalar untaint_engine_type( 'SQLite' ), 'SQLite', 'accept: SQLite';
is scalar untaint_engine_type( 'sqlite' ), 'sqlite', 'accept: sqlite';
is scalar untaint_engine_type( 'Excel' ), undef, 'reject: Excel';
ok !tainted( untaint_engine_type( taint( 'SQLite' ) ) ), 'launder taint';
};
subtest 'untaint_ip_address' => sub {
is scalar untaint_ip_address( '192.0.2.1' ), '192.0.2.1', 'accept: 192.0.2.1';
is scalar untaint_ip_address( '192.0.2' ), undef, 'reject: 192.0.2';
is scalar untaint_ip_address( '192' ), undef, 'reject: 192';
is scalar untaint_ip_address( '192.0.2.1:3306' ), undef, 'reject: 192.0.2.1:3306';
is scalar untaint_ip_address( '2001:db8::' ), '2001:db8::', 'accept: 2001:db8::';
is scalar untaint_ip_address( '2001:db8::/32' ), undef, 'reject: 2001:db8::/32';
is scalar untaint_ip_address( '2001:db8:ffff:ffff:ffff:ffff:ffff:ffff' ), '2001:db8:ffff:ffff:ffff:ffff:ffff:ffff', 'accept: 2001:db8:ffff:ffff:ffff:ffff:ffff:ffff';
is scalar untaint_ip_address( '2001:db8:ffff:ffff:ffff:ffff:ffff' ), undef, 'reject: 2001:db8:ffff:ffff:ffff:ffff:ffff';
is scalar untaint_ip_address( '2001:db8::255.255.255.254' ), '2001:db8::255.255.255.254', 'accept: 2001:db8::255.255.255.254';
is scalar untaint_ip_address( '2001:db8::255.255.255' ), undef, 'reject: 2001:db8::255.255.255';
is scalar untaint_ip_address( '::1' ), '::1', 'accept: ::1';
is scalar untaint_ip_address( ':::1' ), undef, 'reject: :::1';
ok !tainted( untaint_ip_address( taint( '192.0.2.1' ) ) ), 'launder taint';
};
subtest 'untaint_ldh_domain' => sub {
is scalar untaint_ldh_domain( 'localhost' ), 'localhost', 'accept: localhost';
is scalar untaint_ldh_domain( 'example.com' ), 'example.com', 'accept: example.com';
is scalar untaint_ldh_domain( 'example.com.' ), 'example.com.', 'accept: example.com.';
is scalar untaint_ldh_domain( '192.0.2.1' ), '192.0.2.1', 'accept: 192.0.2.1';
is scalar untaint_ldh_domain( '0/26.2.0.192.in-addr.arpa' ), '0/26.2.0.192.in-addr.arpa', 'accept: 0/26.2.0.192.in-addr.arpa';
is scalar untaint_ldh_domain( '_http._tcp.example.com' ), '_http._tcp.example.com', 'accept: _http._tcp.example.com';
is scalar untaint_ldh_domain( '192.0.2.1:3306' ), undef, 'reject: 192.0.2.1:3306';
is scalar untaint_ldh_domain( '1!26.2.0.192.in-addr.arpa' ), undef, 'reject: 1!26.2.0.192.in-addr.arpa';
is scalar untaint_ldh_domain( '$http.example.com' ), undef, 'reject: $http.example.com';
ok !tainted( untaint_ldh_domain( taint( 'localhost' ) ) ), 'launder taint';
};
subtest 'untaint_locale_tag' => sub {
is scalar untaint_locale_tag( 'en_US' ), 'en_US', 'accept: en_US';
is scalar untaint_locale_tag( 'en' ), undef, 'reject: en';
is scalar untaint_locale_tag( 'English' ), undef, 'reject: English';
ok !tainted( untaint_locale_tag( taint( 'en_US' ) ) ), 'launder taint';
};
subtest 'untaint_mariadb_database' => sub {
is scalar untaint_mariadb_database( 'zonemaster' ), 'zonemaster', 'accept: zonemaster';
is scalar untaint_mariadb_database( 'ZONEMASTER' ), 'ZONEMASTER', 'accept: ZONEMASTER';
is scalar untaint_mariadb_database( 'dollar$' ), 'dollar$', 'accept: dollar$';
is scalar untaint_mariadb_database( '$dollar' ), '$dollar', 'accept: $dollar';
is scalar untaint_mariadb_database( '0zonemaster' ), '0zonemaster', 'accept: 0zonemaster';
is scalar untaint_mariadb_database( 'zm_backend' ), 'zm_backend', 'accept: zm_backend';
is scalar untaint_mariadb_database( 'zm backend' ), undef, 'reject: zm backend';
is scalar untaint_mariadb_database( 'zm-backend' ), undef, 'reject: zm-backend';
is scalar untaint_mariadb_database( '' ), undef, 'reject empty string';
is scalar untaint_mariadb_database( 'zönemästër' ), undef, 'reject: zönemästër';
is scalar untaint_mariadb_database( 'a' x 65 ), undef, 'reject 65 characters';
is scalar untaint_mariadb_database( 'a' x 64 ), 'a' x 64, 'accept 64 characters';
ok !tainted( untaint_mariadb_database( taint( 'zonemaster' ) ) ), 'launder taint';
};
subtest 'untaint_mariadb_user' => sub {
is scalar untaint_mariadb_user( 'zonemaster' ), 'zonemaster', 'accept: zonemaster';
is scalar untaint_mariadb_user( 'ZONEMASTER' ), 'ZONEMASTER', 'accept: ZONEMASTER';
is scalar untaint_mariadb_user( '$dollar' ), '$dollar', 'accept: $dollar';
is scalar untaint_mariadb_user( '0zonemaster' ), '0zonemaster', 'accept: 0zonemaster';
is scalar untaint_mariadb_user( 'zm_backend' ), 'zm_backend', 'accept: zm_backend';
is scalar untaint_mariadb_user( 'zm backend' ), undef, 'reject: zm backend';
is scalar untaint_mariadb_user( 'zm-backend' ), undef, 'reject: zm-backend';
is scalar untaint_mariadb_user( '' ), undef, 'reject empty string';
is scalar untaint_mariadb_user( 'zönemästër' ), undef, 'reject: zönemästër';
is scalar untaint_mariadb_user( 'a' x 81 ), undef, 'reject 81 characters';
is scalar untaint_mariadb_user( 'a' x 80 ), 'a' x 80, 'accept 80 characters';
ok !tainted( untaint_mariadb_user( taint( 'zonemaster' ) ) ), 'launder taint';
};
subtest 'untaint_password' => sub {
is scalar untaint_password( '123456' ), '123456', 'accept: 123456';
is scalar untaint_password( 'password' ), 'password', 'accept: password';
is scalar untaint_password( '!@#$%^&*<' ), '!@#$%^&*<', 'accept: !@#$%^&*<';
is scalar untaint_password( 'Qwertyuiop' ), 'Qwertyuiop', 'accept: Qwertyuiop';
is scalar untaint_password( 'battery staple' ), 'battery staple', 'accept: battery staple';
is scalar untaint_password( '' ), '', 'accept the empty string';
is scalar untaint_password( "\t" ), undef, 'reject tab character';
is scalar untaint_password( "\x80" ), undef, 'reject del character';
is scalar untaint_password( ' x' ), undef, 'reject initial space';
is scalar untaint_password( '<x' ), undef, 'reject initial <';
is scalar untaint_password( 'åäö' ), undef, 'reject: åäö';
is scalar untaint_password( 'a' x 100 ), 'a' x 100, 'accept 100 characters';
is scalar untaint_password( 'a' x 101 ), undef, 'reject 101 characters';
ok !tainted( untaint_password( taint( '123456' ) ) ), 'launder taint';
};
subtest 'untaint_postgresql_ident' => sub {
is scalar untaint_postgresql_ident( 'zonemaster' ), 'zonemaster', 'accept: zonemaster';
is scalar untaint_postgresql_ident( 'ZONEMASTER' ), 'ZONEMASTER', 'accept: ZONEMASTER';
is scalar untaint_postgresql_ident( 'zm_backend' ), 'zm_backend', 'accept: zm_backend';
is scalar untaint_postgresql_ident( 'dollar$' ), 'dollar$', 'accept: dollar$';
is scalar untaint_postgresql_ident( '$dollar' ), undef, 'reject: $dollar';
is scalar untaint_postgresql_ident( 'zm backend' ), undef, 'reject: zm backend';
is scalar untaint_postgresql_ident( '0zonemaster' ), undef, 'reject: 0zonemaster';
is scalar untaint_postgresql_ident( 'zm-backend' ), undef, 'reject: zm-backend';
is scalar untaint_postgresql_ident( '' ), undef, 'reject empty string';
is scalar untaint_postgresql_ident( 'zönemästër' ), undef, 'reject: zönemästër';
is scalar untaint_postgresql_ident( 'a' x 64 ), undef, 'reject 64 characters';
is scalar untaint_postgresql_ident( 'a' x 63 ), 'a' x 63, 'accept 63 characters';
ok !tainted( untaint_postgresql_ident( taint( 'zonemaster' ) ) ), 'launder taint';
};
subtest 'untaint_profile_name' => sub {
is scalar untaint_profile_name( 'default' ), 'default', 'accept: default';
is scalar untaint_profile_name( '-leading-dash' ), undef, 'reject: -leading-dash';
is scalar untaint_profile_name( 'trailing-dash-' ), undef, 'reject: trailing-dash-';
is scalar untaint_profile_name( 'middle-dash' ), 'middle-dash', 'accept: middle-dash';
is scalar untaint_profile_name( '_leading_underscore' ), undef, 'reject: _leading_underscore';
is scalar untaint_profile_name( 'trailing_underscore_' ), undef, 'reject: trailing_underscore_';
is scalar untaint_profile_name( 'middle_underscore' ), 'middle_underscore', 'accept: middle_underscore';
is scalar untaint_profile_name( '0-leading-digit' ), '0-leading-digit', 'accept: 0-leading-digit';
is scalar untaint_profile_name( 'a' ), 'a', 'accept: a';
is scalar untaint_profile_name( '-' ), undef, 'reject dash';
is scalar untaint_profile_name( '_' ), undef, 'reject underscore';
is scalar untaint_profile_name( 'a' x 32 ), 'a' x 32, 'accept 32 characters';
is scalar untaint_profile_name( 'a' x 33 ), undef, 'reject 33 characters';
ok !tainted( untaint_profile_name( taint( 'default' ) ) ), 'launder taint';
};
subtest 'untaint_non_negative_int' => sub {
is scalar untaint_non_negative_int( '1' ), '1', 'accept: 1';
is scalar untaint_non_negative_int( '0' ), '0', 'accept: 0';
is scalar untaint_non_negative_int( '99999' ), '99999', 'accept: 99999';
is scalar untaint_non_negative_int( '100000' ), undef, 'reject: 100000';
is scalar untaint_non_negative_int( '0.5' ), undef, 'reject: 0.5';
is scalar untaint_non_negative_int( '-1' ), undef, 'reject: -1';
ok !tainted( untaint_non_negative_int( taint( '1' ) ) ), 'launder taint';
};
subtest 'untaint_strictly_positive_int' => sub {
is scalar untaint_strictly_positive_int( '1' ), '1', 'accept: 1';
is scalar untaint_strictly_positive_int( '99999' ), '99999', 'accept: 99999';
is scalar untaint_strictly_positive_int( '100000' ), undef, 'reject: 100000';
is scalar untaint_strictly_positive_int( '0' ), undef, 'reject: 0';
is scalar untaint_strictly_positive_int( '0.5' ), undef, 'reject: 0.5';
is scalar untaint_strictly_positive_int( '-1' ), undef, 'reject: -1';
ok !tainted( untaint_strictly_positive_int( taint( '1' ) ) ), 'launder taint';
};
subtest 'untaint_strictly_positive_millis' => sub {
is scalar untaint_strictly_positive_millis( '0.5' ), '0.5', 'accept: 0.5';
is scalar untaint_strictly_positive_millis( '0.001' ), '0.001', 'accept: 0.001';
is scalar untaint_strictly_positive_millis( '99999.999' ), '99999.999', 'accept: 99999.999';
is scalar untaint_strictly_positive_millis( '1' ), '1', 'accept: 1';
is scalar untaint_strictly_positive_millis( '99999' ), '99999', 'accept: 99999';
is scalar untaint_strictly_positive_millis( '0.0009' ), undef, 'reject: 0.0009';
is scalar untaint_strictly_positive_millis( '100000' ), undef, 'reject: 100000';
is scalar untaint_strictly_positive_millis( '0' ), undef, 'reject: 0';
is scalar untaint_strictly_positive_millis( '0.0' ), undef, 'reject: 0.0';
is scalar untaint_strictly_positive_millis( '-1' ), undef, 'reject: -1';
ok !tainted( untaint_strictly_positive_millis( taint( '0.5' ) ) ), 'launder taint';
};
};

View File

@@ -0,0 +1,44 @@
#!/bin/sh
case $1 in
cli)
shift 1
zonemaster-cli $@
;;
zmb)
shift 1
zmb $@
;;
zmtest)
shift 1
zmtest $@
;;
rpcapi)
/usr/local/bin/starman --listen=0.0.0.0:5000 --preload-app --user=zonemaster --group=zonemaster /usr/local/bin/zonemaster_backend_rpcapi.psgi
;;
testagent)
/usr/local/bin/zonemaster_backend_testagent -user=zonemaster --group=zonemaster foreground
;;
full)
exec /init
;;
*)
echo "'$1' is not a valid option.
Available options:
- cli : pass argument to zonemaster-cli then quit
- full : start both rpcapi & testagent
- rpcapi
- testagent
- zmb
- zmtest
"
;;
esac;