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:
236
Dockerfile.backend
Normal file
236
Dockerfile.backend
Normal file
@@ -0,0 +1,236 @@
|
||||
# =============================================================================
|
||||
# Zonemaster Backend — multi-stage build from source
|
||||
# Chain: LDNS → Engine → CLI → Backend
|
||||
# =============================================================================
|
||||
|
||||
# ── Stage 1: Build LDNS ──────────────────────────────────────────────────────
|
||||
FROM alpine:3.22 AS ldns-build
|
||||
|
||||
RUN apk add --no-cache \
|
||||
build-base \
|
||||
libidn2-dev \
|
||||
make \
|
||||
openssl-dev \
|
||||
perl-app-cpanminus \
|
||||
perl-dev \
|
||||
perl-devel-checklib \
|
||||
perl-extutils-depends \
|
||||
perl-extutils-pkgconfig \
|
||||
perl-lwp-protocol-https \
|
||||
perl-mime-base32 \
|
||||
perl-module-install \
|
||||
perl-test-differences \
|
||||
perl-test-fatal \
|
||||
perl-test-nowarnings \
|
||||
&& cpanm --notest --no-wget --from=https://cpan.metacpan.org/ \
|
||||
Module::Install::XSUtil
|
||||
|
||||
COPY zonemaster-ldns/ /src/ldns/
|
||||
RUN cpanm --notest --no-wget /src/ldns/
|
||||
|
||||
# ── Stage 2: LDNS runtime base ────────────────────────────────────────────────
|
||||
FROM alpine:3.22 AS ldns
|
||||
|
||||
COPY --from=ldns-build \
|
||||
/usr/local/lib/perl5/site_perl/auto/Zonemaster \
|
||||
/usr/local/lib/perl5/site_perl/auto/Zonemaster
|
||||
COPY --from=ldns-build \
|
||||
/usr/local/lib/perl5/site_perl/Zonemaster \
|
||||
/usr/local/lib/perl5/site_perl/Zonemaster
|
||||
|
||||
RUN apk add --no-cache libidn2 perl
|
||||
|
||||
# ── Stage 3: Build Engine ─────────────────────────────────────────────────────
|
||||
FROM ldns AS engine-build
|
||||
|
||||
RUN apk add --no-cache \
|
||||
gcc \
|
||||
make \
|
||||
musl-dev \
|
||||
perl-dev \
|
||||
perl-app-cpanminus \
|
||||
perl-mailtools \
|
||||
perl-module-build-tiny \
|
||||
perl-class-accessor \
|
||||
perl-clone \
|
||||
perl-file-sharedir \
|
||||
perl-file-slurp \
|
||||
perl-io-socket-inet6 \
|
||||
perl-list-moreutils \
|
||||
perl-locale-msgfmt \
|
||||
perl-log-any \
|
||||
perl-lwp-protocol-https \
|
||||
perl-mail-spf \
|
||||
perl-module-install \
|
||||
perl-pod-coverage \
|
||||
perl-readonly \
|
||||
perl-sub-override \
|
||||
perl-test-differences \
|
||||
perl-test-exception \
|
||||
perl-test-fatal \
|
||||
perl-test-nowarnings \
|
||||
perl-test-pod \
|
||||
perl-text-csv \
|
||||
perl-yaml \
|
||||
perl-yaml-libyaml \
|
||||
&& cpanm --no-wget --from=https://cpan.metacpan.org/ \
|
||||
Email::Valid \
|
||||
List::Compare \
|
||||
Locale::PO \
|
||||
Locale::TextDomain \
|
||||
Module::Find \
|
||||
Net::IP::XS
|
||||
|
||||
COPY zonemaster-engine/ /src/engine/
|
||||
RUN cpanm --notest --no-wget /src/engine/
|
||||
|
||||
# ── Stage 4: Engine runtime base ─────────────────────────────────────────────
|
||||
FROM ldns AS engine
|
||||
|
||||
COPY --from=engine-build /usr/local/lib/perl5/site_perl /usr/local/lib/perl5/site_perl
|
||||
COPY --from=engine-build /usr/local/share/perl5/site_perl /usr/local/share/perl5/site_perl
|
||||
|
||||
RUN apk add --no-cache \
|
||||
musl-locales \
|
||||
perl-class-accessor \
|
||||
perl-clone \
|
||||
perl-file-sharedir \
|
||||
perl-file-slurp \
|
||||
perl-io-socket-inet6 \
|
||||
perl-list-moreutils \
|
||||
perl-locale-msgfmt \
|
||||
perl-log-any \
|
||||
perl-mail-spf \
|
||||
perl-mailtools \
|
||||
perl-module-install \
|
||||
perl-net-ip \
|
||||
perl-readonly \
|
||||
perl-text-csv \
|
||||
perl-try-tiny \
|
||||
perl-yaml-libyaml
|
||||
|
||||
# ── Stage 5: Build CLI ────────────────────────────────────────────────────────
|
||||
FROM engine AS cli-build
|
||||
|
||||
RUN apk add --no-cache \
|
||||
make \
|
||||
perl-app-cpanminus \
|
||||
perl-json-xs \
|
||||
perl-lwp-protocol-https \
|
||||
perl-mojolicious \
|
||||
perl-test-deep \
|
||||
perl-test-differences \
|
||||
perl-try-tiny \
|
||||
&& cpanm --notest --no-wget --from=https://cpan.metacpan.org/ \
|
||||
JSON::Validator
|
||||
|
||||
COPY zonemaster-cli/ /src/cli/
|
||||
RUN cpanm --notest --no-wget /src/cli/
|
||||
|
||||
# ── Stage 6: CLI runtime base ─────────────────────────────────────────────────
|
||||
FROM engine AS cli
|
||||
|
||||
RUN apk add --no-cache perl-json-xs perl-try-tiny
|
||||
|
||||
COPY --from=cli-build /usr/local/bin/zonemaster-cli /usr/local/bin/zonemaster-cli
|
||||
COPY --from=cli-build /usr/local/lib/perl5/site_perl /usr/local/lib/perl5/site_perl
|
||||
COPY --from=cli-build /usr/local/share/perl5/site_perl /usr/local/share/perl5/site_perl
|
||||
|
||||
# ── Stage 7: Build Backend ────────────────────────────────────────────────────
|
||||
FROM cli AS backend-build
|
||||
|
||||
RUN apk add --no-cache \
|
||||
make \
|
||||
curl \
|
||||
gcc \
|
||||
perl-dev \
|
||||
musl-dev \
|
||||
perl-app-cpanminus \
|
||||
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 \
|
||||
&& cpanm --notest --no-wget --from=https://cpan.metacpan.org/ \
|
||||
Net::Statsd
|
||||
|
||||
COPY zonemaster-backend/ /src/backend/
|
||||
RUN cpanm --notest --no-wget /src/backend/
|
||||
|
||||
# ── Stage 8: Backend runtime (final image) ────────────────────────────────────
|
||||
FROM cli AS backend
|
||||
|
||||
RUN apk add --no-cache \
|
||||
jq \
|
||||
perl-config-inifiles \
|
||||
perl-mojolicious \
|
||||
perl-moose \
|
||||
perl-dbi \
|
||||
perl-dbd-sqlite \
|
||||
perl-plack \
|
||||
perl-parallel-forkmanager
|
||||
|
||||
COPY --from=backend-build /usr/local/share/perl5 /usr/local/share/perl5
|
||||
COPY --from=backend-build /usr/local/bin/ /usr/local/bin/
|
||||
COPY --from=backend-build /usr/lib/perl5 /usr/lib/perl5
|
||||
|
||||
RUN addgroup -S zonemaster \
|
||||
&& adduser -S zonemaster -G zonemaster
|
||||
|
||||
RUN SHARE=$(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 "$SHARE/backend_config.ini" /etc/zonemaster/
|
||||
|
||||
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 root
|
||||
|
||||
# Install s6-overlay for process supervision (rpcapi + testagent)
|
||||
ARG S6_OVERLAY_VERSION=3.2.1.0
|
||||
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
|
||||
|
||||
COPY zonemaster-backend/zonemaster_launch /usr/local/bin/zonemaster_launch
|
||||
RUN chmod +x /usr/local/bin/zonemaster_launch
|
||||
|
||||
# Register rpcapi service (JSON-RPC API on :5000)
|
||||
RUN mkdir -p /etc/s6-overlay/s6-rc.d/rpcapi \
|
||||
&& echo "longrun" > /etc/s6-overlay/s6-rc.d/rpcapi/type \
|
||||
&& printf '#!/command/with-contenv sh\nexec zonemaster_launch rpcapi\n' \
|
||||
> /etc/s6-overlay/s6-rc.d/rpcapi/run \
|
||||
&& chmod +x /etc/s6-overlay/s6-rc.d/rpcapi/run \
|
||||
&& touch /etc/s6-overlay/s6-rc.d/user/contents.d/rpcapi
|
||||
|
||||
# Register testagent service (background test runner)
|
||||
RUN mkdir -p /etc/s6-overlay/s6-rc.d/testagent \
|
||||
&& echo "longrun" > /etc/s6-overlay/s6-rc.d/testagent/type \
|
||||
&& printf '#!/command/with-contenv sh\nexec zonemaster_launch testagent\n' \
|
||||
> /etc/s6-overlay/s6-rc.d/testagent/run \
|
||||
&& chmod +x /etc/s6-overlay/s6-rc.d/testagent/run \
|
||||
&& touch /etc/s6-overlay/s6-rc.d/user/contents.d/testagent
|
||||
|
||||
EXPOSE 5000
|
||||
|
||||
ENTRYPOINT ["/init"]
|
||||
23
Dockerfile.gui
Normal file
23
Dockerfile.gui
Normal file
@@ -0,0 +1,23 @@
|
||||
# ── Stage 1: Build Astro static site ─────────────────────────────────────────
|
||||
FROM node:22-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY zonemaster-gui/package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY zonemaster-gui/ ./
|
||||
|
||||
# Build as static production site with Spanish as default, API proxied via /api
|
||||
ENV NODE_ENV=production
|
||||
ENV PUBLIC_API_URL=/api
|
||||
ENV PUBLIC_BASE_URL=/
|
||||
RUN npm run build
|
||||
|
||||
# ── Stage 2: Serve with nginx ─────────────────────────────────────────────────
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY --from=builder /app/public/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
23
docker-compose.yml
Normal file
23
docker-compose.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.backend
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- zonemaster-data:/var/lib/zonemaster
|
||||
- zonemaster-logs:/var/log/zonemaster
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.gui
|
||||
ports:
|
||||
- "5353:80"
|
||||
depends_on:
|
||||
- backend
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
zonemaster-data:
|
||||
zonemaster-logs:
|
||||
33
nginx.conf
Normal file
33
nginx.conf
Normal file
@@ -0,0 +1,33 @@
|
||||
server {
|
||||
listen 80;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Redirect root to Spanish
|
||||
location = / {
|
||||
return 301 /es/;
|
||||
}
|
||||
|
||||
# Proxy API calls to the backend service
|
||||
location /api/ {
|
||||
proxy_pass http://backend:5000/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
# Rewrite /{lang}/result/{id} to /{lang}/result/id/index.html
|
||||
location ~ ^/([^/]+)/result/[^/]+/?$ {
|
||||
set $lang $1;
|
||||
try_files /$lang/result/id/index.html =404;
|
||||
}
|
||||
|
||||
# Rewrite /result/{id} to /es/result/id/index.html
|
||||
location ~ ^/result/[^/]+/?$ {
|
||||
try_files /es/result/id/index.html =404;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
}
|
||||
15
zonemaster-backend/.github/pull_request_template.md
vendored
Normal file
15
zonemaster-backend/.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
## Purpose
|
||||
|
||||
This PR...
|
||||
|
||||
## Context
|
||||
|
||||
(e.g. Fixes #9999, Follow-up to #9999, etc.)
|
||||
|
||||
## Changes
|
||||
|
||||
...
|
||||
|
||||
## How to test this PR
|
||||
|
||||
...
|
||||
138
zonemaster-backend/.github/workflows/ci.yml
vendored
Normal file
138
zonemaster-backend/.github/workflows/ci.yml
vendored
Normal 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
25
zonemaster-backend/.gitignore
vendored
Normal 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
|
||||
30
zonemaster-backend/CONTRIBUTING.md
Normal file
30
zonemaster-backend/CONTRIBUTING.md
Normal 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
758
zonemaster-backend/Changes
Normal 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 Backend’s 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 Translator’s 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
|
||||
108
zonemaster-backend/Dockerfile
Normal file
108
zonemaster-backend/Dockerfile
Normal 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"]
|
||||
42
zonemaster-backend/LICENSE
Normal file
42
zonemaster-backend/LICENSE
Normal 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/>.
|
||||
93
zonemaster-backend/MANIFEST
Normal file
93
zonemaster-backend/MANIFEST
Normal 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
|
||||
84
zonemaster-backend/MANIFEST.SKIP
Normal file
84
zonemaster-backend/MANIFEST.SKIP
Normal 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
|
||||
97
zonemaster-backend/Makefile.PL
Normal file
97
zonemaster-backend/Makefile.PL
Normal 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;
|
||||
68
zonemaster-backend/README.md
Normal file
68
zonemaster-backend/README.md
Normal 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
|
||||
99
zonemaster-backend/docs/Architecture.md
Normal file
99
zonemaster-backend/docs/Architecture.md
Normal 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
|
||||
28
zonemaster-backend/docs/TypographicConventions.md
Normal file
28
zonemaster-backend/docs/TypographicConventions.md
Normal 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
|
||||
48
zonemaster-backend/docs/files-description.md
Normal file
48
zonemaster-backend/docs/files-description.md
Normal 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.
|
||||
@@ -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)
|
||||
|
||||
```
|
||||
|
||||
24
zonemaster-backend/lib/Zonemaster/Backend.pm
Normal file
24
zonemaster-backend/lib/Zonemaster/Backend.pm
Normal 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;
|
||||
928
zonemaster-backend/lib/Zonemaster/Backend/Config.pm
Normal file
928
zonemaster-backend/lib/Zonemaster/Backend/Config.pm
Normal 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;
|
||||
155
zonemaster-backend/lib/Zonemaster/Backend/Config/DCPlugin.pm
Normal file
155
zonemaster-backend/lib/Zonemaster/Backend/Config/DCPlugin.pm
Normal 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;
|
||||
1056
zonemaster-backend/lib/Zonemaster/Backend/DB.pm
Normal file
1056
zonemaster-backend/lib/Zonemaster/Backend/DB.pm
Normal file
File diff suppressed because it is too large
Load Diff
344
zonemaster-backend/lib/Zonemaster/Backend/DB/MySQL.pm
Normal file
344
zonemaster-backend/lib/Zonemaster/Backend/DB/MySQL.pm
Normal 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;
|
||||
315
zonemaster-backend/lib/Zonemaster/Backend/DB/PostgreSQL.pm
Normal file
315
zonemaster-backend/lib/Zonemaster/Backend/DB/PostgreSQL.pm
Normal 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;
|
||||
301
zonemaster-backend/lib/Zonemaster/Backend/DB/SQLite.pm
Normal file
301
zonemaster-backend/lib/Zonemaster/Backend/DB/SQLite.pm
Normal 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;
|
||||
162
zonemaster-backend/lib/Zonemaster/Backend/Errors.pm
Normal file
162
zonemaster-backend/lib/Zonemaster/Backend/Errors.pm
Normal 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;
|
||||
127
zonemaster-backend/lib/Zonemaster/Backend/Log.pm
Normal file
127
zonemaster-backend/lib/Zonemaster/Backend/Log.pm
Normal 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;
|
||||
60
zonemaster-backend/lib/Zonemaster/Backend/Metrics.pm
Normal file
60
zonemaster-backend/lib/Zonemaster/Backend/Metrics.pm
Normal 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(@_);
|
||||
}
|
||||
}
|
||||
916
zonemaster-backend/lib/Zonemaster/Backend/RPCAPI.pm
Normal file
916
zonemaster-backend/lib/Zonemaster/Backend/RPCAPI.pm
Normal file
@@ -0,0 +1,916 @@
|
||||
package Zonemaster::Backend::RPCAPI;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use 5.14.2;
|
||||
|
||||
# Public Modules
|
||||
use DBI qw(:utils);
|
||||
use Digest::MD5 qw(md5_hex);
|
||||
use File::Slurp qw(append_file);
|
||||
use HTML::Entities;
|
||||
use JSON::PP;
|
||||
use JSON::Validator::Joi;
|
||||
use Log::Any qw($log);
|
||||
use Mojo::JSON::Pointer;
|
||||
use Scalar::Util qw(blessed);
|
||||
use JSON::Validator::Schema::Draft7;
|
||||
use Locale::TextDomain qw[Zonemaster-Backend];
|
||||
use Locale::Messages qw[LC_MESSAGES LC_ALL];
|
||||
use POSIX qw (setlocale);
|
||||
use Encode;
|
||||
|
||||
# Zonemaster Modules
|
||||
use Zonemaster::Engine;
|
||||
use Zonemaster::Engine::Normalization qw( normalize_name trim_space );
|
||||
use Zonemaster::Engine::Profile;
|
||||
use Zonemaster::Engine::Recursor;
|
||||
use Zonemaster::Backend;
|
||||
use Zonemaster::Backend::Config;
|
||||
use Zonemaster::Backend::Translator;
|
||||
use Zonemaster::Backend::Validator;
|
||||
use Zonemaster::Backend::Errors;
|
||||
|
||||
my $zm_validator = Zonemaster::Backend::Validator->new;
|
||||
our %json_schemas;
|
||||
my $recursor = Zonemaster::Engine::Recursor->new;
|
||||
|
||||
sub joi {
|
||||
return JSON::Validator::Joi->new;
|
||||
}
|
||||
|
||||
sub new {
|
||||
my ( $type, $params ) = @_;
|
||||
|
||||
my $self = {};
|
||||
bless( $self, $type );
|
||||
|
||||
if ( ! $params || ! $params->{config} ) {
|
||||
handle_exception("Missing 'config' parameter");
|
||||
}
|
||||
|
||||
$self->{config} = $params->{config};
|
||||
|
||||
my $dbtype;
|
||||
if ( $params->{dbtype} ) {
|
||||
$dbtype = $self->{config}->check_db($params->{dbtype});
|
||||
} else {
|
||||
$dbtype = $self->{config}->DB_engine;
|
||||
}
|
||||
|
||||
$self->_init_db($dbtype);
|
||||
|
||||
$self->{_profiles} = Zonemaster::Backend::Config->load_profiles( #
|
||||
$self->{config}->PUBLIC_PROFILES,
|
||||
$self->{config}->PRIVATE_PROFILES,
|
||||
);
|
||||
|
||||
return ( $self );
|
||||
}
|
||||
|
||||
sub _init_db {
|
||||
my ( $self, $dbtype ) = @_;
|
||||
|
||||
eval {
|
||||
my $dbclass = Zonemaster::Backend::DB->get_db_class( $dbtype );
|
||||
$self->{db} = $dbclass->from_config( $self->{config} );
|
||||
};
|
||||
|
||||
if ($@) {
|
||||
handle_exception("Failed to initialize the [$dbtype] database backend module: [$@]");
|
||||
}
|
||||
}
|
||||
|
||||
sub handle_exception {
|
||||
my ( $exception ) = @_;
|
||||
|
||||
if ( !$exception->isa('Zonemaster::Backend::Error') ) {
|
||||
my $reason = $exception;
|
||||
$exception = Zonemaster::Backend::Error::Internal->new( reason => $reason );
|
||||
}
|
||||
|
||||
my $log_extra = $exception->as_hash;
|
||||
delete $log_extra->{message};
|
||||
|
||||
if ( $exception->isa('Zonemaster::Backend::Error::Internal') ) {
|
||||
$log->error($exception->as_string, $log_extra);
|
||||
} else {
|
||||
$log->info($exception->as_string, $log_extra);
|
||||
}
|
||||
|
||||
die $exception->as_hash;
|
||||
}
|
||||
|
||||
$json_schemas{version_info} = joi->object->strict;
|
||||
sub version_info {
|
||||
my ( $self ) = @_;
|
||||
|
||||
my %ver;
|
||||
eval {
|
||||
$ver{zonemaster_ldns} = Zonemaster::LDNS->VERSION;
|
||||
$ver{zonemaster_engine} = Zonemaster::Engine->VERSION;
|
||||
$ver{zonemaster_backend} = Zonemaster::Backend->VERSION;
|
||||
};
|
||||
if ($@) {
|
||||
handle_exception( $@ );
|
||||
}
|
||||
|
||||
return \%ver;
|
||||
}
|
||||
|
||||
# Experimental
|
||||
$json_schemas{system_versions} = $json_schemas{version_info};
|
||||
sub system_versions {
|
||||
return version_info( @_ );
|
||||
}
|
||||
|
||||
$json_schemas{profile_names} = joi->object->strict;
|
||||
sub profile_names {
|
||||
my ( $self ) = @_;
|
||||
|
||||
my %profiles;
|
||||
eval { %profiles = $self->{config}->PUBLIC_PROFILES };
|
||||
if ( $@ ) {
|
||||
handle_exception( $@ );
|
||||
}
|
||||
|
||||
return [ keys %profiles ];
|
||||
}
|
||||
|
||||
# Experimental
|
||||
$json_schemas{conf_profiles} = $json_schemas{profile_names};
|
||||
sub conf_profiles {
|
||||
my $result = {
|
||||
profiles => profile_names( @_ )
|
||||
};
|
||||
return $result;
|
||||
}
|
||||
|
||||
# Return the list of language tags supported by get_test_results(). The tags are
|
||||
# derived from the locale tags set in the configuration file.
|
||||
$json_schemas{get_language_tags} = joi->object->strict;
|
||||
sub get_language_tags {
|
||||
my ( $self ) = @_;
|
||||
|
||||
my @lang_tags;
|
||||
eval {
|
||||
my %locales = $self->{config}->LANGUAGE_locale;
|
||||
|
||||
@lang_tags = sort keys %locales;
|
||||
};
|
||||
if ( $@ ) {
|
||||
handle_exception( $@ );
|
||||
}
|
||||
|
||||
return \@lang_tags;
|
||||
}
|
||||
|
||||
# Experimental
|
||||
$json_schemas{conf_languages} = $json_schemas{get_language_tags};
|
||||
sub conf_languages {
|
||||
my $result = {
|
||||
languages => get_language_tags( @_ )
|
||||
};
|
||||
return $result;
|
||||
}
|
||||
|
||||
$json_schemas{get_host_by_name} = {
|
||||
type => 'object',
|
||||
additionalProperties => 0,
|
||||
required => [ 'hostname' ],
|
||||
properties => {
|
||||
hostname => $zm_validator->domain_name
|
||||
}
|
||||
};
|
||||
sub get_host_by_name {
|
||||
my ( $self, $params ) = @_;
|
||||
my @adresses;
|
||||
|
||||
eval {
|
||||
my $ns_name = $params->{hostname};
|
||||
|
||||
@adresses = map { {$ns_name => $_->short} } $recursor->get_addresses_for($ns_name);
|
||||
@adresses = { $ns_name => '0.0.0.0' } if not @adresses;
|
||||
|
||||
};
|
||||
if ($@) {
|
||||
handle_exception( $@ );
|
||||
}
|
||||
|
||||
return \@adresses;
|
||||
}
|
||||
|
||||
# Experimental
|
||||
$json_schemas{lookup_address_records} = $json_schemas{get_host_by_name};
|
||||
sub lookup_address_records {
|
||||
my $result = {
|
||||
address_records => get_host_by_name( @_ )
|
||||
};
|
||||
return $result;
|
||||
}
|
||||
|
||||
$json_schemas{get_data_from_parent_zone} = {
|
||||
type => 'object',
|
||||
additionalProperties => 0,
|
||||
required => [ 'domain' ],
|
||||
properties => {
|
||||
domain => $zm_validator->domain_name,
|
||||
language => $zm_validator->language_tag,
|
||||
}
|
||||
};
|
||||
sub get_data_from_parent_zone {
|
||||
my ( $self, $params ) = @_;
|
||||
|
||||
my $result = eval {
|
||||
my %result;
|
||||
my $domain = $params->{domain};
|
||||
my ( $_errors, $normalized_domain ) = normalize_name( trim_space ( $domain ) );
|
||||
|
||||
my @ns_list;
|
||||
my @ns_names;
|
||||
|
||||
my $zone = Zonemaster::Engine->zone( $normalized_domain );
|
||||
push @ns_list, { ns => $_->name->string, ip => $_->address->short} for @{$zone->glue};
|
||||
|
||||
my @ds_list;
|
||||
|
||||
$zone = Zonemaster::Engine->zone($normalized_domain);
|
||||
my $ds_p = $zone->parent->query_one( $zone->name, 'DS', { dnssec => 1, cd => 1, recurse => 1 } );
|
||||
if ($ds_p) {
|
||||
my @ds = $ds_p->get_records( 'DS', 'answer' );
|
||||
|
||||
foreach my $ds ( @ds ) {
|
||||
next unless $ds->type eq 'DS';
|
||||
push(@ds_list, { keytag => $ds->keytag, algorithm => $ds->algorithm, digtype => $ds->digtype, digest => $ds->hexdigest });
|
||||
}
|
||||
}
|
||||
|
||||
$result{ns_list} = \@ns_list;
|
||||
$result{ds_list} = \@ds_list;
|
||||
return \%result;
|
||||
};
|
||||
if ($@) {
|
||||
handle_exception( $@ );
|
||||
}
|
||||
elsif ($result) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
# Experimental
|
||||
$json_schemas{lookup_delegation_data} = $json_schemas{get_data_from_parent_zone};
|
||||
sub lookup_delegation_data {
|
||||
return get_data_from_parent_zone( @_ );
|
||||
}
|
||||
|
||||
$json_schemas{start_domain_test} = {
|
||||
type => 'object',
|
||||
additionalProperties => 0,
|
||||
required => [ 'domain' ],
|
||||
properties => {
|
||||
domain => $zm_validator->domain_name,
|
||||
ipv4 => joi->boolean->compile,
|
||||
ipv6 => joi->boolean->compile,
|
||||
nameservers => {
|
||||
type => 'array',
|
||||
items => $zm_validator->nameserver
|
||||
},
|
||||
ds_info => {
|
||||
type => 'array',
|
||||
items => $zm_validator->ds_info
|
||||
},
|
||||
profile => $zm_validator->profile_name,
|
||||
client_id => $zm_validator->client_id->compile,
|
||||
client_version => $zm_validator->client_version->compile,
|
||||
config => joi->string->compile,
|
||||
priority => $zm_validator->priority->compile,
|
||||
queue => $zm_validator->queue->compile,
|
||||
language => $zm_validator->language_tag,
|
||||
}
|
||||
};
|
||||
sub start_domain_test {
|
||||
my ( $self, $params ) = @_;
|
||||
|
||||
my $result = 0;
|
||||
eval {
|
||||
$params->{profile} //= "default";
|
||||
$params->{priority} //= 10;
|
||||
$params->{queue} //= 0;
|
||||
|
||||
my $profile = $self->{_profiles}{ $params->{profile} };
|
||||
$params->{ipv4} //= $profile->get( "net.ipv4" );
|
||||
$params->{ipv6} //= $profile->get( "net.ipv6" );
|
||||
|
||||
$result = $self->{db}->create_new_test( $params->{domain}, $params, $self->{config}->ZONEMASTER_age_reuse_previous_test );
|
||||
};
|
||||
if ($@) {
|
||||
handle_exception( $@ );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
# Experimental
|
||||
$json_schemas{job_create} = $json_schemas{start_domain_test};
|
||||
sub job_create {
|
||||
my $result = {
|
||||
job_id => start_domain_test( @_ )
|
||||
};
|
||||
return $result;
|
||||
}
|
||||
|
||||
$json_schemas{test_progress} = joi->object->strict->props(
|
||||
test_id => $zm_validator->test_id->required
|
||||
);
|
||||
sub test_progress {
|
||||
my ( $self, $params ) = @_;
|
||||
|
||||
my $result = 0;
|
||||
eval {
|
||||
my $test_id = $params->{test_id};
|
||||
$result = $self->{db}->test_progress( $test_id );
|
||||
};
|
||||
if ($@) {
|
||||
handle_exception( $@ );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
# Experimental
|
||||
$json_schemas{job_status} = joi->object->strict->props(
|
||||
job_id => $zm_validator->test_id->required
|
||||
);
|
||||
sub job_status {
|
||||
my ( $self, $params ) = @_;
|
||||
|
||||
$params->{test_id} = delete $params->{job_id};
|
||||
|
||||
my $result = {
|
||||
progress => $self->test_progress( $params )
|
||||
};
|
||||
return $result;
|
||||
}
|
||||
|
||||
$json_schemas{get_test_params} = joi->object->strict->props(
|
||||
test_id => $zm_validator->test_id->required
|
||||
);
|
||||
sub get_test_params {
|
||||
my ( $self, $params ) = @_;
|
||||
|
||||
my $result;
|
||||
eval {
|
||||
my $test_id = $params->{test_id};
|
||||
|
||||
$result = $self->{db}->get_test_params( $test_id );
|
||||
};
|
||||
if ($@) {
|
||||
handle_exception( $@ );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
# Experimental
|
||||
$json_schemas{job_params} = joi->object->strict->props(
|
||||
job_id => $zm_validator->test_id->required
|
||||
);
|
||||
sub job_params {
|
||||
my ( $self, $params ) = @_;
|
||||
|
||||
$params->{test_id} = delete $params->{job_id};
|
||||
|
||||
return $self->get_test_params( $params );
|
||||
}
|
||||
|
||||
$json_schemas{get_test_results} = {
|
||||
type => 'object',
|
||||
additionalProperties => 0,
|
||||
required => [ 'id', 'language' ],
|
||||
properties => {
|
||||
id => $zm_validator->test_id->required->compile,
|
||||
language => $zm_validator->language_tag,
|
||||
}
|
||||
};
|
||||
sub get_test_results {
|
||||
my ( $self, $params ) = @_;
|
||||
|
||||
my $result;
|
||||
eval{
|
||||
|
||||
my $locale = $self->_get_locale( $params );
|
||||
|
||||
my $translator;
|
||||
$translator = Zonemaster::Backend::Translator->instance();
|
||||
|
||||
my $previous_locale = $translator->locale;
|
||||
if ( !$translator->locale( $locale ) ) {
|
||||
die "Failed to set locale: $locale";
|
||||
}
|
||||
|
||||
eval { $translator->data } if $translator; # Provoke lazy loading of translation data
|
||||
|
||||
my @zm_results;
|
||||
my %testcases;
|
||||
|
||||
my $test_info = $self->{db}->test_results( $params->{id} );
|
||||
foreach my $test_res ( @{ $test_info->{results} } ) {
|
||||
my $res;
|
||||
if ( $test_res->{module} eq 'Nameserver' ) {
|
||||
$res->{ns} = ( $test_res->{args}->{ns} ) ? ( $test_res->{args}->{ns} ) : ( 'All' );
|
||||
}
|
||||
elsif ($test_res->{module} eq 'SYSTEM'
|
||||
&& $test_res->{tag} eq 'POLICY_DISABLED'
|
||||
&& $test_res->{args}->{name} eq 'Example' )
|
||||
{
|
||||
next;
|
||||
}
|
||||
|
||||
$res->{module} = $test_res->{module};
|
||||
$res->{message} = $translator->translate_tag( $test_res ) . "\n";
|
||||
$res->{message} =~ s/,/, /isg;
|
||||
$res->{message} =~ s/;/; /isg;
|
||||
$res->{level} = $test_res->{level};
|
||||
$res->{testcase} = $test_res->{testcase} // 'UNSPECIFIED';
|
||||
$testcases{$res->{testcase}} = $translator->test_case_description($res->{testcase});
|
||||
|
||||
if ( $test_res->{module} eq 'SYSTEM' ) {
|
||||
if ( $res->{message} =~ /policy\.json/ ) {
|
||||
my ( $policy ) = ( $res->{message} =~ /\s(\/.*)$/ );
|
||||
if ( $policy ) {
|
||||
my $policy_description = 'DEFAULT POLICY';
|
||||
$policy_description = 'SOME OTHER POLICY' if ( $policy =~ /some\/other\/policy\/path/ );
|
||||
$res->{message} =~ s/$policy/$policy_description/;
|
||||
}
|
||||
else {
|
||||
$res->{message} = 'UNKNOWN POLICY FORMAT';
|
||||
}
|
||||
}
|
||||
elsif ( $res->{message} =~ /config\.json/ ) {
|
||||
my ( $config ) = ( $res->{message} =~ /\s(\/.*)$/ );
|
||||
if ( $config ) {
|
||||
my $config_description = 'DEFAULT CONFIGURATION';
|
||||
$config_description = 'SOME OTHER CONFIGURATION' if ( $config =~ /some\/other\/configuration\/path/ );
|
||||
$res->{message} =~ s/$config/$config_description/;
|
||||
}
|
||||
else {
|
||||
$res->{message} = 'UNKNOWN CONFIG FORMAT';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
push( @zm_results, $res );
|
||||
}
|
||||
|
||||
$result = $test_info;
|
||||
$result->{testcase_descriptions} = \%testcases;
|
||||
$result->{results} = \@zm_results;
|
||||
|
||||
$translator->locale( $previous_locale );
|
||||
|
||||
$result = $test_info;
|
||||
$result->{results} = \@zm_results;
|
||||
};
|
||||
if ($@) {
|
||||
handle_exception( $@ );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
# Experimental
|
||||
$json_schemas{job_results} = {
|
||||
type => 'object',
|
||||
additionalProperties => 0,
|
||||
required => [ 'job_id', 'language' ],
|
||||
properties => {
|
||||
job_id => $zm_validator->test_id->required->compile,
|
||||
language => $zm_validator->language_tag,
|
||||
}
|
||||
};
|
||||
sub job_results {
|
||||
my ( $self, $params ) = @_;
|
||||
|
||||
$params->{id} = delete $params->{job_id};
|
||||
|
||||
my $result = $self->get_test_results( $params );
|
||||
|
||||
return {
|
||||
created_at => $result->{created_at},
|
||||
job_id => $result->{hash_id},
|
||||
results => $result->{results},
|
||||
params => $result->{params},
|
||||
testcase_descriptions => $result->{testcase_descriptionsd},
|
||||
};
|
||||
}
|
||||
|
||||
$json_schemas{get_test_history} = {
|
||||
type => 'object',
|
||||
additionalProperties => 0,
|
||||
required => [ 'frontend_params' ],
|
||||
properties => {
|
||||
offset => joi->integer->min(0)->compile,
|
||||
limit => joi->integer->min(0)->compile,
|
||||
filter => joi->string->regex('^(?:all|delegated|undelegated)$')->compile,
|
||||
frontend_params => {
|
||||
type => 'object',
|
||||
additionalProperties => 0,
|
||||
required => [ 'domain' ],
|
||||
properties => {
|
||||
domain => $zm_validator->domain_name
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
sub get_test_history {
|
||||
my ( $self, $params ) = @_;
|
||||
|
||||
my $results;
|
||||
|
||||
eval {
|
||||
$params->{offset} //= 0;
|
||||
$params->{limit} //= 200;
|
||||
$params->{filter} //= "all";
|
||||
|
||||
$results = $self->{db}->get_test_history( $params );
|
||||
my @results = map { { %$_, undelegated => $_->{undelegated} ? JSON::PP::true : JSON::PP::false } } @$results;
|
||||
$results = \@results;
|
||||
|
||||
};
|
||||
if ($@) {
|
||||
handle_exception( $@ );
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
# Experimental
|
||||
$json_schemas{domain_history} = {
|
||||
type => 'object',
|
||||
additionalProperties => 0,
|
||||
required => [ 'params' ],
|
||||
properties => {
|
||||
offset => joi->integer->min(0)->compile,
|
||||
limit => joi->integer->min(0)->compile,
|
||||
filter => joi->string->regex('^(?:all|delegated|undelegated)$')->compile,
|
||||
params => {
|
||||
type => 'object',
|
||||
additionalProperties => 0,
|
||||
required => [ 'domain' ],
|
||||
properties => {
|
||||
domain => $zm_validator->domain_name
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
sub domain_history {
|
||||
my ( $self, $params ) = @_;
|
||||
|
||||
$params->{frontend_params} = delete $params->{params};
|
||||
|
||||
my $results = $self->get_test_history( $params );
|
||||
|
||||
return {
|
||||
history => [
|
||||
map {
|
||||
{
|
||||
job_id => $_->{id},
|
||||
created_at => $_->{created_at},
|
||||
overall_result => $_->{overall_result},
|
||||
undelegated => $_->{undelegated},
|
||||
}
|
||||
} @$results
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
$json_schemas{add_api_user} = joi->object->strict->props(
|
||||
username => $zm_validator->username->required,
|
||||
api_key => $zm_validator->api_key->required,
|
||||
);
|
||||
sub add_api_user {
|
||||
my ( $self, $params, undef, $remote_ip ) = @_;
|
||||
|
||||
my $result = 0;
|
||||
|
||||
eval {
|
||||
my $allow = 0;
|
||||
if ( defined $remote_ip ) {
|
||||
$allow = 1 if ( $remote_ip eq '::1' || $remote_ip eq '127.0.0.1' || $remote_ip eq '::ffff:127.0.0.1' );
|
||||
}
|
||||
else {
|
||||
$allow = 1;
|
||||
}
|
||||
|
||||
if ( $allow ) {
|
||||
$result = 1 if ( $self->{db}->add_api_user( $params->{username}, $params->{api_key} ) eq '1' );
|
||||
}
|
||||
else {
|
||||
die Zonemaster::Backend::Error::PermissionDenied->new(
|
||||
message => 'Call to "add_api_user" method not permitted from a remote IP',
|
||||
data => { remote_ip => $remote_ip }
|
||||
);
|
||||
}
|
||||
};
|
||||
if ($@) {
|
||||
handle_exception( $@ );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
# Experimental
|
||||
$json_schemas{user_create} = $json_schemas{add_api_user};
|
||||
sub user_create {
|
||||
my $result = {
|
||||
success => add_api_user( @_ )
|
||||
};
|
||||
return $result;
|
||||
}
|
||||
|
||||
$json_schemas{add_batch_job} = {
|
||||
type => 'object',
|
||||
additionalProperties => 0,
|
||||
required => [ 'username', 'api_key', 'domains' ],
|
||||
properties => {
|
||||
username => $zm_validator->username->required->compile,
|
||||
api_key => $zm_validator->api_key->required->compile,
|
||||
domains => {
|
||||
type => "array",
|
||||
additionalItems => 0,
|
||||
items => $zm_validator->domain_name,
|
||||
minItems => 1
|
||||
},
|
||||
test_params => {
|
||||
type => 'object',
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
ipv4 => joi->boolean->compile,
|
||||
ipv6 => joi->boolean->compile,
|
||||
nameservers => {
|
||||
type => 'array',
|
||||
items => $zm_validator->nameserver
|
||||
},
|
||||
ds_info => {
|
||||
type => 'array',
|
||||
items => $zm_validator->ds_info
|
||||
},
|
||||
profile => $zm_validator->profile_name,
|
||||
client_id => $zm_validator->client_id->compile,
|
||||
client_version => $zm_validator->client_version->compile,
|
||||
config => joi->string->compile,
|
||||
priority => $zm_validator->priority->compile,
|
||||
queue => $zm_validator->queue->compile,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
sub add_batch_job {
|
||||
my ( $self, $params ) = @_;
|
||||
|
||||
my $results;
|
||||
eval {
|
||||
$params->{test_params}{profile} //= "default";
|
||||
$params->{test_params}{priority} //= 5;
|
||||
$params->{test_params}{queue} //= 0;
|
||||
|
||||
my $profile = $self->{_profiles}{ $params->{test_params}{profile} };
|
||||
$params->{test_params}{ipv4} //= $profile->get( "net.ipv4" );
|
||||
$params->{test_params}{ipv6} //= $profile->get( "net.ipv6" );
|
||||
|
||||
$results = $self->{db}->add_batch_job( $params );
|
||||
};
|
||||
if ($@) {
|
||||
handle_exception( $@ );
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
# Experimental
|
||||
$json_schemas{batch_create} = {
|
||||
type => 'object',
|
||||
additionalProperties => 0,
|
||||
required => [ 'username', 'api_key', 'domains' ],
|
||||
properties => {
|
||||
username => $zm_validator->username->required->compile,
|
||||
api_key => $zm_validator->api_key->required->compile,
|
||||
domains => {
|
||||
type => "array",
|
||||
additionalItems => 0,
|
||||
items => $zm_validator->domain_name,
|
||||
minItems => 1
|
||||
},
|
||||
job_params => {
|
||||
type => 'object',
|
||||
additionalProperties => 0,
|
||||
properties => {
|
||||
ipv4 => joi->boolean->compile,
|
||||
ipv6 => joi->boolean->compile,
|
||||
nameservers => {
|
||||
type => 'array',
|
||||
items => $zm_validator->nameserver
|
||||
},
|
||||
ds_info => {
|
||||
type => 'array',
|
||||
items => $zm_validator->ds_info
|
||||
},
|
||||
profile => $zm_validator->profile_name,
|
||||
client_id => $zm_validator->client_id->compile,
|
||||
client_version => $zm_validator->client_version->compile,
|
||||
config => joi->string->compile,
|
||||
priority => $zm_validator->priority->compile,
|
||||
queue => $zm_validator->queue->compile,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
sub batch_create {
|
||||
my ( $self, $params ) = @_;
|
||||
|
||||
$params->{test_params} = delete $params->{job_params};
|
||||
|
||||
my $result = {
|
||||
batch_id => $self->add_batch_job( $params )
|
||||
};
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
$json_schemas{batch_status} = {
|
||||
type => 'object',
|
||||
additionalProperties => 0,
|
||||
required => [ 'batch_id' ],
|
||||
properties => {
|
||||
batch_id => $zm_validator->batch_id->required,
|
||||
list_waiting_tests => joi->boolean->compile,
|
||||
list_running_tests => joi->boolean->compile,
|
||||
list_finished_tests => joi->boolean->compile,
|
||||
}
|
||||
};
|
||||
sub batch_status {
|
||||
my ( $self, $params ) = @_;
|
||||
|
||||
my $result;
|
||||
eval {
|
||||
$result = $self->{db}->batch_status($params);
|
||||
};
|
||||
if ($@) {
|
||||
handle_exception( $@ );
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
sub _get_locale {
|
||||
my ( $self, $params ) = @_;
|
||||
my @error;
|
||||
|
||||
if ( ref $params ne 'HASH' ) {
|
||||
return undef;
|
||||
}
|
||||
|
||||
my $language = $params->{language};
|
||||
if ( !defined $language ) {
|
||||
return undef;
|
||||
}
|
||||
|
||||
my %locales = $self->{config}->LANGUAGE_locale;
|
||||
|
||||
my $locale = $locales{$language};
|
||||
if ( !defined $locale ) {
|
||||
return undef;
|
||||
}
|
||||
|
||||
return $locale . '.UTF-8';
|
||||
}
|
||||
|
||||
sub _set_error_message_locale {
|
||||
my ( $self, $params ) = @_;
|
||||
|
||||
my @error_response = ();
|
||||
my $locale = $self->_get_locale( $params );
|
||||
|
||||
if (not defined $locale or $locale eq "") {
|
||||
# Don't translate message if locale is not defined
|
||||
$locale = "C";
|
||||
}
|
||||
|
||||
# Use POSIX implementation instead of Locale::Messages wrapper
|
||||
setlocale( LC_ALL, $locale );
|
||||
return @error_response;
|
||||
}
|
||||
|
||||
my $rpc_request = joi->object->props(
|
||||
jsonrpc => joi->string->required,
|
||||
method => $zm_validator->jsonrpc_method()->required,
|
||||
id => joi->type([qw(null number string)]));
|
||||
sub jsonrpc_validate {
|
||||
my ( $self, $jsonrpc_request ) = @_;
|
||||
|
||||
my @error_rpc = $rpc_request->validate($jsonrpc_request);
|
||||
if ((ref($jsonrpc_request) eq 'HASH' && !exists $jsonrpc_request->{id}) || @error_rpc) {
|
||||
$self->_set_error_message_locale;
|
||||
return {
|
||||
jsonrpc => '2.0',
|
||||
id => undef,
|
||||
error => {
|
||||
code => '-32600',
|
||||
message => 'The JSON sent is not a valid request object.',
|
||||
data => "@error_rpc"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
my $method_schema = $json_schemas{$jsonrpc_request->{method}};
|
||||
if (blessed $method_schema) {
|
||||
$method_schema = $method_schema->compile;
|
||||
}
|
||||
|
||||
# The "params" key of the JSONRPC object is optional per the JSONRPC 2.0
|
||||
# specification, but if the method being called requires at least one
|
||||
# parameter, omitting it is an error.
|
||||
|
||||
if ( exists $method_schema->{required} and not exists $jsonrpc_request->{params} ) {
|
||||
return {
|
||||
jsonrpc => '2.0',
|
||||
id => $jsonrpc_request->{id},
|
||||
error => {
|
||||
code => '-32602',
|
||||
message => "Missing 'params' object",
|
||||
}
|
||||
};
|
||||
}
|
||||
elsif ( exists $jsonrpc_request->{params} ) {
|
||||
my @error_response = $self->validate_params($method_schema, $jsonrpc_request->{params});
|
||||
|
||||
if ( scalar @error_response ) {
|
||||
return {
|
||||
jsonrpc => '2.0',
|
||||
id => $jsonrpc_request->{id},
|
||||
error => {
|
||||
code => '-32602',
|
||||
message => decode_utf8(__ 'Invalid method parameter(s).'),
|
||||
data => \@error_response
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
sub validate_params {
|
||||
my ( $self, $method_schema, $params ) = @_;
|
||||
my @error_response = ();
|
||||
|
||||
push @error_response, $self->_set_error_message_locale( $params );
|
||||
|
||||
if (blessed $method_schema) {
|
||||
$method_schema = $method_schema->compile;
|
||||
}
|
||||
my $jv = JSON::Validator::Schema::Draft7->new->coerce('booleans,numbers,strings')->data($method_schema);
|
||||
$jv->formats(Zonemaster::Backend::Validator::formats( $self->{config} ));
|
||||
my @json_validation_error = $jv->validate( $params );
|
||||
|
||||
# Customize error message from json validation
|
||||
foreach my $err ( @json_validation_error ) {
|
||||
my $message = $err->message;
|
||||
my @details = @{$err->details};
|
||||
|
||||
# Handle 'required' errors globally so it does not get overwritten
|
||||
if ($details[1] eq 'required') {
|
||||
$message = N__ 'Missing property';
|
||||
} else {
|
||||
my @path = split '/', $err->path, -1;
|
||||
shift @path; # first item is an empty string
|
||||
my $found = 1;
|
||||
my $data = Mojo::JSON::Pointer->new($method_schema);
|
||||
|
||||
foreach my $p (@path) {
|
||||
if ( $data->contains("/properties/$p") ) {
|
||||
$data = $data->get("/properties/$p")
|
||||
} elsif ( $p =~ /^\d+$/ and $data->contains("/items") ) {
|
||||
$data = $data->get("/items")
|
||||
} else {
|
||||
$found = 0;
|
||||
last;
|
||||
}
|
||||
$data = Mojo::JSON::Pointer->new($data);
|
||||
}
|
||||
|
||||
if ($found and exists $data->data->{'x-error-message'}) {
|
||||
$message = $data->data->{'x-error-message'};
|
||||
}
|
||||
}
|
||||
|
||||
push @error_response, { path => $err->path, message => $message };
|
||||
|
||||
}
|
||||
|
||||
# Translate messages
|
||||
@error_response = map { { %$_, ( message => decode_utf8 __ $_->{message} ) } } @error_response;
|
||||
|
||||
return @error_response;
|
||||
}
|
||||
|
||||
1;
|
||||
218
zonemaster-backend/lib/Zonemaster/Backend/TestAgent.pm
Normal file
218
zonemaster-backend/lib/Zonemaster/Backend/TestAgent.pm
Normal 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;
|
||||
54
zonemaster-backend/lib/Zonemaster/Backend/Translator.pm
Normal file
54
zonemaster-backend/lib/Zonemaster/Backend/Translator.pm
Normal 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;
|
||||
554
zonemaster-backend/lib/Zonemaster/Backend/Validator.pm
Normal file
554
zonemaster-backend/lib/Zonemaster/Backend/Validator.pm
Normal 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;
|
||||
42
zonemaster-backend/script/add-batch-job.pl
Normal file
42
zonemaster-backend/script/add-batch-job.pl
Normal 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
750
zonemaster-backend/script/zmb
Executable 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 );
|
||||
87
zonemaster-backend/script/zmtest
Executable file
87
zonemaster-backend/script/zmtest
Executable 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}"
|
||||
252
zonemaster-backend/script/zonemaster_backend_rpcapi.psgi
Normal file
252
zonemaster-backend/script/zonemaster_backend_rpcapi.psgi
Normal 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;
|
||||
};
|
||||
293
zonemaster-backend/script/zonemaster_backend_testagent
Executable file
293
zonemaster-backend/script/zonemaster_backend_testagent
Executable 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
|
||||
56
zonemaster-backend/share/GNUmakefile
Normal file
56
zonemaster-backend/share/GNUmakefile
Normal 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
|
||||
14
zonemaster-backend/share/Makefile
Normal file
14
zonemaster-backend/share/Makefile
Normal 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} $@
|
||||
20
zonemaster-backend/share/backend_config.ci_mysql.ini
Normal file
20
zonemaster-backend/share/backend_config.ci_mysql.ini
Normal 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
|
||||
|
||||
20
zonemaster-backend/share/backend_config.ci_postgresql.ini
Normal file
20
zonemaster-backend/share/backend_config.ci_postgresql.ini
Normal 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
|
||||
|
||||
17
zonemaster-backend/share/backend_config.ci_sqlite.ini
Normal file
17
zonemaster-backend/share/backend_config.ci_sqlite.ini
Normal 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
|
||||
|
||||
50
zonemaster-backend/share/backend_config.ini
Executable file
50
zonemaster-backend/share/backend_config.ini
Executable 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
|
||||
3
zonemaster-backend/share/cleanup-mysql.sql
Normal file
3
zonemaster-backend/share/cleanup-mysql.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
-- Remove Zonemaster data from database
|
||||
DROP DATABASE zonemaster;
|
||||
DROP USER 'zonemaster'@'localhost';
|
||||
3
zonemaster-backend/share/cleanup-postgres.sql
Normal file
3
zonemaster-backend/share/cleanup-postgres.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
-- Remove Zonemaster data from database
|
||||
DROP DATABASE zonemaster;
|
||||
DROP USER zonemaster;
|
||||
15
zonemaster-backend/share/create_db.pl
Executable file
15
zonemaster-backend/share/create_db.pl
Executable 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();
|
||||
89
zonemaster-backend/share/da.po
Normal file
89
zonemaster-backend/share/da.po
Normal 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"
|
||||
91
zonemaster-backend/share/es.po
Normal file
91
zonemaster-backend/share/es.po
Normal 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"
|
||||
91
zonemaster-backend/share/fi.po
Normal file
91
zonemaster-backend/share/fi.po
Normal 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"
|
||||
71
zonemaster-backend/share/fr.po
Normal file
71
zonemaster-backend/share/fr.po
Normal 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 n’a 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 à s’exé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"
|
||||
7
zonemaster-backend/share/freebsd-pwd.conf
Normal file
7
zonemaster-backend/share/freebsd-pwd.conf
Normal 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
|
||||
71
zonemaster-backend/share/nb.po
Normal file
71
zonemaster-backend/share/nb.po
Normal 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"
|
||||
2
zonemaster-backend/share/patch/README.txt
Normal file
2
zonemaster-backend/share/patch/README.txt
Normal 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
|
||||
@@ -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? That’s 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]);
|
||||
|
||||
# I’ve tried to avoid hardcoding numbers but FETCH statements somehow
|
||||
# don’t 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 PostgreSQL’s 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();
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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() };
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
85
zonemaster-backend/share/sl.po
Normal file
85
zonemaster-backend/share/sl.po
Normal 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"
|
||||
92
zonemaster-backend/share/sv.po
Normal file
92
zonemaster-backend/share/sv.po
Normal 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"
|
||||
2
zonemaster-backend/share/tmpfiles.conf
Normal file
2
zonemaster-backend/share/tmpfiles.conf
Normal file
@@ -0,0 +1,2 @@
|
||||
#Type Path Mode UID GID Age Argument
|
||||
d /run/zonemaster 0755 zonemaster zonemaster - -
|
||||
9
zonemaster-backend/share/update-po
Executable file
9
zonemaster-backend/share/update-po
Executable 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"
|
||||
73
zonemaster-backend/share/zm-rpcapi.lsb
Normal file
73
zonemaster-backend/share/zm-rpcapi.lsb
Normal 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
|
||||
13
zonemaster-backend/share/zm-rpcapi.service
Normal file
13
zonemaster-backend/share/zm-rpcapi.service
Normal 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
|
||||
60
zonemaster-backend/share/zm-testagent.lsb
Normal file
60
zonemaster-backend/share/zm-testagent.lsb
Normal 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
|
||||
13
zonemaster-backend/share/zm-testagent.service
Normal file
13
zonemaster-backend/share/zm-testagent.service
Normal 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
|
||||
28
zonemaster-backend/share/zm_rpcapi-bsd
Executable file
28
zonemaster-backend/share/zm_rpcapi-bsd
Executable 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"
|
||||
46
zonemaster-backend/share/zm_testagent-bsd
Executable file
46
zonemaster-backend/share/zm_testagent-bsd
Executable 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"
|
||||
12
zonemaster-backend/t/00-load.t
Normal file
12
zonemaster-backend/t/00-load.t
Normal 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;
|
||||
197
zonemaster-backend/t/TestUtil.pm
Normal file
197
zonemaster-backend/t/TestUtil.pm
Normal 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;
|
||||
361
zonemaster-backend/t/batches.t
Normal file
361
zonemaster-backend/t/batches.t
Normal 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();
|
||||
891
zonemaster-backend/t/config.t
Normal file
891
zonemaster-backend/t/config.t
Normal 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
221
zonemaster-backend/t/db.t
Normal 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();
|
||||
150
zonemaster-backend/t/db_ddl.t
Normal file
150
zonemaster-backend/t/db_ddl.t
Normal 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();
|
||||
}
|
||||
26
zonemaster-backend/t/idn.data
Normal file
26
zonemaster-backend/t/idn.data
Normal 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
126
zonemaster-backend/t/idn.t
Normal 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();
|
||||
301
zonemaster-backend/t/lifecycle.t
Normal file
301
zonemaster-backend/t/lifecycle.t
Normal 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;
|
||||
}
|
||||
}
|
||||
37
zonemaster-backend/t/manifest.t
Normal file
37
zonemaster-backend/t/manifest.t
Normal 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';
|
||||
};
|
||||
237
zonemaster-backend/t/parameters_validation.t
Normal file
237
zonemaster-backend/t/parameters_validation.t
Normal 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;
|
||||
};
|
||||
66
zonemaster-backend/t/po-files.t
Normal file
66
zonemaster-backend/t/po-files.t
Normal 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();
|
||||
86
zonemaster-backend/t/queue.t
Normal file
86
zonemaster-backend/t/queue.t
Normal 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;
|
||||
}
|
||||
}
|
||||
242
zonemaster-backend/t/rpc_validation.t
Normal file
242
zonemaster-backend/t/rpc_validation.t
Normal 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";
|
||||
118
zonemaster-backend/t/test01.data
Normal file
118
zonemaster-backend/t/test01.data
Normal file
File diff suppressed because one or more lines are too long
425
zonemaster-backend/t/test01.t
Normal file
425
zonemaster-backend/t/test01.t
Normal 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();
|
||||
84
zonemaster-backend/t/test_profile.json
Normal file
84
zonemaster-backend/t/test_profile.json
Normal 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"
|
||||
]
|
||||
}
|
||||
85
zonemaster-backend/t/test_profile_network_true.json
Normal file
85
zonemaster-backend/t/test_profile_network_true.json
Normal 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"
|
||||
]
|
||||
}
|
||||
85
zonemaster-backend/t/test_profile_no_network.json
Normal file
85
zonemaster-backend/t/test_profile_no_network.json
Normal 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"
|
||||
]
|
||||
}
|
||||
289
zonemaster-backend/t/test_validate_syntax.t
Normal file
289
zonemaster-backend/t/test_validate_syntax.t
Normal 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 );
|
||||
}
|
||||
};
|
||||
81
zonemaster-backend/t/translator.t
Normal file
81
zonemaster-backend/t/translator.t
Normal 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;
|
||||
215
zonemaster-backend/t/validator.t
Normal file
215
zonemaster-backend/t/validator.t
Normal 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';
|
||||
};
|
||||
};
|
||||
44
zonemaster-backend/zonemaster_launch
Executable file
44
zonemaster-backend/zonemaster_launch
Executable 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;
|
||||
15
zonemaster-cli/.github/pull_request_template.md
vendored
Normal file
15
zonemaster-cli/.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
## Purpose
|
||||
|
||||
This PR...
|
||||
|
||||
## Context
|
||||
|
||||
(e.g. Fixes #9999, Follow-up to #9999, etc.)
|
||||
|
||||
## Changes
|
||||
|
||||
...
|
||||
|
||||
## How to test this PR
|
||||
|
||||
...
|
||||
112
zonemaster-cli/.github/workflows/ci.yml
vendored
Normal file
112
zonemaster-cli/.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
---
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- master
|
||||
- 'release/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- master
|
||||
- 'release/**'
|
||||
|
||||
jobs:
|
||||
run-tests:
|
||||
strategy:
|
||||
matrix:
|
||||
compatibility:
|
||||
- develop
|
||||
# - latest
|
||||
perl:
|
||||
- '5.40'
|
||||
- '5.36'
|
||||
- '5.26'
|
||||
runner:
|
||||
- ubuntu-22.04
|
||||
|
||||
runs-on: ${{ matrix.runner }}
|
||||
|
||||
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 Zonemaster dependencies (latest)
|
||||
if: ${{ matrix.compatibility == 'latest' }}
|
||||
run: |
|
||||
cpanm --sudo --notest \
|
||||
Module::Install \
|
||||
ExtUtils::PkgConfig \
|
||||
Zonemaster::Engine
|
||||
|
||||
- name: Install Zonemaster dependencies (develop)
|
||||
if: ${{ matrix.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
|
||||
perl Makefile.PL # Generate MYMETA.yml to appease cpanm .
|
||||
( cd zonemaster-ldns ; cpanm --sudo --notest . )
|
||||
rm -rf zonemaster-ldns
|
||||
git clone --branch=develop --depth=1 \
|
||||
https://github.com/zonemaster/zonemaster-engine.git
|
||||
perl Makefile.PL # Generate MYMETA.yml to appease cpanm .
|
||||
( cd zonemaster-engine ; cpanm --sudo --notest . )
|
||||
rm -rf 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 --verbose --notest --installdeps .
|
||||
|
||||
- name: Install Zonemaster::CLI
|
||||
run: |
|
||||
cpanm --sudo --verbose --notest .
|
||||
|
||||
- name: Show content of log files
|
||||
if: ${{ failure() }}
|
||||
run: cat /home/runner/.cpanm/work/*/build.log
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
prove -lv t
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user