spf-dkim-dmarc-demo/dns/web-api/lib/Email/SpoofingDemo/DNS/ZoneFile.pm

240 lines
5.0 KiB
Perl
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package Email::SpoofingDemo::DNS::ZoneFile;
use strict;
use warnings;
use utf8;
use v5.10;
use Exporter qw(import);
use File::Slurp;
use Try::Tiny;
our @EXPORT_OK = qw(get_txt get_zone_file import_zone_file remove_txt set_txt
zone_file_path);
sub safe_system {
my @argv = @_;
my $retval = system @argv;
die $! if ($retval == -1);
if ($retval != 0) {
my $status = $retval >> 8;
die join(" ", @argv) . " exited with status $status\n";
};
return 1;
}
sub is_dynamic_zone {
my ($zone, $dynamic_zones) = @_;
return grep { $_ eq $zone } @$dynamic_zones;
}
sub rndc_freeze {
my ($zone) = @_;
safe_system('rndc', 'freeze', $zone);
}
sub rndc_thaw {
my ($zone) = @_;
safe_system('rndc', 'thaw', $zone);
}
sub rndc_reload {
my ($zone) = @_;
safe_system('rndc', 'reload', $zone);
}
sub encode_txt {
my ($text) = @_;
my $chunk_length = 255;
my $text_bytes = $text;
utf8::encode($text_bytes);
my @parts;
for (my $i = 0; $i < length($text_bytes); $i += $chunk_length) {
my $part = substr($text_bytes, $i, 255);
$part =~ s/[\x00-\x1f"\x7f-\xff]/sprintf('\\%03d', ord($0))/eg;
push @parts, qq{"$part"};
}
return join(" ", @parts);
}
sub relative_to_absolute {
my ($zone, $relative_owner) = @_;
if ($relative_owner eq '') {
return $zone;
}
else {
return "$relative_owner.$zone";
}
}
sub nsupdate {
my ($zone, @instructions) = @_;
open(my $pipe, '|-', 'nsupdate') or die "Cant fork: $!";
say $pipe qq{zone $zone};
say $pipe $_ foreach (@instructions);
close($pipe);
return (($? >> 8) == 0);
}
sub get_txt {
my ($zone, $relative_owner, $filter, $sublabels_ok) = @_;
$filter //= qr{.*};
$sublabels_ok //= 0;
my @dig_opts = qw(+nottlid +noclass +norecurse +noall +answer @127.0.0.1);
my $owner_suffix = relative_to_absolute($zone, $relative_owner);
my $qname = ($sublabels_ok ? $zone : $owner_suffix);
my $qtype = ($sublabels_ok ? 'AXFR' : 'TXT');
my $owner_match_re = ($sublabels_ok ? qr/^([^.]+\.)*$owner_suffix\.$/i : qr/^$qname\.$/i);
my @result;
open(my $pipe, '-|', 'dig', @dig_opts, $qtype, $qname) or die "Cant fork: $!";
while (<$pipe>) {
chomp;
next if /^;/;
next if /^\s*$/;
my ($owner, $type, $data) = split(" ", $_, 3);
unless ($type eq 'TXT') {
next;
}
unless ($owner =~ $owner_match_re) {
next;
}
my $concat_data = ($data =~ s/"((?:[^\\"]|\\"|\\\d{3})*)"\s*/$1/gr);
unless ($concat_data =~ $filter) {
next;
}
push @result, { owner => ($owner =~ s/\.$//r), txt => $concat_data };
}
close($pipe);
if (wantarray) {
return @result;
}
else {
return $result[0];
}
}
sub set_txt {
my ($zone, $relative_owner, $txt) = @_;
my $owner = relative_to_absolute($zone, $relative_owner);
my $txt_encoded = encode_txt($txt);
nsupdate(
$zone,
"update delete $owner 0 IN TXT",
"update add $owner IN TXT $txt_encoded",
"send");
}
sub remove_txt {
my ($zone, $relative_owner) = @_;
my $owner = relative_to_absolute($zone, $relative_owner);
nsupdate(
$zone,
"update delete $owner 0 IN TXT",
"send");
}
sub check_zone {
my ($zone_contents, $origin) = @_;
open(my $pipe, '|-', 'named-checkzone', $origin, '-') or die "Cant fork: $!";
print $pipe $zone_contents;
close($pipe);
return (($? >> 8) == 0);
}
sub zone_file_path {
my ($zone, $base_dir, $force) = @_;
my @components = reverse(split(/\./, $zone));
my $path = $base_dir . '/' . (join('/', @components)) . ".zone";
if (not -f $path and not $force) {
return undef;
}
return $path;
}
sub get_zone_file {
my ($zone, $base_dir, $dynamic_zones) = @_;
my $path = zone_file_path($zone, $base_dir);
return undef if not defined $path;
rndc_freeze($zone) if is_dynamic_zone($zone, $dynamic_zones);
my $contents;
try {
$contents = read_file($path);
}
finally {
rndc_thaw($zone) if is_dynamic_zone($zone, $dynamic_zones);
};
return $contents;
}
sub import_zone_file {
my ($zone, $base_dir, $contents, $dynamic_zones) = @_;
if ($contents =~ /^\s*$/) {
warn "Zone to import is empty";
return undef;
}
my $path = zone_file_path($zone, $base_dir);
if (not defined $path) {
warn "No zone file for $zone in $base_dir";
return undef;
}
if (not check_zone($contents, $zone)) {
warn "Zone file contents do not pass syntax check";
return undef;
}
rndc_freeze($zone) if is_dynamic_zone($zone, $dynamic_zones);
try {
write_file($path, $contents);
rndc_reload($zone) unless is_dynamic_zone($zone, $dynamic_zones);
}
catch {
die $_;
}
finally {
rndc_thaw($zone) if is_dynamic_zone($zone, $dynamic_zones);
};
return 1;
}
1;